From 502c7680a2d99551a9e9b40d0bf8f333e276ad7e Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 3 Aug 2023 18:40:47 -0500 Subject: [PATCH 001/156] chore: fix release and security pipelines (#8891) --- .github/workflows/release.yaml | 12 ++---------- .github/workflows/security.yaml | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 95f07feda29ca..d78fbe7c5a5b9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -98,16 +98,8 @@ jobs: - name: Setup Go uses: ./.github/actions/setup-go - - name: Cache Node - id: cache-node - uses: buildjet/cache@v3 - with: - path: | - **/node_modules - .eslintcache - key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - js-${{ runner.os }}- + - name: Setup Node + uses: ./.github/actions/setup-node - name: Install nsis and zstd run: sudo apt-get install -y nsis zstd diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index daed19ee5dff5..48c8f16fd9bea 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -69,16 +69,8 @@ jobs: - name: Setup Go uses: ./.github/actions/setup-go - - name: Cache Node - id: cache-node - uses: buildjet/cache@v3 - with: - path: | - **/node_modules - .eslintcache - key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - js-${{ runner.os }}- + - name: Setup Node + uses: ./.github/actions/setup-node - name: Setup sqlc uses: ./.github/actions/setup-sqlc From e43608395cd2bd3aacf0af4103e4a98782d9698e Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 3 Aug 2023 19:46:02 -0500 Subject: [PATCH 002/156] feat: add frontend for locked workspaces (#8655) - Fix workspaces query for locked workspaces. --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/autobuild/lifecycle_executor.go | 2 +- coderd/database/dbauthz/dbauthz.go | 4 +- coderd/database/dbfake/dbfake.go | 20 ++- coderd/database/dbmetrics/dbmetrics.go | 6 +- coderd/database/dbmock/dbmock.go | 7 +- coderd/database/modelqueries.go | 1 + coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 41 ++++- coderd/database/queries/workspaces.sql | 13 +- coderd/searchquery/search.go | 6 + coderd/workspaces.go | 26 ++- coderd/workspaces_test.go | 40 +++++ docs/api/workspaces.md | 164 ++++++++++++++++-- site/src/api/api.ts | 15 ++ .../Dashboard/DashboardProvider.tsx | 10 ++ site/src/components/Workspace/Workspace.tsx | 31 ++-- .../components/WorkspaceActions/Buttons.tsx | 18 ++ .../WorkspaceActions/WorkspaceActions.tsx | 9 +- .../components/WorkspaceActions/constants.ts | 12 +- .../ImpendingDeletionBadge.tsx | 26 +-- .../ImpendingDeletionBanner.tsx | 96 ++++++---- .../WorkspaceStatusBadge.tsx | 6 +- site/src/i18n/en/templateSettingsPage.json | 6 +- .../InactivityDialog.stories.tsx | 2 +- .../TemplateScheduleForm/InactivityDialog.tsx | 35 +++- .../TemplateScheduleForm.tsx | 59 +++++-- .../useWorkspacesToBeDeleted.ts | 63 +++++-- .../TemplateSchedulePage.test.tsx | 8 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 1 + .../pages/WorkspacesPage/WorkspacesPage.tsx | 38 +++- .../WorkspacesPage/WorkspacesPageView.tsx | 39 ++--- .../pages/WorkspacesPage/filter/filter.tsx | 11 +- site/src/utils/filters.ts | 1 + .../xServices/workspace/workspaceXService.ts | 34 +++- 36 files changed, 664 insertions(+), 192 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2c115a30c3261..5d70095ee850c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6097,7 +6097,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7f7d12a51a53a..57f5c5051aa70 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5379,7 +5379,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index f7176ae8cd721..b36a265b7ee98 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -178,7 +178,7 @@ func (e *Executor) runOnce(t time.Time) Stats { // Lock the workspace if it has breached the template's // threshold for inactivity. if reason == database.BuildReasonAutolock { - err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{ + ws, err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{ ID: ws.ID, LockedAt: sql.NullTime{ Time: database.Now(), diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index ab865e2cc0f70..5e82320832442 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2587,11 +2587,11 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } -func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } - return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg) + return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg) } func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 0d5c19fa7656d..03ae01182dbd0 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5250,9 +5250,9 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.Workspace{}, err } q.mutex.Lock() defer q.mutex.Unlock() @@ -5274,7 +5274,7 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat } } if template.ID == uuid.Nil { - return xerrors.Errorf("unable to find workspace template") + return database.Workspace{}, xerrors.Errorf("unable to find workspace template") } if template.LockedTTL > 0 { workspace.DeletingAt = sql.NullTime{ @@ -5284,9 +5284,9 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat } } q.workspaces[index] = workspace - return nil + return workspace, nil } - return sql.ErrNoRows + return database.Workspace{}, sql.ErrNoRows } func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { @@ -5730,6 +5730,16 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } } + // We omit locked workspaces by default. + if arg.LockedAt.IsZero() && workspace.LockedAt.Valid { + continue + } + + // Filter out workspaces that are locked after the timestamp. + if !arg.LockedAt.IsZero() && workspace.LockedAt.Time.Before(arg.LockedAt) { + continue + } + if len(arg.TemplateIDs) > 0 { match := false for _, id := range arg.TemplateIDs { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index f78d9b44a46c0..ee7f8ae53b433 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1572,11 +1572,11 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas return err } -func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { start := time.Now() - r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg) + ws, r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg) m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedDeletingAt").Observe(time.Since(start).Seconds()) - return r0 + return ws, r0 } func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index adf573cb99e82..6a0edec4f015d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3307,11 +3307,12 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{ } // UpdateWorkspaceLockedDeletingAt mocks base method. -func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) error { +func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateWorkspaceLockedDeletingAt", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(database.Workspace) + ret1, _ := ret[1].(error) + return ret0, ret1 } // UpdateWorkspaceLockedDeletingAt indicates an expected call of UpdateWorkspaceLockedDeletingAt. diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index ffa346d04998c..c75f38fc3b596 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -217,6 +217,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, + arg.LockedAt, arg.Offset, arg.Limit, ) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 5e9da77b66c0a..783524375f822 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -277,7 +277,7 @@ type sqlcQuerier interface { UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error + UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) // This allows editing the properties of a workspace proxy. UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ff22cc3120193..ba0f3cfb54188 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9001,6 +9001,14 @@ WHERE ) > 0 ELSE true END + -- Filter by locked workspaces. By default we do not return locked + -- workspaces since they are considered soft-deleted. + AND CASE + WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN + locked_at IS NOT NULL AND locked_at >= $10 + ELSE + locked_at IS NULL + END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter ORDER BY @@ -9012,11 +9020,11 @@ ORDER BY LOWER(workspaces.name) ASC LIMIT CASE - WHEN $11 :: integer > 0 THEN - $11 + WHEN $12 :: integer > 0 THEN + $12 END OFFSET - $10 + $11 ` type GetWorkspacesParams struct { @@ -9029,6 +9037,7 @@ type GetWorkspacesParams struct { Name string `db:"name" json:"name"` HasAgent string `db:"has_agent" json:"has_agent"` AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"` + LockedAt time.Time `db:"locked_at" json:"locked_at"` Offset int32 `db:"offset_" json:"offset_"` Limit int32 `db:"limit_" json:"limit_"` } @@ -9064,6 +9073,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, + arg.LockedAt, arg.Offset, arg.Limit, ) @@ -9366,7 +9376,7 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo return err } -const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :exec +const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :one UPDATE workspaces SET @@ -9383,6 +9393,7 @@ WHERE workspaces.template_id = templates.id AND workspaces.id = $1 +RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at ` type UpdateWorkspaceLockedDeletingAtParams struct { @@ -9390,9 +9401,25 @@ type UpdateWorkspaceLockedDeletingAtParams struct { LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` } -func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt) - return err +func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) { + row := q.db.QueryRowContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt) + var i Workspace + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.OrganizationID, + &i.TemplateID, + &i.Deleted, + &i.Name, + &i.AutostartSchedule, + &i.Ttl, + &i.LastUsedAt, + &i.LockedAt, + &i.DeletingAt, + ) + return i, err } const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 5e540a0e5c90a..9dd8aa00b5f55 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -259,6 +259,14 @@ WHERE ) > 0 ELSE true END + -- Filter by locked workspaces. By default we do not return locked + -- workspaces since they are considered soft-deleted. + AND CASE + WHEN @locked_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN + locked_at IS NOT NULL AND locked_at >= @locked_at + ELSE + locked_at IS NULL + END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter ORDER BY @@ -474,7 +482,7 @@ WHERE ) ) AND workspaces.deleted = 'false'; --- name: UpdateWorkspaceLockedDeletingAt :exec +-- name: UpdateWorkspaceLockedDeletingAt :one UPDATE workspaces SET @@ -490,7 +498,8 @@ FROM WHERE workspaces.template_id = templates.id AND - workspaces.id = $1; + workspaces.id = $1 +RETURNING workspaces.*; -- name: UpdateWorkspacesDeletingAtByTemplateID :exec UPDATE diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 9b216d0180e15..3518f0744947e 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -114,9 +114,15 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT filter.Name = parser.String(values, "", "name") filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus])) filter.HasAgent = parser.String(values, "", "has-agent") + filter.LockedAt = parser.Time(values, time.Time{}, "locked_at", "2006-01-02") if _, ok := values["deleting_by"]; ok { postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02")) + // We want to make sure to grab locked workspaces since they + // are omitted by default. + if filter.LockedAt.IsZero() { + filter.LockedAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + } } parser.ErrorExcessParams(values) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0aa1cb0675155..8ad02faff33fb 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -768,7 +768,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { // @Tags Workspaces // @Param workspace path string true "Workspace ID" format(uuid) // @Param request body codersdk.UpdateWorkspaceLock true "Lock or unlock a workspace" -// @Success 200 {object} codersdk.Response +// @Success 200 {object} codersdk.Workspace // @Router /workspaces/{workspace}/lock [put] func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -779,9 +779,6 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { return } - code := http.StatusOK - resp := codersdk.Response{} - // If the workspace is already in the desired state do nothing! if workspace.LockedAt.Valid == req.Lock { httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{ @@ -797,7 +794,7 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { lockedAt.Time = database.Now() } - err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{ + workspace, err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{ ID: workspace.ID, LockedAt: lockedAt, }) @@ -809,10 +806,21 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { return } - // TODO should we kick off a build to stop the workspace if it's started - // from this endpoint? I'm leaning no to keep things simple and kick - // the responsibility back to the client. - httpapi.Write(ctx, rw, code, resp) + data, err := api.workspaceData(ctx, []database.Workspace{workspace}) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace( + workspace, + data.builds[0], + data.templates[0], + findUser(workspace.OwnerID, data.users), + )) } // @Summary Extend workspace deadline by ID diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index db5db020488b7..2213cd4657a70 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1407,6 +1407,46 @@ func TestWorkspaceFilterManual(t *testing.T) { // and template.InactivityTTL should be 0 assert.Len(t, res.Workspaces, 0) }) + + t.Run("LockedAt", func(t *testing.T) { + // this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(authToken), + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + // update template with inactivity ttl + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + // Create another workspace to validate that we do not return unlocked workspaces. + _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ + Lock: true, + }) + require.NoError(t, err) + + res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + FilterQuery: fmt.Sprintf("locked_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")), + }) + require.NoError(t, err) + require.Len(t, res.Workspaces, 1) + require.NotNil(t, res.Workspaces[0].LockedAt) + }) } func TestOffsetLimit(t *testing.T) { diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 390a82448886c..6cfb468f6af50 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -931,22 +931,164 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ ```json { - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] + "autostart_schedule": "string", + "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "last_used_at": "2019-08-24T14:15:22Z", + "latest_build": { + "build_number": 0, + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "deadline": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator_name": "string", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "MISSING_TEMPLATE_PARAMETER", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "queue_position": 0, + "queue_size": 0, + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "max_deadline": "2019-08-24T14:15:22Z", + "reason": "initiator", + "resources": [ + { + "agents": [ + { + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "sharing_level": "owner", + "slug": "string", + "subdomain": true, + "url": "string" + } + ], + "architecture": "string", + "connection_timeout_seconds": 0, + "created_at": "2019-08-24T14:15:22Z", + "directory": "string", + "disconnected_at": "2019-08-24T14:15:22Z", + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "expanded_directory": "string", + "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": false, + "reason": "agent has lost connection" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "instance_id": "string", + "last_connected_at": "2019-08-24T14:15:22Z", + "latency": { + "property1": { + "latency_ms": 0, + "preferred": true + }, + "property2": { + "latency_ms": 0, + "preferred": true + } + }, + "lifecycle_state": "created", + "login_before_ready": true, + "logs_length": 0, + "logs_overflowed": true, + "name": "string", + "operating_system": "string", + "ready_at": "2019-08-24T14:15:22Z", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, + "started_at": "2019-08-24T14:15:22Z", + "startup_script": "string", + "startup_script_behavior": "blocking", + "startup_script_timeout_seconds": 0, + "status": "connecting", + "subsystem": "envbox", + "troubleshooting_url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "version": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "hide": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", + "metadata": [ + { + "key": "string", + "sensitive": true, + "value": "string" + } + ], + "name": "string", + "type": "string", + "workspace_transition": "start" + } + ], + "status": "pending", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_name": "string", + "transition": "start", + "updated_at": "2019-08-24T14:15:22Z", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string", + "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", + "workspace_owner_name": "string" + }, + "locked_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "outdated": true, + "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", + "owner_name": "string", + "template_allow_user_cancel_workspace_jobs": true, + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "ttl_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 | [codersdk.Response](schemas.md#codersdkresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Workspace](schemas.md#codersdkworkspace) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/api.ts b/site/src/api/api.ts index bd2f36967f74c..4faea039e3792 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -554,6 +554,21 @@ export const cancelWorkspaceBuild = async ( return response.data } +export const updateWorkspaceLock = async ( + workspaceId: string, + lock: boolean, +): Promise => { + const data: TypesGen.UpdateWorkspaceLock = { + lock: lock, + } + + const response = await axios.put( + `/api/v2/workspaces/${workspaceId}/lock`, + data, + ) + return response.data +} + export const restartWorkspace = async ({ workspace, buildParameters, diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index fd9065abdb27a..ed26b64ffc481 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -87,3 +87,13 @@ export const useDashboard = (): DashboardProviderValue => { return context } + +export const useIsWorkspaceActionsEnabled = (): boolean => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + return allowWorkspaceActions && allowAdvancedScheduling +} diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 364bb50643bba..4e97fa0bb4b12 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -1,5 +1,6 @@ import Button from "@mui/material/Button" import { makeStyles } from "@mui/styles" +import LockIcon from "@mui/icons-material/Lock" import { Avatar } from "components/Avatar/Avatar" import { AgentRow } from "components/Resources/AgentRow" import { @@ -26,7 +27,7 @@ import { } from "components/PageHeader/FullWidthPageHeader" import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" import { ErrorAlert } from "components/Alert/ErrorAlert" -import { ImpendingDeletionBanner } from "components/WorkspaceDeletion" +import { LockedWorkspaceBanner } from "components/WorkspaceDeletion" import { useLocalStorage } from "hooks" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import AlertTitle from "@mui/material/AlertTitle" @@ -53,6 +54,7 @@ export interface WorkspaceProps { handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void + handleUnlock: () => void isUpdating: boolean isRestarting: boolean workspace: TypesGen.Workspace @@ -86,6 +88,7 @@ export const Workspace: FC> = ({ handleCancel, handleSettings, handleChangeVersion, + handleUnlock, workspace, isUpdating, isRestarting, @@ -167,14 +170,19 @@ export const Workspace: FC> = ({ <> - - {workspace.name} - + {workspace.locked_at ? ( + + ) : ( + + {workspace.name} + + )} +
{workspace.name} {workspace.owner_name} @@ -203,6 +211,7 @@ export const Workspace: FC> = ({ handleCancel={handleCancel} handleSettings={handleSettings} handleChangeVersion={handleChangeVersion} + handleUnlock={handleUnlock} canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} @@ -253,8 +262,8 @@ export const Workspace: FC> = ({ {/* determines its own visibility */} - = ({ ) } +export const UnlockButton: FC = ({ + handleAction, + loading, +}) => { + return ( + } + onClick={handleAction} + > + Unlock + + ) +} + export const StartButton: FC< Omit & { workspace: Workspace diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 8242b470c8c67..409c77e673eab 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -12,6 +12,7 @@ import { StopButton, RestartButton, UpdateButton, + UnlockButton, } from "./Buttons" import { ButtonMapping, @@ -33,6 +34,7 @@ export interface WorkspaceActionsProps { handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void + handleUnlock: () => void isUpdating: boolean isRestarting: boolean children?: ReactNode @@ -49,6 +51,7 @@ export const WorkspaceActions: FC = ({ handleCancel, handleSettings, handleChangeVersion, + handleUnlock, isUpdating, isRestarting, canChangeVersions, @@ -58,7 +61,7 @@ export const WorkspaceActions: FC = ({ canCancel, canAcceptJobs, actions: actionsByStatus, - } = actionsByWorkspaceStatus(workspace.latest_build.status) + } = actionsByWorkspaceStatus(workspace, workspace.latest_build.status) const canBeUpdated = workspace.outdated && canAcceptJobs const menuTriggerRef = useRef(null) const [isMenuOpen, setIsMenuOpen] = useState(false) @@ -93,6 +96,10 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.canceling]: , [ButtonTypesEnum.deleted]: , [ButtonTypesEnum.pending]: , + [ButtonTypesEnum.unlock]: , + [ButtonTypesEnum.unlocking]: ( + + ), } // Returns a function that will execute the action and close the menu diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index c4e81ca976ca5..c21fdedc98249 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -1,4 +1,4 @@ -import { WorkspaceStatus } from "api/typesGenerated" +import { Workspace, WorkspaceStatus } from "api/typesGenerated" import { ReactNode } from "react" // the button types we have @@ -12,6 +12,8 @@ export enum ButtonTypesEnum { deleting = "deleting", update = "update", updating = "updating", + unlock = "lock", + unlocking = "unlocking", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -29,8 +31,16 @@ interface WorkspaceAbilities { } export const actionsByWorkspaceStatus = ( + workspace: Workspace, status: WorkspaceStatus, ): WorkspaceAbilities => { + if (workspace.locked_at) { + return { + actions: [ButtonTypesEnum.unlock], + canCancel: false, + canAcceptJobs: false, + } + } return statusToActions[status] } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx index eb27db82a3e2e..dc09566fc69e9 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -1,31 +1,17 @@ import { Workspace } from "api/typesGenerated" -import { displayImpendingDeletion } from "./utils" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Pill } from "components/Pill/Pill" -import ErrorIcon from "@mui/icons-material/ErrorOutline" +import LockIcon from "@mui/icons-material/Lock" -export const ImpendingDeletionBadge = ({ +export const LockedBadge = ({ workspace, }: { workspace: Workspace }): JSX.Element | null => { - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - // return null - - if ( - !displayImpendingDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) - ) { + const experimentEnabled = useIsWorkspaceActionsEnabled() + if (!workspace.locked_at || !experimentEnabled) { return null } - return } text="Impending deletion" type="error" /> + return } text="Locked" type="error" /> } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index d793c16d5dc3a..501dd50dfa95f 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -1,8 +1,7 @@ import { Workspace } from "api/typesGenerated" -import { displayImpendingDeletion } from "./utils" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Alert } from "components/Alert/Alert" -import { formatDistanceToNow, differenceInDays, add, format } from "date-fns" +import { formatDistanceToNow } from "date-fns" import Link from "@mui/material/Link" import { Link as RouterLink } from "react-router-dom" @@ -11,69 +10,90 @@ export enum Count { Multiple, } -export const ImpendingDeletionBanner = ({ - workspace, +export const LockedWorkspaceBanner = ({ + workspaces, onDismiss, shouldRedisplayBanner, count = Count.Singular, }: { - workspace?: Workspace + workspaces?: Workspace[] onDismiss: () => void shouldRedisplayBanner: boolean count?: Count }): JSX.Element | null => { - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") + const experimentEnabled = useIsWorkspaceActionsEnabled() + + if (!workspaces) { + return null + } + + const hasLockedWorkspaces = workspaces.find( + (workspace) => workspace.locked_at, + ) + + const hasDeletionScheduledWorkspaces = workspaces.find( + (workspace) => workspace.deleting_at, + ) if ( - !workspace || - !displayImpendingDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) || + // Only show this if the experiment is included. + !experimentEnabled || + !hasLockedWorkspaces || // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion !shouldRedisplayBanner ) { return null } - // if deleting_at is 7 days away or less, display an 'error' banner to convey urgency to user - const daysUntilDelete = differenceInDays( - Date.parse(workspace.last_used_at), - new Date(), - ) + const formatDate = (dateStr: string): string => { + const date = new Date(dateStr) + return date.toLocaleDateString(undefined, { + month: "long", + day: "numeric", + year: "numeric", + }) + } - const plusFourteen = add(new Date(), { days: 14 }) + const alertText = (): string => { + if (workspaces.length === 1) { + if ( + hasDeletionScheduledWorkspaces && + hasDeletionScheduledWorkspaces.deleting_at && + hasDeletionScheduledWorkspaces.locked_at + ) { + return `This workspace has been locked since ${formatDistanceToNow( + Date.parse(hasDeletionScheduledWorkspaces.locked_at), + )} and is scheduled to be deleted at ${formatDate( + hasDeletionScheduledWorkspaces.deleting_at, + )} . To keep it you must unlock the workspace.` + } else if (hasLockedWorkspaces && hasLockedWorkspaces.locked_at) { + return `This workspace has been locked since ${formatDate( + hasLockedWorkspaces.locked_at, + )} + and cannot be interacted + with. Locked workspaces are eligible for + permanent deletion. To prevent deletion, unlock + the workspace.` + } + } + return "" + } return ( - + {count === Count.Singular ? ( - `This workspace has been unused for ${formatDistanceToNow( - Date.parse(workspace.last_used_at), - )} and is scheduled for deletion. To keep it, connect via SSH or the web terminal.` + alertText() ) : ( <> There are{" "} workspaces {" "} - that will be deleted soon due to inactivity. To keep these workspaces, - connect to them via SSH or the web terminal. + that may be deleted soon due to inactivity. Unlock the workspaces you + wish to retain. )} diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 7f742dab2ff3c..2ace2d0b903b8 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles" import { combineClasses } from "utils/combineClasses" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { - ImpendingDeletionBadge, + LockedBadge, ImpendingDeletionText, } from "components/WorkspaceDeletion" import { getDisplayWorkspaceStatus } from "utils/workspace" @@ -25,8 +25,8 @@ export const WorkspaceStatusBadge: FC< return ( {/* determines its own visibility */} - - + + diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index 11f314e0a7472..d1b0927b5a78a 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -22,9 +22,9 @@ "failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces", "failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.", "failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.", - "inactivityTTLHelperText_zero": "Coder will not automatically delete inactive workspaces", - "inactivityTTLHelperText_one": "Coder will automatically delete inactive workspaces after {{count}} day.", - "inactivityTTLHelperText_other": "Coder will automatically delete inactive workspaces after {{count}} days.", + "inactivityTTLHelperText_zero": "Coder will not automatically lock inactive workspaces", + "inactivityTTLHelperText_one": "Coder will automatically lock inactive workspaces after {{count}} day.", + "inactivityTTLHelperText_other": "Coder will automatically lock inactive workspaces after {{count}} days.", "lockedTTLHelperText_zero": "Coder will not automatically delete locked workspaces", "lockedTTLHelperText_one": "Coder will automatically delete locked workspaces after {{count}} day.", "lockedTTLHelperText_other": "Coder will automatically delete locked workspaces after {{count}} days.", diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx index 645c7dfb84ddd..6128700299e28 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx @@ -14,6 +14,6 @@ export const OpenDialog: Story = { submitValues: () => null, isInactivityDialogOpen: true, setIsInactivityDialogOpen: () => null, - workspacesToBeDeletedToday: 2, + workspacesToBeLockedToday: 2, }, } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx index 3f5ec252b08be..a9407f6d5eab5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx @@ -4,12 +4,12 @@ export const InactivityDialog = ({ submitValues, isInactivityDialogOpen, setIsInactivityDialogOpen, - workspacesToBeDeletedToday, + workspacesToBeLockedToday, }: { submitValues: () => void isInactivityDialogOpen: boolean setIsInactivityDialogOpen: (arg0: boolean) => void - workspacesToBeDeletedToday: number + workspacesToBeLockedToday: number }) => { return ( setIsInactivityDialogOpen(false)} - title="Delete inactive workspaces" + title="Lock inactive workspaces" + confirmText="Lock Workspaces" + description={`There are ${ + workspacesToBeLockedToday ? workspacesToBeLockedToday : "" + } workspaces that already match this filter and will be locked upon form submission. Are you sure you want to proceed?`} + /> + ) +} + +export const DeleteLockedDialog = ({ + submitValues, + isLockedDialogOpen, + setIsLockedDialogOpen, + workspacesToBeDeletedToday, +}: { + submitValues: () => void + isLockedDialogOpen: boolean + setIsLockedDialogOpen: (arg0: boolean) => void + workspacesToBeDeletedToday: number +}) => { + return ( + { + submitValues() + setIsLockedDialogOpen(false) + }} + onClose={() => setIsLockedDialogOpen(false)} + title="Delete Locked Workspaces" confirmText="Delete Workspaces" description={`There are ${ workspacesToBeDeletedToday ? workspacesToBeDeletedToday : "" diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 598f701338f1e..1efdcec885cfb 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -16,8 +16,11 @@ import Link from "@mui/material/Link" import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import Switch from "@mui/material/Switch" -import { InactivityDialog } from "./InactivityDialog" -import { useWorkspacesToBeDeleted } from "./useWorkspacesToBeDeleted" +import { DeleteLockedDialog, InactivityDialog } from "./InactivityDialog" +import { + useWorkspacesToBeLocked, + useWorkspacesToBeDeleted, +} from "./useWorkspacesToBeDeleted" import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers" import { TTLHelperText } from "./TTLHelperText" import { docs } from "utils/docs" @@ -89,10 +92,16 @@ export const TemplateScheduleForm: FC = ({ onSubmit: () => { if ( form.values.inactivity_cleanup_enabled && + workspacesToBeLockedToday && + workspacesToBeLockedToday.length > 0 + ) { + setIsInactivityDialogOpen(true) + } else if ( + form.values.locked_cleanup_enabled && workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 ) { - setIsInactivityDialogOpen(true) + setIsLockedDialogOpen(true) } else { submitValues() } @@ -106,10 +115,18 @@ export const TemplateScheduleForm: FC = ({ const { t } = useTranslation("templateSettingsPage") const styles = useStyles() - const workspacesToBeDeletedToday = useWorkspacesToBeDeleted(form.values) + const workspacesToBeLockedToday = useWorkspacesToBeLocked( + template, + form.values, + ) + const workspacesToBeDeletedToday = useWorkspacesToBeDeleted( + template, + form.values, + ) const [isInactivityDialogOpen, setIsInactivityDialogOpen] = useState(false) + const [isLockedDialogOpen, setIsLockedDialogOpen] = useState(false) const submitValues = () => { // on submit, convert from hours => ms @@ -324,12 +341,11 @@ export const TemplateScheduleForm: FC = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Failure Cleanup" /> @@ -341,7 +357,7 @@ export const TemplateScheduleForm: FC = ({ onChange={handleToggleInactivityCleanup} /> } - label="Enable Inactivity Cleanup" + label="Enable Inactivity TTL" /> = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Inactivity Cleanup" /> @@ -375,7 +390,7 @@ export const TemplateScheduleForm: FC = ({ onChange={handleToggleLockedCleanup} /> } - label="Enable Locked Cleanup" + label="Enable Locked TTL" /> = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Locked Cleanup" /> )} - + {workspacesToBeLockedToday && workspacesToBeLockedToday.length > 0 && ( + + )} + {workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 && ( + + )} + { + const { data: workspacesData } = useQuery({ + queryKey: ["workspaces"], + queryFn: () => + getWorkspaces({ + q: "template:" + template.name, + }), + enabled: formValues.inactivity_cleanup_enabled, + }) + + return workspacesData?.workspaces?.filter((workspace: Workspace) => { + if (!formValues.inactivity_ttl_ms) { + return + } + + if (workspace.locked_at) { + return + } + + const proposedLocking = new Date( + new Date(workspace.last_used_at).getTime() + + formValues.inactivity_ttl_ms * 86400000, + ) + + if (compareAsc(proposedLocking, new Date()) < 1) { + return workspace + } + }) +} export const useWorkspacesToBeDeleted = ( + template: Template, formValues: TemplateScheduleFormValues, ) => { const { data: workspacesData } = useQuery({ queryKey: ["workspaces"], - queryFn: () => getWorkspaces({}), - enabled: formValues.inactivity_cleanup_enabled, + queryFn: () => + getWorkspaces({ + q: "template:" + template.name, + }), + enabled: formValues.locked_cleanup_enabled, }) return workspacesData?.workspaces?.filter((workspace: Workspace) => { - const isInactive = inactiveStatuses.includes(workspace.latest_build.status) + if (!workspace.locked_at || !formValues.locked_ttl_ms) { + return false + } - const proposedDeletion = add(new Date(workspace.last_used_at), { - days: formValues.inactivity_ttl_ms, - }) + const proposedLocking = new Date( + new Date(workspace.locked_at).getTime() + + formValues.locked_ttl_ms * 86400000, + ) - if (isInactive && compareAsc(proposedDeletion, endOfToday()) < 1) { + if (compareAsc(proposedLocking, new Date()) < 1) { return workspace } }) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 5b37f01f5b8ce..15612a544d89e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -63,12 +63,12 @@ const fillAndSubmitForm = async ({ await user.type(failureTtlField, failure_ttl_ms.toString()) const inactivityTtlField = screen.getByRole("checkbox", { - name: /Inactivity Cleanup/i, + name: /Inactivity TTL/i, }) await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) const lockedTtlField = screen.getByRole("checkbox", { - name: /Locked Cleanup/i, + name: /Locked TTL/i, }) await user.type(lockedTtlField, locked_ttl_ms.toString()) @@ -76,6 +76,10 @@ const fillAndSubmitForm = async ({ FooterFormLanguage.defaultSubmitLabel, ) await user.click(submitButton) + + // User needs to confirm inactivity and locked ttl + const confirmButton = await screen.findByTestId("confirm-button") + await user.click(confirmButton) } describe("TemplateSchedulePage", () => { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 4407f8f017775..3fd3124399da3 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -176,6 +176,7 @@ export const WorkspaceReadyPage = ({ handleChangeVersion={() => { setChangeVersionDialogOpen(true) }} + handleUnlock={() => workspaceSend({ type: "UNLOCK" })} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 112a89ae5eaa1..6194f1fc4add8 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,5 +1,7 @@ import { usePagination } from "hooks/usePagination" -import { FC } from "react" +import { Workspace } from "api/typesGenerated" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" +import { FC, useEffect, useState } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "utils/page" import { useWorkspacesData, useWorkspaceUpdate } from "./data" @@ -9,8 +11,10 @@ import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus" import { useSearchParams } from "react-router-dom" import { useFilter } from "components/Filter/filter" import { useUserFilterMenu } from "components/Filter/UserFilter" +import { getWorkspaces } from "api/api" const WorkspacesPage: FC = () => { + const [lockedWorkspaces, setLockedWorkspaces] = useState([]) // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to // each hook. @@ -21,6 +25,37 @@ const WorkspacesPage: FC = () => { ...pagination, query: filterProps.filter.query, }) + + const experimentEnabled = useIsWorkspaceActionsEnabled() + // If workspace actions are enabled we need to fetch the locked + // workspaces as well. This lets us determine whether we should + // show a banner to the user indicating that some of their workspaces + // are at risk of being deleted. + useEffect(() => { + if (experimentEnabled) { + const includesLocked = filterProps.filter.query.includes("locked_at") + const lockedQuery = includesLocked + ? filterProps.filter.query + : filterProps.filter.query + " locked_at:1970-01-01" + + if (includesLocked && data) { + setLockedWorkspaces(data.workspaces) + } else { + getWorkspaces({ q: lockedQuery }) + .then((resp) => { + setLockedWorkspaces(resp.workspaces) + }) + .catch(() => { + // TODO + }) + } + } else { + // If the experiment isn't included then we'll pretend + // like locked workspaces don't exist. + setLockedWorkspaces([]) + } + }, [experimentEnabled, data, filterProps.filter.query]) + const updateWorkspace = useWorkspaceUpdate(queryKey) return ( @@ -31,6 +66,7 @@ const WorkspacesPage: FC = () => { page: number @@ -45,6 +45,7 @@ export const WorkspacesPageView: FC< React.PropsWithChildren > = ({ workspaces, + lockedWorkspaces, error, limit, count, @@ -53,32 +54,14 @@ export const WorkspacesPageView: FC< onUpdateWorkspace, page, }) => { - const { saveLocal, getLocal } = useLocalStorage() + const { saveLocal } = useLocalStorage() - const workspaceIdsWithImpendingDeletions = workspaces + const workspacesDeletionScheduled = lockedWorkspaces ?.filter((workspace) => workspace.deleting_at) .map((workspace) => workspace.id) - /** - * Returns a boolean indicating if there are workspaces that have been - * recently marked for deletion but are not in local storage. - * If there are, we want to alert the user so they can potentially take action - * before deletion takes place. - * @returns {boolean} - */ - const isNewWorkspacesImpendingDeletion = (): boolean => { - const dismissedList = getLocal("dismissedWorkspaceList") - if (!dismissedList) { - return true - } - - const diff = difference( - workspaceIdsWithImpendingDeletions, - JSON.parse(dismissedList), - ) - - return diff && diff.length > 0 - } + const hasLockedWorkspace = + lockedWorkspaces !== undefined && lockedWorkspaces.length > 0 return ( @@ -104,13 +87,13 @@ export const WorkspacesPageView: FC< {/* determines its own visibility */} - workspace.deleting_at)} - shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()} + saveLocal( "dismissedWorkspaceList", - JSON.stringify(workspaceIdsWithImpendingDeletions), + JSON.stringify(workspacesDeletionScheduled), ) } count={Count.Multiple} diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 2772347d995cf..8d98c0b03182f 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,5 +1,6 @@ import { FC } from "react" import Box from "@mui/material/Box" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Avatar, AvatarProps } from "components/Avatar/Avatar" import { Palette, PaletteColor } from "@mui/material/styles" import { TemplateFilterMenu, StatusFilterMenu } from "./menus" @@ -43,9 +44,17 @@ export const WorkspacesFilter = ({ status: StatusFilterMenu } }) => { + const presets = [...PRESET_FILTERS] + if (useIsWorkspaceActionsEnabled()) { + presets.push({ + query: workspaceFilterQuery.locked, + name: "Locked workspaces", + }) + } + return ( { + const message = getErrorMessage(data, "Error unlocking workspace.") + displayError(message) + }, assignMissedParameters: assign({ missedParameters: (_, { data }) => { if (!(data instanceof API.MissingBuildParameters)) { @@ -675,6 +695,18 @@ export const workspaceMachine = createMachine( throw Error("Cannot cancel workspace without build id") } }, + unlockWorkspace: (context) => async (send) => { + if (context.workspace) { + const unlockWorkspacePromise = await API.updateWorkspaceLock( + context.workspace.id, + false, + ) + send({ type: "REFRESH_WORKSPACE", data: unlockWorkspacePromise }) + return unlockWorkspacePromise + } else { + throw Error("Cannot unlock workspace without workspace id") + } + }, listening: (context) => (send) => { if (!context.eventSource) { send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" }) From 2e45a0ffd7bd4f83bd51837317033dd3d425f07b Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Fri, 4 Aug 2023 00:52:59 -0400 Subject: [PATCH 003/156] fix(helm): set correct prom port in helm notes (#8888) --- helm/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index e3a1298628d5b..43d317507d609 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -10,7 +10,7 @@ coder: # - CODER_TLS_ENABLE: set if tls.secretName is not empty. # - CODER_TLS_CERT_FILE: set if tls.secretName is not empty. # - CODER_TLS_KEY_FILE: set if tls.secretName is not empty. - # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:6060 and cannot be changed. + # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:2112 and cannot be changed. # Prometheus must still be enabled by setting CODER_PROMETHEUS_ENABLE. # - KUBE_POD_IP # - CODER_DERP_SERVER_RELAY_URL From b77d6b2c84ddcecb35d16e7b4a1f80548a9f8606 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 4 Aug 2023 09:55:38 +0300 Subject: [PATCH 004/156] ci: delete comments by github-action[bot] (#8896) --- .github/workflows/pr-cleanup.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index 510c8f4299361..bb4f7146c7025 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -71,3 +71,10 @@ jobs: run: | set -euxo pipefail kubectl delete certificate "pr${{ steps.pr_number.outputs.PR_NUMBER }}-tls" -n pr-deployment-certs || echo "certificate not found" + + - name: Delete PR Comments + uses: izhangzhihao/delete-comment@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + delete_user_name: github-actions[bot] + issue_number: ${{ github.event.number }} From cb4989cd8d9d67238fcfc39cfd876263f8605a95 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 4 Aug 2023 12:32:28 +0400 Subject: [PATCH 005/156] feat: add PSK for external provisionerd auth (#8877) Signed-off-by: Spike Curtis --- cli/root.go | 18 +- cli/testdata/coder_server_--help.golden | 4 + cli/testdata/server-config.yaml.golden | 3 + coderd/apidoc/docs.go | 3 + coderd/apidoc/swagger.json | 3 + coderd/coderdtest/coderdtest.go | 6 +- codersdk/client.go | 3 + codersdk/deployment.go | 10 + codersdk/organizations.go | 5 +- codersdk/provisionerdaemons.go | 51 +++-- docs/api/general.md | 1 + docs/api/schemas.md | 4 + docs/cli/provisionerd_start.md | 9 + docs/cli/server.md | 10 + enterprise/cli/provisionerdaemons.go | 24 ++- enterprise/cli/provisionerdaemons_test.go | 56 +++++ enterprise/cli/server.go | 1 + .../coder_provisionerd_start_--help.golden | 3 + .../cli/testdata/coder_server_--help.golden | 4 + enterprise/coderd/coderd.go | 24 ++- .../coderd/coderdenttest/coderdenttest.go | 2 + enterprise/coderd/provisionerdaemons.go | 53 +++-- enterprise/coderd/provisionerdaemons_test.go | 192 ++++++++++++++++-- site/src/api/typesGenerated.ts | 1 + 24 files changed, 429 insertions(+), 61 deletions(-) create mode 100644 enterprise/cli/provisionerdaemons_test.go diff --git a/cli/root.go b/cli/root.go index 4c268235a0f96..036be18a01300 100644 --- a/cli/root.go +++ b/cli/root.go @@ -494,6 +494,15 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) { // InitClient sets client to a new client. // It reads from global configuration files if flags are not set. func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc { + return r.initClientInternal(client, false) +} + +func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) clibase.MiddlewareFunc { + return r.initClientInternal(client, true) +} + +// nolint: revive +func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) clibase.MiddlewareFunc { if client == nil { panic("client is nil") } @@ -508,7 +517,7 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc { rawURL, err := conf.URL().Read() // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return (errUnauthenticated) + return errUnauthenticated } if err != nil { return err @@ -524,9 +533,10 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc { r.token, err = conf.Session().Read() // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return (errUnauthenticated) - } - if err != nil { + if !allowTokenMissing { + return errUnauthenticated + } + } else if err != nil { return err } } diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index cb7ca61b4913a..121ce98a98bd7 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -373,6 +373,10 @@ updating, and deleting workspace resources. --provisioner-daemon-poll-jitter duration, $CODER_PROVISIONER_DAEMON_POLL_JITTER (default: 100ms) Random jitter added to the poll interval. + --provisioner-daemon-psk string, $CODER_PROVISIONER_DAEMON_PSK + Pre-shared key to authenticate external provisioner daemons to Coder + server. + --provisioner-daemons int, $CODER_PROVISIONER_DAEMONS (default: 3) Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index c7a8df03414e6..7eab5aba07ecc 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -327,6 +327,9 @@ provisioning: # Time to force cancel provisioning tasks that are stuck. # (default: 10m0s, type: duration) forceCancelInterval: 10m0s + # Pre-shared key to authenticate external provisioner daemons to Coder server. + # (default: , type: string) + daemonPSK: "" # Enable one or more experiments. These are not ready for production. Separate # multiple experiments with commas, or enter '*' to opt-in to all available # experiments. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 5d70095ee850c..8d7920c2d2e8d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8753,6 +8753,9 @@ const docTemplate = `{ "daemon_poll_jitter": { "type": "integer" }, + "daemon_psk": { + "type": "string" + }, "daemons": { "type": "integer" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 57f5c5051aa70..bc4c0e7b8a958 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7863,6 +7863,9 @@ "daemon_poll_jitter": { "type": "integer" }, + "daemon_psk": { + "type": "string" + }, "daemons": { "type": "integer" }, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 4d4fbb8c5e78e..351a6d0d9a075 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -497,7 +497,11 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui }() closer := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return client.ServeProvisionerDaemon(ctx, org, []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, tags) + return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: org, + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, + Tags: tags, + }) }, &provisionerd.Options{ Filesystem: fs, Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), diff --git a/codersdk/client.go b/codersdk/client.go index ad9e46ccf7f4a..98a109aa725e1 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -71,6 +71,9 @@ const ( // command that was invoked to produce the request. It is for internal use // only. CLITelemetryHeader = "Coder-CLI-Telemetry" + + // ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon + ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK" ) // loggableMimeTypes is a list of MIME types that are safe to log diff --git a/codersdk/deployment.go b/codersdk/deployment.go index af861138e6d7d..e714d9c1c34b5 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -328,6 +328,7 @@ type ProvisionerConfig struct { DaemonPollInterval clibase.Duration `json:"daemon_poll_interval" typescript:",notnull"` DaemonPollJitter clibase.Duration `json:"daemon_poll_jitter" typescript:",notnull"` ForceCancelInterval clibase.Duration `json:"force_cancel_interval" typescript:",notnull"` + DaemonPSK clibase.String `json:"daemon_psk" typescript:",notnull"` } type RateLimitConfig struct { @@ -1230,6 +1231,15 @@ when required by your organization's security policy.`, Group: &deploymentGroupProvisioning, YAML: "forceCancelInterval", }, + { + Name: "Provisioner Daemon Pre-shared Key (PSK)", + Description: "Pre-shared key to authenticate external provisioner daemons to Coder server.", + Flag: "provisioner-daemon-psk", + Env: "CODER_PROVISIONER_DAEMON_PSK", + Value: &c.Provisioner.DaemonPSK, + Group: &deploymentGroupProvisioning, + YAML: "daemonPSK", + }, // RateLimit settings { Name: "Disable All Rate Limits", diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 26290fd4f4761..96b026a3197a5 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -149,10 +149,11 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, return organization, json.NewDecoder(res.Body).Decode(&organization) } -// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization. +// ProvisionerDaemons returns provisioner daemons available. func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) { res, err := c.Request(ctx, http.MethodGet, - "/api/v2/provisionerdaemons", + // TODO: the organization path parameter is currently ignored. + "/api/v2/organizations/default/provisionerdaemons", nil, ) if err != nil { diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 1c9378f718b3a..674523055e06f 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -164,38 +164,61 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after }), nil } -// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon +// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with +// @typescript-ignore ServeProvisionerDaemonRequest +type ServeProvisionerDaemonRequest struct { + // Organization is the organization for the URL. At present provisioner daemons ARE NOT scoped to organizations + // and so the organization ID is optional. + Organization uuid.UUID `json:"organization" format:"uuid"` + // Provisioners is a list of provisioner types hosted by the provisioner daemon + Provisioners []ProvisionerType `json:"provisioners"` + // Tags is a map of key-value pairs that tag the jobs this provisioner daemon can handle + Tags map[string]string `json:"tags"` + // PreSharedKey is an authentication key to use on the API instead of the normal session token from the client. + PreSharedKey string `json:"pre_shared_key"` +} + +// ServeProvisionerDaemon returns the gRPC service for a provisioner daemon // implementation. The context is during dial, not during the lifetime of the // client. Client should be closed after use. -func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.UUID, provisioners []ProvisionerType, tags map[string]string) (proto.DRPCProvisionerDaemonClient, error) { - serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", organization)) +func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisionerDaemonRequest) (proto.DRPCProvisionerDaemonClient, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", req.Organization)) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } query := serverURL.Query() - for _, provisioner := range provisioners { + for _, provisioner := range req.Provisioners { query.Add("provisioner", string(provisioner)) } - for key, value := range tags { + for key, value := range req.Tags { query.Add("tag", fmt.Sprintf("%s=%s", key, value)) } serverURL.RawQuery = query.Encode() - jar, err := cookiejar.New(nil) - if err != nil { - return nil, xerrors.Errorf("create cookie jar: %w", err) - } - jar.SetCookies(serverURL, []*http.Cookie{{ - Name: SessionTokenCookie, - Value: c.SessionToken(), - }}) httpClient := &http.Client{ - Jar: jar, Transport: c.HTTPClient.Transport, } + headers := http.Header{} + + if req.PreSharedKey == "" { + // use session token if we don't have a PSK. + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(serverURL, []*http.Cookie{{ + Name: SessionTokenCookie, + Value: c.SessionToken(), + }}) + httpClient.Jar = jar + } else { + headers.Set(ProvisionerDaemonPSK, req.PreSharedKey) + } + conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ HTTPClient: httpClient, // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, + HTTPHeader: headers, }) if err != nil { if res == nil { diff --git a/docs/api/general.md b/docs/api/general.md index bb64823bd86c9..3f1f90a02d851 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -305,6 +305,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "provisioner": { "daemon_poll_interval": 0, "daemon_poll_jitter": 0, + "daemon_psk": "string", "daemons": 0, "daemons_echo": true, "force_cancel_interval": 0 diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 39bc8d16b6e6f..192cc9625eddd 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2099,6 +2099,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "provisioner": { "daemon_poll_interval": 0, "daemon_poll_jitter": 0, + "daemon_psk": "string", "daemons": 0, "daemons_echo": true, "force_cancel_interval": 0 @@ -2456,6 +2457,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "provisioner": { "daemon_poll_interval": 0, "daemon_poll_jitter": 0, + "daemon_psk": "string", "daemons": 0, "daemons_echo": true, "force_cancel_interval": 0 @@ -3485,6 +3487,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "daemon_poll_interval": 0, "daemon_poll_jitter": 0, + "daemon_psk": "string", "daemons": 0, "daemons_echo": true, "force_cancel_interval": 0 @@ -3497,6 +3500,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | ----------------------- | ------- | -------- | ------------ | ----------- | | `daemon_poll_interval` | integer | false | | | | `daemon_poll_jitter` | integer | false | | | +| `daemon_psk` | string | false | | | | `daemons` | integer | false | | | | `daemons_echo` | boolean | false | | | | `force_cancel_interval` | integer | false | | | diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md index 583e520389150..b129605933db3 100644 --- a/docs/cli/provisionerd_start.md +++ b/docs/cli/provisionerd_start.md @@ -42,6 +42,15 @@ How often to poll for provisioner jobs. How much to jitter the poll interval by. +### --psk + +| | | +| ----------- | ------------------------------------------ | +| Type | string | +| Environment | $CODER_PROVISIONER_DAEMON_PSK | + +Pre-shared key to authenticate with Coder server. + ### -t, --tag | | | diff --git a/docs/cli/server.md b/docs/cli/server.md index 90c60d7392f00..9591dc8041f9f 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -668,6 +668,16 @@ Collect database metrics (may increase charges for metrics storage). Serve prometheus metrics on the address defined by prometheus address. +### --provisioner-daemon-psk + +| | | +| ----------- | ------------------------------------------ | +| Type | string | +| Environment | $CODER_PROVISIONER_DAEMON_PSK | +| YAML | provisioning.daemonPSK | + +Pre-shared key to authenticate external provisioner daemons to Coder server. + ### --provisioner-daemons | | | diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index f3dfc2ba367d7..baca9fa4b3296 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -44,13 +44,14 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { rawTags []string pollInterval time.Duration pollJitter time.Duration + preSharedKey string ) client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "start", Short: "Run a provisioner daemon", Middleware: clibase.Chain( - r.InitClient(client), + r.InitClientMissingTokenOK(client), ), Handler: func(inv *clibase.Invocation) error { ctx, cancel := context.WithCancel(inv.Context()) @@ -59,11 +60,6 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { notifyCtx, notifyStop := signal.NotifyContext(ctx, agpl.InterruptSignals...) defer notifyStop() - org, err := agpl.CurrentOrganization(inv, client) - if err != nil { - return xerrors.Errorf("get current organization: %w", err) - } - tags, err := agpl.ParseProvisionerTags(rawTags) if err != nil { return err @@ -112,9 +108,13 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient), } srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return client.ServeProvisionerDaemon(ctx, org.ID, []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeTerraform, - }, tags) + return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeTerraform, + }, + Tags: tags, + PreSharedKey: preSharedKey, + }) }, &provisionerd.Options{ Logger: logger, JobPollInterval: pollInterval, @@ -182,6 +182,12 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { Default: (100 * time.Millisecond).String(), Value: clibase.DurationOf(&pollJitter), }, + { + Flag: "psk", + Env: "CODER_PROVISIONER_DAEMON_PSK", + Description: "Pre-shared key to authenticate with Coder server.", + Value: clibase.StringOf(&preSharedKey), + }, } return cmd diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemons_test.go new file mode 100644 index 0000000000000..69b23d870757c --- /dev/null +++ b/enterprise/cli/provisionerdaemons_test.go @@ -0,0 +1,56 @@ +package cli_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" + "github.com/coder/coder/pty/ptytest" + "github.com/coder/coder/testutil" +) + +func TestProvisionerDaemon_PSK(t *testing.T) { + t.Parallel() + + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + ProvisionerDaemonPSK: "provisionersftw", + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw") + err := conf.URL().Write(client.URL.String()) + require.NoError(t, err) + pty := ptytest.New(t).Attach(inv) + ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong) + defer cancel() + clitest.Start(t, inv) + pty.ExpectMatchContext(ctx, "starting provisioner daemon") +} + +func TestProvisionerDaemon_SessionToken(t *testing.T) { + t.Parallel() + + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + ProvisionerDaemonPSK: "provisionersftw", + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + inv, conf := newCLI(t, "provisionerd", "start") + clitest.SetupConfig(t, client, conf) + pty := ptytest.New(t).Attach(inv) + ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong) + defer cancel() + clitest.Start(t, inv) + pty.ExpectMatchContext(ctx, "starting provisioner daemon") +} diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index b0561a0de1850..70a06ff0548e4 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -66,6 +66,7 @@ func (r *RootCmd) server() *clibase.Cmd { DERPServerRegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()), ProxyHealthInterval: options.DeploymentValues.ProxyHealthStatusInterval.Value(), DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(), + ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(), } api, err := coderd.New(ctx, o) diff --git a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden index 1236cfb5ae7e1..5258c33125173 100644 --- a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden +++ b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden @@ -12,6 +12,9 @@ Run a provisioner daemon --poll-jitter duration, $CODER_PROVISIONERD_POLL_JITTER (default: 100ms) How much to jitter the poll interval by. + --psk string, $CODER_PROVISIONER_DAEMON_PSK + Pre-shared key to authenticate with Coder server. + -t, --tag string-array, $CODER_PROVISIONERD_TAGS Tags to filter provisioner jobs by. diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index cb7ca61b4913a..121ce98a98bd7 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -373,6 +373,10 @@ updating, and deleting workspace resources. --provisioner-daemon-poll-jitter duration, $CODER_PROVISIONER_DAEMON_POLL_JITTER (default: 100ms) Random jitter added to the poll interval. + --provisioner-daemon-psk string, $CODER_PROVISIONER_DAEMON_PSK + Pre-shared key to authenticate external provisioner daemons to Coder + server. + --provisioner-daemons int, $CODER_PROVISIONER_DAEMONS (default: 3) Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this. diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index c3cc5e0be5ccd..71d975e3ef1d6 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -67,6 +67,10 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { AGPL: coderd.New(options.Options), Options: options, + provisionerDaemonAuth: &provisionerDaemonAuth{ + psk: options.ProvisionerDaemonPSK, + authorizer: options.Authorizer, + }, } defer func() { if err != nil { @@ -193,14 +197,21 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Get("/", api.groupByOrganization) }) }) + // TODO: provisioner daemons are not scoped to organizations in the database, so placing them + // under an organization route doesn't make sense. In order to allow the /serve endpoint to + // work with a pre-shared key (PSK) without an API key, these routes will simply ignore the + // value of {organization}. That is, the route will work with any organization ID, whether or + // not it exits. This doesn't leak any information about the existence of organizations, so is + // fine from a security perspective, but might be a little surprising. + // + // We may in future decide to scope provisioner daemons to organizations, so we'll keep the API + // route as is. r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) { r.Use( api.provisionerDaemonsEnabledMW, - apiKeyMiddleware, - httpmw.ExtractOrganizationParam(api.Database), ) - r.Get("/", api.provisionerDaemons) - r.Get("/serve", api.provisionerDaemonServe) + r.With(apiKeyMiddleware).Get("/", api.provisionerDaemons) + r.With(apiKeyMiddlewareOptional).Get("/serve", api.provisionerDaemonServe) }) r.Route("/templates/{template}/acl", func(r chi.Router) { r.Use( @@ -362,6 +373,9 @@ type Options struct { EntitlementsUpdateInterval time.Duration ProxyHealthInterval time.Duration Keys map[string]ed25519.PublicKey + + // optional pre-shared key for authentication of external provisioner daemons + ProvisionerDaemonPSK string } type API struct { @@ -383,6 +397,8 @@ type API struct { entitlementsUpdateMu sync.Mutex entitlementsMu sync.RWMutex entitlements codersdk.Entitlements + + provisionerDaemonAuth *provisionerDaemonAuth } func (api *API) Close() error { diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 92e0b627d60ae..64cb15c740fed 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -56,6 +56,7 @@ type Options struct { DontAddLicense bool DontAddFirstUser bool ReplicaSyncUpdateInterval time.Duration + ProvisionerDaemonPSK string } // New constructs a codersdk client connected to an in-memory Enterprise API instance. @@ -94,6 +95,7 @@ func NewWithAPI(t *testing.T, options *Options) ( Keys: Keys, ProxyHealthInterval: options.ProxyHealthInterval, DefaultQuietHoursSchedule: oop.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(), + ProvisionerDaemonPSK: options.ProvisionerDaemonPSK, }) require.NoError(t, err) setHandler(coderAPI.AGPL.RootHandler) diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 055704a6bcb11..1b3010d833200 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -2,6 +2,7 @@ package coderd import ( "context" + "crypto/subtle" "database/sql" "encoding/json" "errors" @@ -87,6 +88,40 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, apiDaemons) } +type provisionerDaemonAuth struct { + psk string + authorizer rbac.Authorizer +} + +// authorize returns mutated tags and true if the given HTTP request is authorized to access the provisioner daemon +// protobuf API, and returns nil, false otherwise. +func (p *provisionerDaemonAuth) authorize(r *http.Request, tags map[string]string) (map[string]string, bool) { + ctx := r.Context() + apiKey, ok := httpmw.APIKeyOptional(r) + if ok { + tags = provisionerdserver.MutateTags(apiKey.UserID, tags) + if tags[provisionerdserver.TagScope] == provisionerdserver.ScopeUser { + // Any authenticated user can create provisioner daemons scoped + // for jobs that they own, + return tags, true + } + ua := httpmw.UserAuthorization(r) + if err := p.authorizer.Authorize(ctx, ua.Actor, rbac.ActionCreate, rbac.ResourceProvisionerDaemon); err == nil { + // User is allowed to create provisioner daemons + return tags, true + } + } + + // Check for PSK + if p.psk != "" { + psk := r.Header.Get(codersdk.ProvisionerDaemonPSK) + if subtle.ConstantTimeCompare([]byte(p.psk), []byte(psk)) == 1 { + return tags, true + } + } + return nil, false +} + // Serves the provisioner daemon protobuf API over a WebSocket. // // @Summary Serve provisioner daemon @@ -134,19 +169,11 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) } } - // Any authenticated user can create provisioner daemons scoped - // for jobs that they own, but only authorized users can create - // globally scoped provisioners that attach to all jobs. - apiKey := httpmw.APIKey(r) - tags = provisionerdserver.MutateTags(apiKey.UserID, tags) - - if tags[provisionerdserver.TagScope] == provisionerdserver.ScopeOrganization { - if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceProvisionerDaemon) { - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "You aren't allowed to create provisioner daemons for the organization.", - }) - return - } + tags, authorized := api.provisionerDaemonAuth.authorize(r, tags) + if !authorized { + httpapi.Write(ctx, rw, http.StatusForbidden, + codersdk.Response{Message: "You aren't allowed to create provisioner daemons"}) + return } provisioners := make([]database.ProvisionerType, 0) diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index 28a89431b4f00..1586a92773e73 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -17,6 +17,7 @@ import ( "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" + "github.com/coder/coder/testutil" ) func TestProvisionerDaemonServe(t *testing.T) { @@ -28,23 +29,43 @@ func TestProvisionerDaemonServe(t *testing.T) { codersdk.FeatureExternalProvisionerDaemons: 1, }, }}) - srv, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeEcho, - }, map[string]string{}) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + srv, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{}, + }) require.NoError(t, err) srv.DRPCConn().Close() + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 1) }) t.Run("NoLicense", func(t *testing.T) { t.Parallel() client, user := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true}) - _, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeEcho, - }, map[string]string{}) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + _, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{}, + }) require.Error(t, err) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + + // querying provisioner daemons is forbidden without license + _, err = client.ProvisionerDaemons(ctx) + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) t.Run("Organization", func(t *testing.T) { @@ -55,15 +76,24 @@ func TestProvisionerDaemonServe(t *testing.T) { }, }}) another, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOrgAdmin(user.OrganizationID)) - _, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeEcho, - }, map[string]string{ - provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, }) require.Error(t, err) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 0) }) t.Run("OrganizationNoPerms", func(t *testing.T) { @@ -74,15 +104,24 @@ func TestProvisionerDaemonServe(t *testing.T) { }, }}) another, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) - _, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ - codersdk.ProvisionerTypeEcho, - }, map[string]string{ - provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, }) require.Error(t, err) var apiError *codersdk.Error require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 0) }) t.Run("UserLocal", func(t *testing.T) { @@ -141,4 +180,129 @@ func TestProvisionerDaemonServe(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, another, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) }) + + t.Run("PSK", func(t *testing.T) { + t.Parallel() + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + ProvisionerDaemonPSK: "provisionersftw", + }) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + another := codersdk.New(client.URL) + srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, + PreSharedKey: "provisionersftw", + }) + require.NoError(t, err) + err = srv.DRPCConn().Close() + require.NoError(t, err) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 1) + }) + + t.Run("BadPSK", func(t *testing.T) { + t.Parallel() + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + ProvisionerDaemonPSK: "provisionersftw", + }) + another := codersdk.New(client.URL) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, + PreSharedKey: "the wrong key", + }) + require.Error(t, err) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 0) + }) + + t.Run("NoAuth", func(t *testing.T) { + t.Parallel() + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + ProvisionerDaemonPSK: "provisionersftw", + }) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + another := codersdk.New(client.URL) + _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, + }) + require.Error(t, err) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 0) + }) + + t.Run("NoPSK", func(t *testing.T) { + t.Parallel() + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + another := codersdk.New(client.URL) + _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Organization: user.OrganizationID, + Provisioners: []codersdk.ProvisionerType{ + codersdk.ProvisionerTypeEcho, + }, + Tags: map[string]string{ + provisionerdserver.TagScope: provisionerdserver.ScopeOrganization, + }, + PreSharedKey: "provisionersftw", + }) + require.Error(t, err) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 0) + }) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fdfa3542dbf6a..f8b91abb925b6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -714,6 +714,7 @@ export interface ProvisionerConfig { readonly daemon_poll_interval: number readonly daemon_poll_jitter: number readonly force_cancel_interval: number + readonly daemon_psk: string } // From codersdk/provisionerdaemons.go From aff025e78c8e28ee460fb645e95c716a6ed88bb1 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 4 Aug 2023 11:25:24 +0100 Subject: [PATCH 006/156] chore(docs): fix link to helm values highlighting affinity (#8901) --- docs/admin/scale.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/scale.md b/docs/admin/scale.md index 999a30aeae44a..5b5e2369f54fd 100644 --- a/docs/admin/scale.md +++ b/docs/admin/scale.md @@ -42,7 +42,7 @@ Users accessing workspaces via SSH will consume fewer resources, as SSH connecti Workspace builds are CPU-intensive, as it relies on Terraform. Various [Terraform providers](https://registry.terraform.io/browse/providers) have different resource requirements. When tested with our [kubernetes](https://github.com/coder/coder/tree/main/examples/templates/kubernetes) template, `coderd` will consume roughly 0.25 cores per concurrent workspace build. -For effective provisioning, our helm chart prefers to schedule [one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/values.yaml#L110-L121). +For effective provisioning, our helm chart prefers to schedule [one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/values.yaml#L188-L202). We recommend: From eae15c07893fa67ed29a95964cf7b31374b0c848 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 4 Aug 2023 06:47:15 -0500 Subject: [PATCH 007/156] chore(examples): bump envbuilder version (#8893) --- examples/templates/devcontainer-docker/main.tf | 4 +++- examples/templates/devcontainer-kubernetes/main.tf | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/templates/devcontainer-docker/main.tf b/examples/templates/devcontainer-docker/main.tf index 8769ff1f07078..5941b0c6b9450 100644 --- a/examples/templates/devcontainer-docker/main.tf +++ b/examples/templates/devcontainer-docker/main.tf @@ -206,7 +206,9 @@ data "coder_parameter" "custom_repo_url" { resource "docker_container" "workspace" { count = data.coder_workspace.me.start_count - image = "ghcr.io/coder/envbuilder:0.1.3" + # Find the latest version here: + # https://github.com/coder/envbuilder/tags + image = "ghcr.io/coder/envbuilder:0.2.1" # Uses lower() to avoid Docker restriction on container names. name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" # Hostname makes the shell more user friendly: coder@my-workspace:~$ diff --git a/examples/templates/devcontainer-kubernetes/main.tf b/examples/templates/devcontainer-kubernetes/main.tf index 65694c25989a9..58c183e5be173 100644 --- a/examples/templates/devcontainer-kubernetes/main.tf +++ b/examples/templates/devcontainer-kubernetes/main.tf @@ -187,8 +187,10 @@ resource "kubernetes_deployment" "workspace" { } spec { container { - name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" - image = "ghcr.io/coder/envbuilder:0.1.3" + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + # Find the latest version here: + # https://github.com/coder/envbuilder/tags + image = "ghcr.io/coder/envbuilder:0.2.1" env { name = "CODER_AGENT_TOKEN" value = coder_agent.main.token From 1c3ec8743c06a18fb3ac35cbf7d5095cd1d69e38 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Fri, 4 Aug 2023 08:40:48 -0400 Subject: [PATCH 008/156] docs: clean up k8s install steps and order (#8869) --- docs/install/kubernetes.md | 96 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md index 191ef1aba0338..ade4b058c2dcf 100644 --- a/docs/install/kubernetes.md +++ b/docs/install/kubernetes.md @@ -7,14 +7,10 @@ to log in and manage templates. ## Install Coder with Helm -> **Warning**: Helm support is new and not yet complete. There may be changes -> to the Helm chart between releases which require manual values updates. Please -> file an issue if you run into any issues. - 1. Create a namespace for Coder, such as `coder`: ```console - $ kubectl create namespace coder + kubectl create namespace coder ``` 1. Create a PostgreSQL deployment. Coder does not manage a database server for @@ -57,12 +53,6 @@ to log in and manage templates. [Postgres operator](https://github.com/zalando/postgres-operator) to manage PostgreSQL deployments on your Kubernetes cluster. -1. Add the Coder Helm repo: - - ```console - helm repo add coder-v2 https://helm.coder.com/v2 - ``` - 1. Create a secret with the database URL: ```console @@ -72,6 +62,12 @@ to log in and manage templates. --from-literal=url="postgres://coder:coder@coder-db-postgresql.coder.svc.cluster.local:5432/coder?sslmode=disable" ``` +1. Add the Coder Helm repo: + + ```console + helm repo add coder-v2 https://helm.coder.com/v2 + ``` + 1. Create a `values.yaml` with the configuration settings you'd like for your deployment. For example: @@ -112,20 +108,52 @@ to log in and manage templates. > [values.yaml](https://github.com/coder/coder/blob/main/helm/values.yaml) > file directly. - If you are deploying Coder on AWS EKS and service is set to `LoadBalancer`, AWS will default to the Classic load balancer. The load balancer external IP will be stuck in a pending status unless sessionAffinity is set to None. +1. Run the following command to install the chart in your cluster. - ```yaml - coder: - service: - type: LoadBalancer - sessionAffinity: None + ```console + helm install coder coder-v2/coder \ + --namespace coder \ + --values values.yaml ``` + You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder has + started, the `coder-*` pods should enter the `Running` state. + +1. Log in to Coder + + Use `kubectl get svc -n coder` to get the IP address of the + LoadBalancer. Visit this in the browser to set up your first account. + + If you do not have a domain, you should set `CODER_ACCESS_URL` + to this URL in the Helm chart and upgrade Coder (see below). + This allows workspaces to connect to the proper Coder URL. + +## Upgrading Coder via Helm + +To upgrade Coder in the future or change values, +you can run the following command: + +```console +helm repo update +helm upgrade coder coder-v2/coder \ + --namespace coder \ + -f values.yaml +``` + ## Load balancing considerations ### AWS -AWS however recommends a Network load balancer in lieu of the Classic load balancer. Use the following `values.yaml` settings to request a Network load balancer: +If you are deploying Coder on AWS EKS and service is set to `LoadBalancer`, AWS will default to the Classic load balancer. The load balancer external IP will be stuck in a pending status unless sessionAffinity is set to None. + +```yaml +coder: + service: + type: LoadBalancer + sessionAffinity: None +``` + +AWS recommends a Network load balancer in lieu of the Classic load balancer. Use the following `values.yaml` settings to request a Network load balancer: ```yaml coder: @@ -152,26 +180,6 @@ coder: value: 10.0.0.1/8 # this will be the CIDR range of your Load Balancer IP address ``` -1. Run the following command to install the chart in your cluster. - - ```console - helm install coder coder-v2/coder \ - --namespace coder \ - --values values.yaml - ``` - - You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder has - started, the `coder-*` pods should enter the `Running` state. - -1. Log in to Coder - - Use `kubectl get svc -n coder` to get the IP address of the - LoadBalancer. Visit this in the browser to set up your first account. - - If you do not have a domain, you should set `CODER_ACCESS_URL` - to this URL in the Helm chart and upgrade Coder (see below). - This allows workspaces to connect to the proper Coder URL. - ### Azure In certain enterprise environments, the [Azure Application Gateway](https://learn.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview) was needed. The Application Gateway supports: @@ -212,18 +220,6 @@ postgres://:@databasehost:/?sslmode=require&sslce > More information on connecting to PostgreSQL databases using certificates can be found [here](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-CLIENTCERT). -## Upgrading Coder via Helm - -To upgrade Coder in the future or change values, -you can run the following command: - -```console -helm repo update -helm upgrade coder coder-v2/coder \ - --namespace coder \ - -f values.yaml -``` - ## Troubleshooting You can view Coder's logs by getting the pod name from `kubectl get pods` and then running `kubectl logs `. You can also From 5106dfde5219bccfb926ad8e1209d9d7b418bd69 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 4 Aug 2023 11:55:33 -0300 Subject: [PATCH 009/156] refactor(site): refactor the ports button design (#8879) * Refactor button * Move component to where it is used * Add base state of port forward component * Add stories and empty state * Add listening ports to handlers * Add review suggestions * Fix minor thing --- .../PortForwardButton/PortForwardButton.tsx | 211 -------------- site/src/components/Resources/AgentRow.tsx | 2 +- .../Resources/PortForwardButton.stories.tsx | 38 +++ .../Resources/PortForwardButton.tsx | 272 ++++++++++++++++++ .../Tooltips/HelpTooltip/HelpTooltip.tsx | 14 +- site/src/pages/TerminalPage/TerminalPage.tsx | 2 +- site/src/testHelpers/entities.ts | 9 + site/src/testHelpers/handlers.ts | 4 + site/src/utils/portForward.ts | 14 + .../portForward/portForwardXService.ts | 47 --- 10 files changed, 349 insertions(+), 264 deletions(-) delete mode 100644 site/src/components/PortForwardButton/PortForwardButton.tsx create mode 100644 site/src/components/Resources/PortForwardButton.stories.tsx create mode 100644 site/src/components/Resources/PortForwardButton.tsx create mode 100644 site/src/utils/portForward.ts delete mode 100644 site/src/xServices/portForward/portForwardXService.ts diff --git a/site/src/components/PortForwardButton/PortForwardButton.tsx b/site/src/components/PortForwardButton/PortForwardButton.tsx deleted file mode 100644 index 5c8b4943f8b60..0000000000000 --- a/site/src/components/PortForwardButton/PortForwardButton.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import Popover from "@mui/material/Popover" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" -import { Stack } from "components/Stack/Stack" -import { useRef, useState, Fragment } from "react" -import { colors } from "theme/colors" -import { CodeExample } from "../CodeExample/CodeExample" -import { - HelpTooltipLink, - HelpTooltipLinksGroup, - HelpTooltipText, - HelpTooltipTitle, -} from "../Tooltips/HelpTooltip" -import { Maybe } from "components/Conditionals/Maybe" -import { useMachine } from "@xstate/react" -import { portForwardMachine } from "xServices/portForward/portForwardXService" -import { SecondaryAgentButton } from "components/Resources/AgentButton" -import { docs } from "utils/docs" - -export interface PortForwardButtonProps { - host: string - username: string - workspaceName: string - agentName: string - agentId: string -} - -export const portForwardURL = ( - host: string, - port: number, - agentName: string, - workspaceName: string, - username: string, -): string => { - const { location } = window - - const subdomain = `${ - isNaN(port) ? 3000 : port - }--${agentName}--${workspaceName}--${username}` - return `${location.protocol}//${host}`.replace("*", subdomain) -} - -const TooltipView: React.FC = (props) => { - const { host, workspaceName, agentName, agentId, username } = props - - const styles = useStyles() - const [port, setPort] = useState("3000") - const urlExample = portForwardURL( - host, - parseInt(port), - agentName, - workspaceName, - username, - ) - - const [state] = useMachine(portForwardMachine, { - context: { agentId: agentId }, - }) - const ports = state.context.listeningPorts?.ports - - return ( - <> - - Access ports running on the agent with the{" "} - port, agent name, workspace name and{" "} - your username URL schema, as shown below. Port URLs are - only accessible by you. - - - - - - Use the form to open applications in a new tab. - - - - { - setPort(e.currentTarget.value) - }} - /> - - - - - - 0)}> - - {ports && - ports.map((p, i) => { - const url = portForwardURL( - host, - p.port, - agentName, - workspaceName, - username, - ) - let label = `${p.port}` - if (p.process_name) { - label = `${p.process_name} - ${p.port}` - } - - return ( - - {i > 0 && ·} - - {label} - - - ) - })} - - - - - - Learn more about web port forwarding - - - - ) -} - -export const PortForwardButton: React.FC = (props) => { - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) - const id = isOpen ? "schedule-popover" : undefined - const styles = useStyles() - - const onClose = () => { - setIsOpen(false) - } - - return ( - <> - { - setIsOpen(true) - }} - > - Port forward - - - Port forward - - - - ) -} - -const useStyles = makeStyles((theme) => ({ - popoverPaper: { - padding: `${theme.spacing(2.5)} ${theme.spacing(3.5)} ${theme.spacing( - 3.5, - )}`, - width: theme.spacing(52), - color: theme.palette.text.secondary, - marginTop: theme.spacing(0.25), - }, - - openUrlButton: { - flexShrink: 0, - }, - - portField: { - // The default border don't contrast well with the popover - "& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": { - borderColor: colors.gray[10], - }, - }, - - code: { - margin: theme.spacing(2, 0), - }, - - form: { - margin: theme.spacing(2, 0), - }, -})) diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index f26c8b8fbdd5c..320f6cc11007d 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -8,7 +8,7 @@ import { OpenDropdown, } from "components/DropdownArrows/DropdownArrows" import { LogLine, logLineHeight } from "components/Logs/Logs" -import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" +import { PortForwardButton } from "./PortForwardButton" import { VSCodeDesktopButton } from "components/VSCodeDesktopButton/VSCodeDesktopButton" import { FC, diff --git a/site/src/components/Resources/PortForwardButton.stories.tsx b/site/src/components/Resources/PortForwardButton.stories.tsx new file mode 100644 index 0000000000000..46014bcca71cb --- /dev/null +++ b/site/src/components/Resources/PortForwardButton.stories.tsx @@ -0,0 +1,38 @@ +import Box from "@mui/material/Box" +import { PortForwardPopoverView } from "./PortForwardButton" +import type { Meta, StoryObj } from "@storybook/react" +import { MockListeningPortsResponse } from "testHelpers/entities" + +const meta: Meta = { + title: "components/PortForwardPopoverView", + component: PortForwardPopoverView, + decorators: [ + (Story) => ( + theme.spacing(38), + border: (theme) => `1px solid ${theme.palette.divider}`, + borderRadius: 1, + backgroundColor: (theme) => theme.palette.background.paper, + }} + > + + + ), + ], +} + +export default meta +type Story = StoryObj + +export const WithPorts: Story = { + args: { + ports: MockListeningPortsResponse.ports, + }, +} + +export const Empty: Story = { + args: { + ports: [], + }, +} diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx new file mode 100644 index 0000000000000..2e9089649e808 --- /dev/null +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -0,0 +1,272 @@ +import Link from "@mui/material/Link" +import Popover from "@mui/material/Popover" +import { makeStyles } from "@mui/styles" +import { useRef, useState } from "react" +import { colors } from "theme/colors" +import { + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, +} from "../Tooltips/HelpTooltip" +import { SecondaryAgentButton } from "components/Resources/AgentButton" +import { docs } from "utils/docs" +import Box from "@mui/material/Box" +import { useQuery } from "@tanstack/react-query" +import { getAgentListeningPorts } from "api/api" +import { WorkspaceAgentListeningPort } from "api/typesGenerated" +import CircularProgress from "@mui/material/CircularProgress" +import { portForwardURL } from "utils/portForward" +import { MockListeningPortsResponse } from "testHelpers/entities" +import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined" + +export interface PortForwardButtonProps { + host: string + username: string + workspaceName: string + agentName: string + agentId: string +} + +export const PortForwardButton: React.FC = (props) => { + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "schedule-popover" : undefined + const styles = useStyles() + const { data: listeningPorts } = useQuery({ + queryKey: ["portForward", props.agentId], + queryFn: () => getAgentListeningPorts(props.agentId), + initialData: MockListeningPortsResponse, + }) + + const onClose = () => { + setIsOpen(false) + } + + return ( + <> + { + setIsOpen(true) + }} + > + Ports + {listeningPorts ? ( + theme.spacing(0, 1), + borderRadius: 7, + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.gray[11], + ml: 1, + }} + > + {listeningPorts.ports.length} + + ) : ( + + )} + + + + + + ) +} + +export const PortForwardPopoverView: React.FC< + PortForwardButtonProps & { ports?: WorkspaceAgentListeningPort[] } +> = (props) => { + const { host, workspaceName, agentName, username, ports } = props + + return ( + <> + theme.spacing(2.5), + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + Forwarded ports + theme.palette.text.secondary }} + > + {ports?.length === 0 + ? "No open ports were detected." + : "The forwarded ports are exclusively accessible to you."} + + theme.spacing(1.5) }}> + {ports?.map((p) => { + const url = portForwardURL( + host, + p.port, + agentName, + workspaceName, + username, + ) + const label = p.process_name !== "" ? p.process_name : p.port + return ( + theme.palette.text.primary, + fontSize: 14, + display: "flex", + alignItems: "center", + gap: 1, + py: 0.5, + fontWeight: 500, + }} + key={p.port} + href={url} + target="_blank" + rel="noreferrer" + > + + {label} + theme.palette.text.secondary, + fontSize: 13, + fontWeight: 400, + }} + > + {p.port} + + + ) + })} + + + + theme.spacing(2.5), + }} + > + Forward port + theme.palette.text.secondary }} + > + Access ports running on the agent: + + + `1px solid ${theme.palette.divider}`, + borderRadius: "4px", + mt: 2, + display: "flex", + alignItems: "center", + "&:focus-within": { + borderColor: (theme) => theme.palette.primary.main, + }, + }} + onSubmit={(e) => { + e.preventDefault() + const formData = new FormData(e.currentTarget) + const port = Number(formData.get("portNumber")) + const url = portForwardURL( + host, + port, + agentName, + workspaceName, + username, + ) + window.open(url, "_blank") + }} + > + theme.spacing(0, 1.5), + background: "none", + border: 0, + outline: "none", + color: (theme) => theme.palette.text.primary, + appearance: "textfield", + display: "block", + width: "100%", + }} + /> + theme.spacing(1.5), + color: (theme) => theme.palette.text.primary, + }} + /> + + + + + Learn more + + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + popoverPaper: { + padding: 0, + width: theme.spacing(38), + color: theme.palette.text.secondary, + marginTop: theme.spacing(0.25), + }, + + openUrlButton: { + flexShrink: 0, + }, + + portField: { + // The default border don't contrast well with the popover + "& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": { + borderColor: colors.gray[10], + }, + }, + + code: { + margin: theme.spacing(2, 0), + }, + + form: { + margin: theme.spacing(2, 0), + }, +})) diff --git a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx index 114d31234c48b..b248450667266 100644 --- a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx @@ -13,6 +13,7 @@ import { } from "react" import { combineClasses } from "utils/combineClasses" import { Stack } from "../../Stack/Stack" +import Box, { BoxProps } from "@mui/material/Box" type Icon = typeof HelpIcon @@ -131,12 +132,16 @@ export const HelpTooltipTitle: FC> = ({ return

{children}

} -export const HelpTooltipText: FC> = ({ - children, -}) => { +export const HelpTooltipText = (props: BoxProps) => { const styles = useStyles({}) - return

{children}

+ return ( + + ) } export const HelpTooltipLink: FC> = ({ @@ -257,6 +262,7 @@ const useStyles = makeStyles((theme) => ({ marginBottom: theme.spacing(1), color: theme.palette.text.primary, fontSize: 14, + lineHeight: "120%", fontWeight: 600, }, diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 4352ee40c0991..a09f289da9805 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -1,6 +1,5 @@ import { makeStyles, useTheme } from "@mui/styles" import { useMachine } from "@xstate/react" -import { portForwardURL } from "components/PortForwardButton/PortForwardButton" import { Stack } from "components/Stack/Stack" import { FC, useCallback, useEffect, useRef, useState } from "react" import { Helmet } from "react-helmet-async" @@ -23,6 +22,7 @@ import { getLatencyColor } from "utils/latency" import Popover from "@mui/material/Popover" import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency" import TerminalPageAlert, { TerminalPageAlertType } from "./TerminalPageAlert" +import { portForwardURL } from "utils/portForward" export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8b208548018ed..3d28ae7ad1ffb 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2281,3 +2281,12 @@ export const MockHealth = { }, coder_version: "v0.27.1-devel+c575292", } + +export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = + { + ports: [ + { process_name: "web", network: "", port: 3000 }, + { process_name: "go", network: "", port: 8080 }, + { process_name: "", network: "", port: 8081 }, + ], + } diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 9d90e09724c37..8c379985caa4b 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -391,4 +391,8 @@ export const handlers = [ rest.get("/api/v2/debug/health", (_, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockHealth)) }), + + rest.get("/api/v2/workspaceagents/:agent/listening-ports", (_, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockListeningPortsResponse)) + }), ] diff --git a/site/src/utils/portForward.ts b/site/src/utils/portForward.ts new file mode 100644 index 0000000000000..ce03d54dc47bc --- /dev/null +++ b/site/src/utils/portForward.ts @@ -0,0 +1,14 @@ +export const portForwardURL = ( + host: string, + port: number, + agentName: string, + workspaceName: string, + username: string, +): string => { + const { location } = window + + const subdomain = `${ + isNaN(port) ? 3000 : port + }--${agentName}--${workspaceName}--${username}` + return `${location.protocol}//${host}`.replace("*", subdomain) +} diff --git a/site/src/xServices/portForward/portForwardXService.ts b/site/src/xServices/portForward/portForwardXService.ts deleted file mode 100644 index cd908155dbcc1..0000000000000 --- a/site/src/xServices/portForward/portForwardXService.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getAgentListeningPorts } from "api/api" -import { WorkspaceAgentListeningPortsResponse } from "api/typesGenerated" -import { createMachine, assign } from "xstate" - -export const portForwardMachine = createMachine( - { - predictableActionArguments: true, - id: "portForwardMachine", - schema: { - context: {} as { - agentId: string - listeningPorts?: WorkspaceAgentListeningPortsResponse - }, - services: {} as { - getListeningPorts: { - data: WorkspaceAgentListeningPortsResponse - } - }, - }, - tsTypes: {} as import("./portForwardXService.typegen").Typegen0, - initial: "loading", - states: { - loading: { - invoke: { - src: "getListeningPorts", - onDone: { - target: "success", - actions: ["assignListeningPorts"], - }, - }, - }, - success: { - type: "final", - }, - }, - }, - { - services: { - getListeningPorts: ({ agentId }) => getAgentListeningPorts(agentId), - }, - actions: { - assignListeningPorts: assign({ - listeningPorts: (_, { data }) => data, - }), - }, - }, -) From 607cd11724c492171a511c03c1384bba6cd52469 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 4 Aug 2023 16:06:28 +0100 Subject: [PATCH 010/156] fix(cli): address race condition in scaletest_test output (#8902) --- cli/exp_scaletest_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cli/exp_scaletest_test.go b/cli/exp_scaletest_test.go index 4c10b722ca357..a70fba0443b10 100644 --- a/cli/exp_scaletest_test.go +++ b/cli/exp_scaletest_test.go @@ -1,7 +1,6 @@ package cli_test import ( - "bytes" "context" "path/filepath" "testing" @@ -72,9 +71,10 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) { "--ssh", ) clitest.SetupConfig(t, client, root) - var stdout, stderr bytes.Buffer - inv.Stdout = &stdout - inv.Stderr = &stderr + pty := ptytest.New(t) + inv.Stdout = pty.Output() + inv.Stderr = pty.Output() + err := inv.WithContext(ctx).Run() require.ErrorContains(t, err, "no scaletest workspaces exist") } @@ -98,9 +98,10 @@ func TestScaleTestDashboard(t *testing.T) { "--scaletest-prometheus-wait", "0s", ) clitest.SetupConfig(t, client, root) - var stdout, stderr bytes.Buffer - inv.Stdout = &stdout - inv.Stderr = &stderr + pty := ptytest.New(t) + inv.Stdout = pty.Output() + inv.Stderr = pty.Output() + err := inv.WithContext(ctx).Run() require.NoError(t, err, "") } From ae88b79fd7074f9ac670568a8f19dc3c22dd254c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 4 Aug 2023 16:15:33 +0100 Subject: [PATCH 011/156] fix(cli): stat: set --host arg in TestStatCPUCmd to avoid test flakes in containers (#8806) --- cli/stat_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/stat_test.go b/cli/stat_test.go index d92574e339b89..7b37a98ce9113 100644 --- a/cli/stat_test.go +++ b/cli/stat_test.go @@ -74,7 +74,7 @@ func TestStatCPUCmd(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) t.Cleanup(cancel) - inv, _ := clitest.New(t, "stat", "cpu", "--output=text") + inv, _ := clitest.New(t, "stat", "cpu", "--output=text", "--host") buf := new(bytes.Buffer) inv.Stdout = buf err := inv.WithContext(ctx).Run() @@ -87,7 +87,7 @@ func TestStatCPUCmd(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) t.Cleanup(cancel) - inv, _ := clitest.New(t, "stat", "cpu", "--output=json") + inv, _ := clitest.New(t, "stat", "cpu", "--output=json", "--host") buf := new(bytes.Buffer) inv.Stdout = buf err := inv.WithContext(ctx).Run() From 9fb18f3ae5c0024474439cf9de6795a2d3e7a125 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 4 Aug 2023 17:00:42 +0100 Subject: [PATCH 012/156] feat(coderd): batch agent stats inserts (#8875) This PR adds support for batching inserts to the workspace_agents_stats table. Up to 1024 stats are batched, and flushed every second in a batch. --- cli/server.go | 11 + coderd/batchstats/batcher.go | 289 ++++++++++++++++++ coderd/batchstats/batcher_internal_test.go | 226 ++++++++++++++ coderd/coderd.go | 12 + coderd/coderdtest/coderdtest.go | 18 +- coderd/database/dbauthz/dbauthz.go | 8 + coderd/database/dbfake/dbfake.go | 51 +++- coderd/database/dbmetrics/dbmetrics.go | 7 + coderd/database/dbmock/dbmock.go | 14 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 84 +++++ .../database/queries/workspaceagentstats.sql | 40 +++ .../prometheusmetrics_test.go | 31 +- coderd/workspaceagents.go | 28 +- 14 files changed, 785 insertions(+), 35 deletions(-) create mode 100644 coderd/batchstats/batcher.go create mode 100644 coderd/batchstats/batcher_internal_test.go diff --git a/cli/server.go b/cli/server.go index 170b7c5eb9f00..15cefb364ce3e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -63,6 +63,7 @@ import ( "github.com/coder/coder/cli/config" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/autobuild" + "github.com/coder/coder/coderd/batchstats" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbfake" "github.com/coder/coder/coderd/database/dbmetrics" @@ -813,6 +814,16 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. options.SwaggerEndpoint = cfg.Swagger.Enable.Value() } + batcher, closeBatcher, err := batchstats.New(ctx, + batchstats.WithLogger(options.Logger.Named("batchstats")), + batchstats.WithStore(options.Database), + ) + if err != nil { + return xerrors.Errorf("failed to create agent stats batcher: %w", err) + } + options.StatsBatcher = batcher + defer closeBatcher() + closeCheckInactiveUsersFunc := dormancy.CheckInactiveUsers(ctx, logger, options.Database) defer closeCheckInactiveUsersFunc() diff --git a/coderd/batchstats/batcher.go b/coderd/batchstats/batcher.go new file mode 100644 index 0000000000000..fc177fd143d6a --- /dev/null +++ b/coderd/batchstats/batcher.go @@ -0,0 +1,289 @@ +package batchstats + +import ( + "context" + "encoding/json" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbauthz" + "github.com/coder/coder/codersdk/agentsdk" +) + +const ( + defaultBufferSize = 1024 + defaultFlushInterval = time.Second +) + +// Batcher holds a buffer of agent stats and periodically flushes them to +// its configured store. It also updates the workspace's last used time. +type Batcher struct { + store database.Store + log slog.Logger + + mu sync.Mutex + // TODO: make this a buffered chan instead? + buf *database.InsertWorkspaceAgentStatsParams + // NOTE: we batch this separately as it's a jsonb field and + // pq.Array + unnest doesn't play nicely with this. + connectionsByProto []map[string]int64 + batchSize int + + // tickCh is used to periodically flush the buffer. + tickCh <-chan time.Time + ticker *time.Ticker + interval time.Duration + // flushLever is used to signal the flusher to flush the buffer immediately. + flushLever chan struct{} + flushForced atomic.Bool + // flushed is used during testing to signal that a flush has completed. + flushed chan<- int +} + +// Option is a functional option for configuring a Batcher. +type Option func(b *Batcher) + +// WithStore sets the store to use for storing stats. +func WithStore(store database.Store) Option { + return func(b *Batcher) { + b.store = store + } +} + +// WithBatchSize sets the number of stats to store in a batch. +func WithBatchSize(size int) Option { + return func(b *Batcher) { + b.batchSize = size + } +} + +// WithInterval sets the interval for flushes. +func WithInterval(d time.Duration) Option { + return func(b *Batcher) { + b.interval = d + } +} + +// WithLogger sets the logger to use for logging. +func WithLogger(log slog.Logger) Option { + return func(b *Batcher) { + b.log = log + } +} + +// New creates a new Batcher and starts it. +func New(ctx context.Context, opts ...Option) (*Batcher, func(), error) { + b := &Batcher{} + b.log = slog.Make(sloghuman.Sink(os.Stderr)) + b.flushLever = make(chan struct{}, 1) // Buffered so that it doesn't block. + for _, opt := range opts { + opt(b) + } + + if b.store == nil { + return nil, nil, xerrors.Errorf("no store configured for batcher") + } + + if b.interval == 0 { + b.interval = defaultFlushInterval + } + + if b.batchSize == 0 { + b.batchSize = defaultBufferSize + } + + if b.tickCh == nil { + b.ticker = time.NewTicker(b.interval) + b.tickCh = b.ticker.C + } + + cancelCtx, cancelFunc := context.WithCancel(ctx) + done := make(chan struct{}) + go func() { + b.run(cancelCtx) + close(done) + }() + + closer := func() { + cancelFunc() + if b.ticker != nil { + b.ticker.Stop() + } + <-done + } + + return b, closer, nil +} + +// Add adds a stat to the batcher for the given workspace and agent. +func (b *Batcher) Add( + agentID uuid.UUID, + templateID uuid.UUID, + userID uuid.UUID, + workspaceID uuid.UUID, + st agentsdk.Stats, +) error { + b.mu.Lock() + defer b.mu.Unlock() + + now := database.Now() + + b.buf.ID = append(b.buf.ID, uuid.New()) + b.buf.CreatedAt = append(b.buf.CreatedAt, now) + b.buf.AgentID = append(b.buf.AgentID, agentID) + b.buf.UserID = append(b.buf.UserID, userID) + b.buf.TemplateID = append(b.buf.TemplateID, templateID) + b.buf.WorkspaceID = append(b.buf.WorkspaceID, workspaceID) + + // Store the connections by proto separately as it's a jsonb field. We marshal on flush. + // b.buf.ConnectionsByProto = append(b.buf.ConnectionsByProto, st.ConnectionsByProto) + b.connectionsByProto = append(b.connectionsByProto, st.ConnectionsByProto) + + b.buf.ConnectionCount = append(b.buf.ConnectionCount, st.ConnectionCount) + b.buf.RxPackets = append(b.buf.RxPackets, st.RxPackets) + b.buf.RxBytes = append(b.buf.RxBytes, st.RxBytes) + b.buf.TxPackets = append(b.buf.TxPackets, st.TxPackets) + b.buf.TxBytes = append(b.buf.TxBytes, st.TxBytes) + b.buf.SessionCountVSCode = append(b.buf.SessionCountVSCode, st.SessionCountVSCode) + b.buf.SessionCountJetBrains = append(b.buf.SessionCountJetBrains, st.SessionCountJetBrains) + b.buf.SessionCountReconnectingPTY = append(b.buf.SessionCountReconnectingPTY, st.SessionCountReconnectingPTY) + b.buf.SessionCountSSH = append(b.buf.SessionCountSSH, st.SessionCountSSH) + b.buf.ConnectionMedianLatencyMS = append(b.buf.ConnectionMedianLatencyMS, st.ConnectionMedianLatencyMS) + + // If the buffer is over 80% full, signal the flusher to flush immediately. + // We want to trigger flushes early to reduce the likelihood of + // accidentally growing the buffer over batchSize. + filled := float64(len(b.buf.ID)) / float64(b.batchSize) + if filled >= 0.8 && !b.flushForced.Load() { + b.flushLever <- struct{}{} + b.flushForced.Store(true) + } + return nil +} + +// Run runs the batcher. +func (b *Batcher) run(ctx context.Context) { + b.initBuf(b.batchSize) + // nolint:gocritic // This is only ever used for one thing - inserting agent stats. + authCtx := dbauthz.AsSystemRestricted(ctx) + for { + select { + case <-b.tickCh: + b.flush(authCtx, false, "scheduled") + case <-b.flushLever: + // If the flush lever is depressed, flush the buffer immediately. + b.flush(authCtx, true, "reaching capacity") + case <-ctx.Done(): + b.log.Warn(ctx, "context done, flushing before exit") + b.flush(authCtx, true, "exit") + return + } + } +} + +// flush flushes the batcher's buffer. +func (b *Batcher) flush(ctx context.Context, forced bool, reason string) { + b.mu.Lock() + b.flushForced.Store(true) + start := time.Now() + count := len(b.buf.ID) + defer func() { + b.flushForced.Store(false) + b.mu.Unlock() + // Notify that a flush has completed. This only happens in tests. + if b.flushed != nil { + select { + case <-ctx.Done(): + close(b.flushed) + default: + b.flushed <- count + } + } + if count > 0 { + elapsed := time.Since(start) + b.log.Debug(ctx, "flush complete", + slog.F("count", count), + slog.F("elapsed", elapsed), + slog.F("forced", forced), + slog.F("reason", reason), + ) + } + }() + + if len(b.buf.ID) == 0 { + return + } + + // marshal connections by proto + payload, err := json.Marshal(b.connectionsByProto) + if err != nil { + b.log.Error(ctx, "unable to marshal agent connections by proto, dropping data", slog.Error(err)) + b.buf.ConnectionsByProto = json.RawMessage(`[]`) + } else { + b.buf.ConnectionsByProto = payload + } + + err = b.store.InsertWorkspaceAgentStats(ctx, *b.buf) + elapsed := time.Since(start) + if err != nil { + b.log.Error(ctx, "error inserting workspace agent stats", slog.Error(err), slog.F("elapsed", elapsed)) + return + } + + b.resetBuf() +} + +// initBuf resets the buffer. b MUST be locked. +func (b *Batcher) initBuf(size int) { + b.buf = &database.InsertWorkspaceAgentStatsParams{ + ID: make([]uuid.UUID, 0, b.batchSize), + CreatedAt: make([]time.Time, 0, b.batchSize), + UserID: make([]uuid.UUID, 0, b.batchSize), + WorkspaceID: make([]uuid.UUID, 0, b.batchSize), + TemplateID: make([]uuid.UUID, 0, b.batchSize), + AgentID: make([]uuid.UUID, 0, b.batchSize), + ConnectionsByProto: json.RawMessage("[]"), + ConnectionCount: make([]int64, 0, b.batchSize), + RxPackets: make([]int64, 0, b.batchSize), + RxBytes: make([]int64, 0, b.batchSize), + TxPackets: make([]int64, 0, b.batchSize), + TxBytes: make([]int64, 0, b.batchSize), + SessionCountVSCode: make([]int64, 0, b.batchSize), + SessionCountJetBrains: make([]int64, 0, b.batchSize), + SessionCountReconnectingPTY: make([]int64, 0, b.batchSize), + SessionCountSSH: make([]int64, 0, b.batchSize), + ConnectionMedianLatencyMS: make([]float64, 0, b.batchSize), + } + + b.connectionsByProto = make([]map[string]int64, 0, size) +} + +func (b *Batcher) resetBuf() { + b.buf.ID = b.buf.ID[:0] + b.buf.CreatedAt = b.buf.CreatedAt[:0] + b.buf.UserID = b.buf.UserID[:0] + b.buf.WorkspaceID = b.buf.WorkspaceID[:0] + b.buf.TemplateID = b.buf.TemplateID[:0] + b.buf.AgentID = b.buf.AgentID[:0] + b.buf.ConnectionsByProto = json.RawMessage(`[]`) + b.buf.ConnectionCount = b.buf.ConnectionCount[:0] + b.buf.RxPackets = b.buf.RxPackets[:0] + b.buf.RxBytes = b.buf.RxBytes[:0] + b.buf.TxPackets = b.buf.TxPackets[:0] + b.buf.TxBytes = b.buf.TxBytes[:0] + b.buf.SessionCountVSCode = b.buf.SessionCountVSCode[:0] + b.buf.SessionCountJetBrains = b.buf.SessionCountJetBrains[:0] + b.buf.SessionCountReconnectingPTY = b.buf.SessionCountReconnectingPTY[:0] + b.buf.SessionCountSSH = b.buf.SessionCountSSH[:0] + b.buf.ConnectionMedianLatencyMS = b.buf.ConnectionMedianLatencyMS[:0] + b.connectionsByProto = b.connectionsByProto[:0] +} diff --git a/coderd/batchstats/batcher_internal_test.go b/coderd/batchstats/batcher_internal_test.go new file mode 100644 index 0000000000000..a6e28f1a9f389 --- /dev/null +++ b/coderd/batchstats/batcher_internal_test.go @@ -0,0 +1,226 @@ +package batchstats + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbgen" + "github.com/coder/coder/coderd/database/dbtestutil" + "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/codersdk/agentsdk" + "github.com/coder/coder/cryptorand" +) + +func TestBatchStats(t *testing.T) { + t.Parallel() + + // Given: a fresh batcher with no data + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + store, _ := dbtestutil.NewDB(t) + + // Set up some test dependencies. + deps1 := setupDeps(t, store) + deps2 := setupDeps(t, store) + tick := make(chan time.Time) + flushed := make(chan int) + + b, closer, err := New(ctx, + WithStore(store), + WithLogger(log), + func(b *Batcher) { + b.tickCh = tick + b.flushed = flushed + }, + ) + require.NoError(t, err) + t.Cleanup(closer) + + // Given: no data points are added for workspace + // When: it becomes time to report stats + t1 := time.Now() + // Signal a tick and wait for a flush to complete. + tick <- t1 + f := <-flushed + require.Equal(t, 0, f, "expected no data to be flushed") + t.Logf("flush 1 completed") + + // Then: it should report no stats. + stats, err := store.GetWorkspaceAgentStats(ctx, t1) + require.NoError(t, err, "should not error getting stats") + require.Empty(t, stats, "should have no stats for workspace") + + // Given: a single data point is added for workspace + t2 := time.Now() + t.Logf("inserting 1 stat") + require.NoError(t, b.Add(deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) + + // When: it becomes time to report stats + // Signal a tick and wait for a flush to complete. + tick <- t2 + f = <-flushed // Wait for a flush to complete. + require.Equal(t, 1, f, "expected one stat to be flushed") + t.Logf("flush 2 completed") + + // Then: it should report a single stat. + stats, err = store.GetWorkspaceAgentStats(ctx, t2) + require.NoError(t, err, "should not error getting stats") + require.Len(t, stats, 1, "should have stats for workspace") + + // Given: a lot of data points are added for both workspaces + // (equal to batch size) + t3 := time.Now() + done := make(chan struct{}) + + go func() { + defer close(done) + t.Logf("inserting %d stats", defaultBufferSize) + for i := 0; i < defaultBufferSize; i++ { + if i%2 == 0 { + require.NoError(t, b.Add(deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) + } else { + require.NoError(t, b.Add(deps2.Agent.ID, deps2.User.ID, deps2.Template.ID, deps2.Workspace.ID, randAgentSDKStats(t))) + } + } + }() + + // When: the buffer comes close to capacity + // Then: The buffer will force-flush once. + f = <-flushed + t.Logf("flush 3 completed") + require.Greater(t, f, 819, "expected at least 819 stats to be flushed (>=80% of buffer)") + // And we should finish inserting the stats + <-done + + stats, err = store.GetWorkspaceAgentStats(ctx, t3) + require.NoError(t, err, "should not error getting stats") + require.Len(t, stats, 2, "should have stats for both workspaces") + + // Ensures that a subsequent flush pushes all the remaining data + t4 := time.Now() + tick <- t4 + f2 := <-flushed + t.Logf("flush 4 completed") + expectedCount := defaultBufferSize - f + require.Equal(t, expectedCount, f2, "did not flush expected remaining rows") + + // Ensure that a subsequent flush does not push stale data. + t5 := time.Now() + tick <- t5 + f = <-flushed + require.Zero(t, f, "expected zero stats to have been flushed") + t.Logf("flush 5 completed") + + stats, err = store.GetWorkspaceAgentStats(ctx, t5) + require.NoError(t, err, "should not error getting stats") + require.Len(t, stats, 0, "should have no stats for workspace") + + // Ensure that buf never grew beyond what we expect + require.Equal(t, defaultBufferSize, cap(b.buf.ID), "buffer grew beyond expected capacity") +} + +// randAgentSDKStats returns a random agentsdk.Stats +func randAgentSDKStats(t *testing.T, opts ...func(*agentsdk.Stats)) agentsdk.Stats { + t.Helper() + s := agentsdk.Stats{ + ConnectionsByProto: map[string]int64{ + "ssh": mustRandInt64n(t, 9) + 1, + "vscode": mustRandInt64n(t, 9) + 1, + "jetbrains": mustRandInt64n(t, 9) + 1, + "reconnecting_pty": mustRandInt64n(t, 9) + 1, + }, + ConnectionCount: mustRandInt64n(t, 99) + 1, + ConnectionMedianLatencyMS: float64(mustRandInt64n(t, 99) + 1), + RxPackets: mustRandInt64n(t, 99) + 1, + RxBytes: mustRandInt64n(t, 99) + 1, + TxPackets: mustRandInt64n(t, 99) + 1, + TxBytes: mustRandInt64n(t, 99) + 1, + SessionCountVSCode: mustRandInt64n(t, 9) + 1, + SessionCountJetBrains: mustRandInt64n(t, 9) + 1, + SessionCountReconnectingPTY: mustRandInt64n(t, 9) + 1, + SessionCountSSH: mustRandInt64n(t, 9) + 1, + Metrics: []agentsdk.AgentMetric{}, + } + for _, opt := range opts { + opt(&s) + } + return s +} + +// deps is a set of test dependencies. +type deps struct { + Agent database.WorkspaceAgent + Template database.Template + User database.User + Workspace database.Workspace +} + +// setupDeps sets up a set of test dependencies. +// It creates an organization, user, template, workspace, and agent +// along with all the other miscellaneous plumbing required to link +// them together. +func setupDeps(t *testing.T, store database.Store) deps { + t.Helper() + + org := dbgen.Organization(t, store, database.Organization{}) + user := dbgen.User(t, store, database.User{}) + _, err := store.InsertOrganizationMember(context.Background(), database.InsertOrganizationMemberParams{ + OrganizationID: org.ID, + UserID: user.ID, + Roles: []string{rbac.RoleOrgMember(org.ID)}, + }) + require.NoError(t, err) + tv := dbgen.TemplateVersion(t, store, database.TemplateVersion{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + tpl := dbgen.Template(t, store, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + ActiveVersionID: tv.ID, + }) + ws := dbgen.Workspace(t, store, database.Workspace{ + TemplateID: tpl.ID, + OwnerID: user.ID, + OrganizationID: org.ID, + LastUsedAt: time.Now().Add(-time.Hour), + }) + pj := dbgen.ProvisionerJob(t, store, database.ProvisionerJob{ + InitiatorID: user.ID, + OrganizationID: org.ID, + }) + _ = dbgen.WorkspaceBuild(t, store, database.WorkspaceBuild{ + TemplateVersionID: tv.ID, + WorkspaceID: ws.ID, + JobID: pj.ID, + }) + res := dbgen.WorkspaceResource(t, store, database.WorkspaceResource{ + Transition: database.WorkspaceTransitionStart, + JobID: pj.ID, + }) + agt := dbgen.WorkspaceAgent(t, store, database.WorkspaceAgent{ + ResourceID: res.ID, + }) + return deps{ + Agent: agt, + Template: tpl, + User: user, + Workspace: ws, + } +} + +// mustRandInt64n returns a random int64 in the range [0, n). +func mustRandInt64n(t *testing.T, n int64) int64 { + t.Helper() + i, err := cryptorand.Intn(int(n)) + require.NoError(t, err) + return int64(i) +} diff --git a/coderd/coderd.go b/coderd/coderd.go index d7b80ff273097..58b6c902c7dbc 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -43,6 +43,7 @@ import ( "github.com/coder/coder/buildinfo" "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/awsidentity" + "github.com/coder/coder/coderd/batchstats" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/database/pubsub" @@ -160,6 +161,7 @@ type Options struct { HTTPClient *http.Client UpdateAgentMetrics func(ctx context.Context, username, workspaceName, agentName string, metrics []agentsdk.AgentMetric) + StatsBatcher *batchstats.Batcher } // @title Coder API @@ -180,6 +182,8 @@ type Options struct { // @in header // @name Coder-Session-Token // New constructs a Coder API handler. +// +//nolint:gocyclo func New(options *Options) *API { if options == nil { options = &Options{} @@ -288,6 +292,10 @@ func New(options *Options) *API { options.UserQuietHoursScheduleStore.Store(&v) } + if options.StatsBatcher == nil { + panic("developer error: options.StatsBatcher is nil") + } + siteCacheDir := options.CacheDir if siteCacheDir != "" { siteCacheDir = filepath.Join(siteCacheDir, "site") @@ -462,6 +470,8 @@ func New(options *Options) *API { cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value()) prometheusMW := httpmw.Prometheus(options.PrometheusRegistry) + api.statsBatcher = options.StatsBatcher + r.Use( httpmw.Recover(api.Logger), tracing.StatusWriterMiddleware, @@ -994,6 +1004,8 @@ type API struct { healthCheckGroup *singleflight.Group[string, *healthcheck.Report] healthCheckCache atomic.Pointer[healthcheck.Report] + + statsBatcher *batchstats.Batcher } // Close waits for all WebSocket connections to drain before returning. diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 351a6d0d9a075..71e3336ab2e87 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -51,11 +51,13 @@ import ( "tailscale.com/types/nettype" "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/autobuild" "github.com/coder/coder/coderd/awsidentity" + "github.com/coder/coder/coderd/batchstats" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/database/dbtestutil" @@ -140,7 +142,8 @@ type Options struct { SwaggerEndpoint bool // Logger should only be overridden if you expect errors // as part of your test. - Logger *slog.Logger + Logger *slog.Logger + StatsBatcher *batchstats.Batcher } // New constructs a codersdk client connected to an in-memory API instance. @@ -241,6 +244,18 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can if options.FilesRateLimit == 0 { options.FilesRateLimit = -1 } + if options.StatsBatcher == nil { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + batcher, closeBatcher, err := batchstats.New(ctx, + batchstats.WithStore(options.Database), + // Avoid cluttering up test output. + batchstats.WithLogger(slog.Make(sloghuman.Sink(io.Discard))), + ) + require.NoError(t, err, "create stats batcher") + options.StatsBatcher = batcher + t.Cleanup(closeBatcher) + } var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore] if options.TemplateScheduleStore == nil { @@ -409,6 +424,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can HealthcheckFunc: options.HealthcheckFunc, HealthcheckTimeout: options.HealthcheckTimeout, HealthcheckRefresh: options.HealthcheckRefresh, + StatsBatcher: options.StatsBatcher, } } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 5e82320832442..488842dcaf351 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2016,6 +2016,14 @@ func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.Ins return q.db.InsertWorkspaceAgentStat(ctx, arg) } +func (q *querier) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error { + if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + + return q.db.InsertWorkspaceAgentStats(ctx, arg) +} + func (q *querier) InsertWorkspaceApp(ctx context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { return database.WorkspaceApp{}, err diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 03ae01182dbd0..cc0d05aaee9c3 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -2810,8 +2810,12 @@ func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter tim } statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsRow{} - for _, agentStat := range latestAgentStats { - stat := statByAgent[agentStat.AgentID] + for agentID, agentStat := range latestAgentStats { + stat := statByAgent[agentID] + stat.AgentID = agentStat.AgentID + stat.TemplateID = agentStat.TemplateID + stat.UserID = agentStat.UserID + stat.WorkspaceID = agentStat.WorkspaceID stat.SessionCountVSCode += agentStat.SessionCountVSCode stat.SessionCountJetBrains += agentStat.SessionCountJetBrains stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY @@ -4177,6 +4181,49 @@ func (q *FakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.Ins return stat, nil } +func (q *FakeQuerier) InsertWorkspaceAgentStats(_ context.Context, arg database.InsertWorkspaceAgentStatsParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + var connectionsByProto []map[string]int64 + if err := json.Unmarshal(arg.ConnectionsByProto, &connectionsByProto); err != nil { + return err + } + for i := 0; i < len(arg.ID); i++ { + cbp, err := json.Marshal(connectionsByProto[i]) + if err != nil { + return xerrors.Errorf("failed to marshal connections_by_proto: %w", err) + } + stat := database.WorkspaceAgentStat{ + ID: arg.ID[i], + CreatedAt: arg.CreatedAt[i], + WorkspaceID: arg.WorkspaceID[i], + AgentID: arg.AgentID[i], + UserID: arg.UserID[i], + ConnectionsByProto: cbp, + ConnectionCount: arg.ConnectionCount[i], + RxPackets: arg.RxPackets[i], + RxBytes: arg.RxBytes[i], + TxPackets: arg.TxPackets[i], + TxBytes: arg.TxBytes[i], + TemplateID: arg.TemplateID[i], + SessionCountVSCode: arg.SessionCountVSCode[i], + SessionCountJetBrains: arg.SessionCountJetBrains[i], + SessionCountReconnectingPTY: arg.SessionCountReconnectingPTY[i], + SessionCountSSH: arg.SessionCountSSH[i], + ConnectionMedianLatencyMS: arg.ConnectionMedianLatencyMS[i], + } + q.workspaceAgentStats = append(q.workspaceAgentStats, stat) + } + + return nil +} + func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceApp{}, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index ee7f8ae53b433..85ee8c26a0d51 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1236,6 +1236,13 @@ func (m metricsStore) InsertWorkspaceAgentStat(ctx context.Context, arg database return stat, err } +func (m metricsStore) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error { + start := time.Now() + r0 := m.s.InsertWorkspaceAgentStats(ctx, arg) + m.queryLatencies.WithLabelValues("InsertWorkspaceAgentStats").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) InsertWorkspaceApp(ctx context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { start := time.Now() app, err := m.s.InsertWorkspaceApp(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 6a0edec4f015d..cb7278369884b 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2598,6 +2598,20 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStat(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStat", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStat), arg0, arg1) } +// InsertWorkspaceAgentStats mocks base method. +func (m *MockStore) InsertWorkspaceAgentStats(arg0 context.Context, arg1 database.InsertWorkspaceAgentStatsParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertWorkspaceAgentStats", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertWorkspaceAgentStats indicates an expected call of InsertWorkspaceAgentStats. +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStats(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStats), arg0, arg1) +} + // InsertWorkspaceApp mocks base method. func (m *MockStore) InsertWorkspaceApp(arg0 context.Context, arg1 database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 783524375f822..b308589ffc350 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -225,6 +225,7 @@ type sqlcQuerier interface { InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) + InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ba0f3cfb54188..3e07fc22f1aa3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7418,6 +7418,90 @@ func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWor return i, err } +const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec +INSERT INTO + workspace_agent_stats ( + id, + created_at, + user_id, + workspace_id, + template_id, + agent_id, + connections_by_proto, + connection_count, + rx_packets, + rx_bytes, + tx_packets, + tx_bytes, + session_count_vscode, + session_count_jetbrains, + session_count_reconnecting_pty, + session_count_ssh, + connection_median_latency_ms + ) +SELECT + unnest($1 :: uuid[]) AS id, + unnest($2 :: timestamptz[]) AS created_at, + unnest($3 :: uuid[]) AS user_id, + unnest($4 :: uuid[]) AS workspace_id, + unnest($5 :: uuid[]) AS template_id, + unnest($6 :: uuid[]) AS agent_id, + jsonb_array_elements($7 :: jsonb) AS connections_by_proto, + unnest($8 :: bigint[]) AS connection_count, + unnest($9 :: bigint[]) AS rx_packets, + unnest($10 :: bigint[]) AS rx_bytes, + unnest($11 :: bigint[]) AS tx_packets, + unnest($12 :: bigint[]) AS tx_bytes, + unnest($13 :: bigint[]) AS session_count_vscode, + unnest($14 :: bigint[]) AS session_count_jetbrains, + unnest($15 :: bigint[]) AS session_count_reconnecting_pty, + unnest($16 :: bigint[]) AS session_count_ssh, + unnest($17 :: double precision[]) AS connection_median_latency_ms +` + +type InsertWorkspaceAgentStatsParams struct { + ID []uuid.UUID `db:"id" json:"id"` + CreatedAt []time.Time `db:"created_at" json:"created_at"` + UserID []uuid.UUID `db:"user_id" json:"user_id"` + WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateID []uuid.UUID `db:"template_id" json:"template_id"` + AgentID []uuid.UUID `db:"agent_id" json:"agent_id"` + ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"` + ConnectionCount []int64 `db:"connection_count" json:"connection_count"` + RxPackets []int64 `db:"rx_packets" json:"rx_packets"` + RxBytes []int64 `db:"rx_bytes" json:"rx_bytes"` + TxPackets []int64 `db:"tx_packets" json:"tx_packets"` + TxBytes []int64 `db:"tx_bytes" json:"tx_bytes"` + SessionCountVSCode []int64 `db:"session_count_vscode" json:"session_count_vscode"` + SessionCountJetBrains []int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` + SessionCountReconnectingPTY []int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` + SessionCountSSH []int64 `db:"session_count_ssh" json:"session_count_ssh"` + ConnectionMedianLatencyMS []float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error { + _, err := q.db.ExecContext(ctx, insertWorkspaceAgentStats, + pq.Array(arg.ID), + pq.Array(arg.CreatedAt), + pq.Array(arg.UserID), + pq.Array(arg.WorkspaceID), + pq.Array(arg.TemplateID), + pq.Array(arg.AgentID), + arg.ConnectionsByProto, + pq.Array(arg.ConnectionCount), + pq.Array(arg.RxPackets), + pq.Array(arg.RxBytes), + pq.Array(arg.TxPackets), + pq.Array(arg.TxBytes), + pq.Array(arg.SessionCountVSCode), + pq.Array(arg.SessionCountJetBrains), + pq.Array(arg.SessionCountReconnectingPTY), + pq.Array(arg.SessionCountSSH), + pq.Array(arg.ConnectionMedianLatencyMS), + ) + return err +} + const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external FROM workspace_apps WHERE agent_id = $1 AND slug = $2 ` diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 1a598bd6a6263..daba093a3d9e1 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -22,6 +22,46 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *; +-- name: InsertWorkspaceAgentStats :exec +INSERT INTO + workspace_agent_stats ( + id, + created_at, + user_id, + workspace_id, + template_id, + agent_id, + connections_by_proto, + connection_count, + rx_packets, + rx_bytes, + tx_packets, + tx_bytes, + session_count_vscode, + session_count_jetbrains, + session_count_reconnecting_pty, + session_count_ssh, + connection_median_latency_ms + ) +SELECT + unnest(@id :: uuid[]) AS id, + unnest(@created_at :: timestamptz[]) AS created_at, + unnest(@user_id :: uuid[]) AS user_id, + unnest(@workspace_id :: uuid[]) AS workspace_id, + unnest(@template_id :: uuid[]) AS template_id, + unnest(@agent_id :: uuid[]) AS agent_id, + jsonb_array_elements(@connections_by_proto :: jsonb) AS connections_by_proto, + unnest(@connection_count :: bigint[]) AS connection_count, + unnest(@rx_packets :: bigint[]) AS rx_packets, + unnest(@rx_bytes :: bigint[]) AS rx_bytes, + unnest(@tx_packets :: bigint[]) AS tx_packets, + unnest(@tx_bytes :: bigint[]) AS tx_bytes, + unnest(@session_count_vscode :: bigint[]) AS session_count_vscode, + unnest(@session_count_jetbrains :: bigint[]) AS session_count_jetbrains, + unnest(@session_count_reconnecting_pty :: bigint[]) AS session_count_reconnecting_pty, + unnest(@session_count_ssh :: bigint[]) AS session_count_ssh, + unnest(@connection_median_latency_ms :: double precision[]) AS connection_median_latency_ms; + -- name: GetTemplateDAUs :many SELECT (created_at at TIME ZONE cast(@tz_offset::integer as text))::date as date, diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index 3ea774df1186d..ad39ec840c526 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -11,6 +11,9 @@ import ( "testing" "time" + "github.com/coder/coder/coderd/batchstats" + "github.com/coder/coder/coderd/database/dbtestutil" + "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -372,9 +375,29 @@ func TestAgents(t *testing.T) { func TestAgentStats(t *testing.T) { t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + + db, pubsub := dbtestutil.NewDB(t) + log := slogtest.Make(t, nil) + + batcher, closeBatcher, err := batchstats.New(ctx, + batchstats.WithStore(db), + // We want our stats, and we want them NOW. + batchstats.WithBatchSize(1), + batchstats.WithInterval(time.Hour), + batchstats.WithLogger(log), + ) + require.NoError(t, err, "create stats batcher failed") + t.Cleanup(closeBatcher) + // Build sample workspaces with test agents and fake agent client - client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - db := api.Database + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + IncludeProvisionerDaemon: true, + Pubsub: pubsub, + StatsBatcher: batcher, + }) user := coderdtest.CreateFirstUser(t, client) @@ -384,11 +407,7 @@ func TestAgentStats(t *testing.T) { registry := prometheus.NewRegistry() - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - // given - var err error var i int64 for i = 0; i < 3; i++ { _, err = agent1.PostStats(ctx, &agentsdk.Stats{ diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 8567ff1d895b3..0f5607db73436 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1410,36 +1410,12 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID) } - payload, err := json.Marshal(req.ConnectionsByProto) - if err != nil { - api.Logger.Error(ctx, "marshal agent connections by proto", slog.F("workspace_agent_id", workspaceAgent.ID), slog.Error(err)) - payload = json.RawMessage("{}") - } - now := database.Now() var errGroup errgroup.Group errGroup.Go(func() error { - _, err = api.Database.InsertWorkspaceAgentStat(ctx, database.InsertWorkspaceAgentStatParams{ - ID: uuid.New(), - CreatedAt: now, - AgentID: workspaceAgent.ID, - WorkspaceID: workspace.ID, - UserID: workspace.OwnerID, - TemplateID: workspace.TemplateID, - ConnectionsByProto: payload, - ConnectionCount: req.ConnectionCount, - RxPackets: req.RxPackets, - RxBytes: req.RxBytes, - TxPackets: req.TxPackets, - TxBytes: req.TxBytes, - SessionCountVSCode: req.SessionCountVSCode, - SessionCountJetBrains: req.SessionCountJetBrains, - SessionCountReconnectingPTY: req.SessionCountReconnectingPTY, - SessionCountSSH: req.SessionCountSSH, - ConnectionMedianLatencyMS: req.ConnectionMedianLatencyMS, - }) - if err != nil { + if err := api.statsBatcher.Add(workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, req); err != nil { + api.Logger.Error(ctx, "failed to add stats to batcher", slog.Error(err)) return xerrors.Errorf("can't insert workspace agent stat: %w", err) } return nil From 9f5ac4d15d4c1c197694e5d91b4d473ea1867d96 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 4 Aug 2023 19:27:12 +0300 Subject: [PATCH 013/156] ci: publish main commit tag to `ghcr.io/coder/coder-preview` (#8897) * wip * push new tag and delete old tag * prune by filtering * fix permission * fix filter * keep last 2 versions * use first 7 characters of sha for tag * do not use gh cli * test * typo * use gh cli again * reduce days to 3 * fixup * typo * keep-last 5 * ready to merge * retain tags from last 7 days * test * ready --- .github/workflows/ci.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 55796e29cb446..3e5838f2a1501 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -701,6 +701,7 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} env: DOCKER_CLI_EXPERIMENTAL: "enabled" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v3 @@ -724,6 +725,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Linux amd64 Docker image + id: build_and_push run: | set -euxo pipefail go mod download @@ -738,3 +740,21 @@ jobs: --version $version \ --push \ build/coder_linux_amd64 + + # Get commit sha to be used as package tag + new_package_tag=$(gh api repos/coder/coder/commits/main | jq -r '.sha[0:7]') + + # Tag image with new package tag and push + docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$new_package_tag + docker push ghcr.io/coder/coder-preview:main-$new_package_tag + + - name: Prune old images + uses: vlaurin/action-ghcr-prune@v0.5.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + organization: coder + container: coder-preview + keep-younger-than: 7 # days + keep-tags-regexes: ^pr + prune-tags-regexes: ^main- + prune-untagged: true From 7224ff2af8d7b251c68765bc4f74d19e224b0ec8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 4 Aug 2023 17:33:05 +0100 Subject: [PATCH 014/156] fix(enterprise/replicasync): fix data race in Manager.Regional (#8910) --- enterprise/replicasync/replicasync.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/enterprise/replicasync/replicasync.go b/enterprise/replicasync/replicasync.go index 42bf402a6682e..d0ca629d25ae8 100644 --- a/enterprise/replicasync/replicasync.go +++ b/enterprise/replicasync/replicasync.go @@ -375,7 +375,13 @@ func (m *Manager) InRegion(regionID int32) []database.Replica { // Regional returns all replicas in the same region excluding itself. func (m *Manager) Regional() []database.Replica { - return m.InRegion(m.self.RegionID) + return m.InRegion(m.regionID()) +} + +func (m *Manager) regionID() int32 { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.self.RegionID } // SetCallback sets a function to execute whenever new peers From 8f7b6a29362625704f79494392583989e7e57ea6 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 4 Aug 2023 15:00:13 -0300 Subject: [PATCH 015/156] fix(site): fix date range on template insights (#8914) --- .../TemplateInsightsPage/TemplateInsightsPage.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 210458a6f2f44..d6161124d3540 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -24,14 +24,15 @@ import { UserLatencyInsightsResponse, } from "api/typesGenerated" import { ComponentProps } from "react" -import subDays from "date-fns/subDays" +import { subDays, addHours, startOfHour } from "date-fns" export default function TemplateInsightsPage() { + const now = new Date() const { template } = useTemplateLayoutContext() const insightsFilter = { template_ids: template.id, - start_time: toTimeFilter(sevenDaysAgo()), - end_time: toTimeFilter(new Date()), + start_time: toStartTimeFilter(subDays(now, 7)), + end_time: startOfHour(addHours(now, 1)).toISOString(), } const { data: templateInsights } = useQuery({ queryKey: ["templates", template.id, "usage"], @@ -323,12 +324,11 @@ function mapToDAUsResponse( } } -function toTimeFilter(date: Date) { +function toStartTimeFilter(date: Date) { date.setHours(0, 0, 0, 0) const year = date.getUTCFullYear() const month = String(date.getUTCMonth() + 1).padStart(2, "0") const day = String(date.getUTCDate()).padStart(2, "0") - return `${year}-${month}-${day}T00:00:00Z` } @@ -348,7 +348,3 @@ function formatTime(seconds: number): string { return hours.toFixed(1) + " hours" } } - -function sevenDaysAgo() { - return subDays(new Date(), 7) -} From 0c7ff4fb8acb539640e1bcc5dbf21113c71ab110 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Fri, 4 Aug 2023 17:03:21 -0500 Subject: [PATCH 016/156] fix(enterprise): ensure SCIM create user can unsuspend (#8916) --- enterprise/coderd/scim.go | 29 ++++++++++++---- enterprise/coderd/scim_test.go | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go index efba55b932684..801ca61349ae3 100644 --- a/enterprise/coderd/scim.go +++ b/enterprise/coderd/scim.go @@ -155,7 +155,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) { } //nolint:gocritic - user, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{ + dbUser, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{ Email: email, Username: sUser.UserName, }) @@ -164,8 +164,22 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) { return } if err == nil { - sUser.ID = user.ID.String() - sUser.UserName = user.Username + sUser.ID = dbUser.ID.String() + sUser.UserName = dbUser.Username + + if sUser.Active && dbUser.Status == database.UserStatusSuspended { + //nolint:gocritic + _, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{ + ID: dbUser.ID, + // The user will get transitioned to Active after logging in. + Status: database.UserStatusDormant, + UpdatedAt: database.Now(), + }) + if err != nil { + _ = handlerutil.WriteError(rw, err) + return + } + } httpapi.Write(ctx, rw, http.StatusOK, sUser) return @@ -201,7 +215,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) { } //nolint:gocritic // needed for SCIM - user, _, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{ + dbUser, _, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{ CreateUserRequest: codersdk.CreateUserRequest{ Username: sUser.UserName, Email: email, @@ -214,8 +228,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) { return } - sUser.ID = user.ID.String() - sUser.UserName = user.Username + sUser.ID = dbUser.ID.String() + sUser.UserName = dbUser.Username httpapi.Write(ctx, rw, http.StatusOK, sUser) } @@ -263,7 +277,8 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) { var status database.UserStatus if sUser.Active { - status = database.UserStatusActive + // The user will get transitioned to Active after logging in. + status = database.UserStatusDormant } else { status = database.UserStatusSuspended } diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go index f0778c26b51d0..a74dc9bf3452b 100644 --- a/enterprise/coderd/scim_test.go +++ b/enterprise/coderd/scim_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "testing" @@ -164,6 +165,54 @@ func TestScim(t *testing.T) { assert.Equal(t, sUser.UserName, userRes.Users[0].Username) }) + t.Run("Unsuspend", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + scimAPIKey := []byte("hi") + client, _ := coderdenttest.New(t, &coderdenttest.Options{ + SCIMAPIKey: scimAPIKey, + LicenseOptions: &coderdenttest.LicenseOptions{ + AccountID: "coolin", + Features: license.Features{ + codersdk.FeatureSCIM: 1, + }, + }, + }) + + sUser := makeScimUser(t) + res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey)) + require.NoError(t, err) + defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + err = json.NewDecoder(res.Body).Decode(&sUser) + require.NoError(t, err) + + sUser.Active = false + res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey)) + require.NoError(t, err) + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + + sUser.Active = true + res, err = client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey)) + require.NoError(t, err) + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + + userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value}) + require.NoError(t, err) + require.Len(t, userRes.Users, 1) + + assert.Equal(t, sUser.Emails[0].Value, userRes.Users[0].Email) + assert.Equal(t, sUser.UserName, userRes.Users[0].Username) + assert.Equal(t, codersdk.UserStatusDormant, userRes.Users[0].Status) + }) + t.Run("DomainStrips", func(t *testing.T) { t.Parallel() @@ -185,7 +234,8 @@ func TestScim(t *testing.T) { sUser.UserName = sUser.UserName + "@coder.com" res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey)) require.NoError(t, err) - defer res.Body.Close() + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() assert.Equal(t, http.StatusOK, res.StatusCode) userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value}) @@ -220,7 +270,8 @@ func TestScim(t *testing.T) { res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{}) require.NoError(t, err) - defer res.Body.Close() + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() assert.Equal(t, http.StatusNotFound, res.StatusCode) }) @@ -242,7 +293,8 @@ func TestScim(t *testing.T) { res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{}) require.NoError(t, err) - defer res.Body.Close() + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() assert.Equal(t, http.StatusInternalServerError, res.StatusCode) }) @@ -276,7 +328,8 @@ func TestScim(t *testing.T) { res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey)) require.NoError(t, err) - defer res.Body.Close() + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() assert.Equal(t, http.StatusOK, res.StatusCode) userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value}) From eddaa7781d9fd6f01cdb41db074f8aa603eea3ef Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Fri, 4 Aug 2023 17:54:27 -0500 Subject: [PATCH 017/156] fix: don't close cached tailnet on pty close (#8917) --- coderd/workspaceapps/proxy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index 9b2d9c4bfa297..d75970bcfa8d9 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -669,7 +669,6 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { return } defer release() - defer agentConn.Close() log.Debug(ctx, "dialed workspace agent") ptNetConn, err := agentConn.ReconnectingPTY(ctx, reconnect, uint16(height), uint16(width), r.URL.Query().Get("command")) if err != nil { From 81752d1b84ce837827f42ab6b9c898f934ebe121 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sat, 5 Aug 2023 11:25:37 -0500 Subject: [PATCH 018/156] fix(cli/delete): prompt for confirmation after workspace is found (#8579) --- cli/delete.go | 17 ++++++++++------- cli/delete_test.go | 8 ++++---- codersdk/workspaces.go | 4 ++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cli/delete.go b/cli/delete.go index 867abe0326a30..3a440c6840381 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -22,16 +22,19 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { - _, err := cliui.Prompt(inv, cliui.PromptOptions{ - Text: "Confirm delete workspace?", - IsConfirm: true, - Default: cliui.ConfirmNo, - }) + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err } - workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) + sinceLastUsed := time.Since(workspace.LastUsedAt) + cliui.Infof(inv.Stderr, "%v was last used %.0f days ago", workspace.FullName(), sinceLastUsed.Hours()/24) + + _, err = cliui.Prompt(inv, cliui.PromptOptions{ + Text: "Confirm delete workspace?", + IsConfirm: true, + Default: cliui.ConfirmNo, + }) if err != nil { return err } @@ -51,7 +54,7 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd { return err } - _, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp))) + _, _ = fmt.Fprintf(inv.Stdout, "\n%s has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.FullName()), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp))) return nil }, } diff --git a/cli/delete_test.go b/cli/delete_test.go index 40f5b9ac22168..3530a60097094 100644 --- a/cli/delete_test.go +++ b/cli/delete_test.go @@ -41,7 +41,7 @@ func TestDelete(t *testing.T) { assert.ErrorIs(t, err, io.EOF) } }() - pty.ExpectMatch("workspace has been deleted") + pty.ExpectMatch("has been deleted") <-doneChan }) @@ -68,7 +68,7 @@ func TestDelete(t *testing.T) { assert.ErrorIs(t, err, io.EOF) } }() - pty.ExpectMatch("workspace has been deleted") + pty.ExpectMatch("has been deleted") <-doneChan }) @@ -113,7 +113,7 @@ func TestDelete(t *testing.T) { assert.ErrorIs(t, err, io.EOF) } }() - pty.ExpectMatch("workspace has been deleted") + pty.ExpectMatch("has been deleted") <-doneChan }) @@ -145,7 +145,7 @@ func TestDelete(t *testing.T) { } }() - pty.ExpectMatch("workspace has been deleted") + pty.ExpectMatch("has been deleted") <-doneChan workspace, err = client.Workspace(context.Background(), workspace.ID) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index a2ef823fcb87e..2ef9840ac4be0 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -48,6 +48,10 @@ type Workspace struct { Health WorkspaceHealth `json:"health"` } +func (w Workspace) FullName() string { + return fmt.Sprintf("%s/%s", w.OwnerName, w.Name) +} + type WorkspaceHealth struct { Healthy bool `json:"healthy" example:"false"` // Healthy is true if the workspace is healthy. FailingAgents []uuid.UUID `json:"failing_agents" format:"uuid"` // FailingAgents lists the IDs of the agents that are failing, if any. From e7047726d80a80004c0c1e6cca5a245fd46f48e2 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 7 Aug 2023 13:00:05 +0300 Subject: [PATCH 019/156] docs: fix a broken link in docs.changelogs/README.md (#8937) --- docs/changelogs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelogs/README.md b/docs/changelogs/README.md index e5740216e4f87..7b1945aa02a2d 100644 --- a/docs/changelogs/README.md +++ b/docs/changelogs/README.md @@ -1,8 +1,8 @@ # Changelogs -These are the changelogs used by [get-changelog.sh](https://github.com/coder/coder/blob/main/scripts/release/changelog.sh) for a release. +These are the changelogs used by [generate_release_notes.sh]https://github.com/coder/coder/blob/main/scripts/release/generate_release_notes.sh) for a release. -These changelogs are currently not kept in-sync with GitHub releases. Use [GitHub releases](https://github.com/coder/coder/releases) for the latest information! +These changelogs are currently not kept in sync with GitHub releases. Use [GitHub releases](https://github.com/coder/coder/releases) for the latest information! ## Writing a changelog From 90c1647fcf4dd6f95dd5265cf22f41e4d2aa2438 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 7 Aug 2023 16:41:20 +0300 Subject: [PATCH 020/156] ci: change `ghcr.io/coder/coder-preview:main` tag to use version names (#8938) --- .github/workflows/ci.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e5838f2a1501..7aa367acd3032 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -701,7 +701,6 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} env: DOCKER_CLI_EXPERIMENTAL: "enabled" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v3 @@ -741,12 +740,9 @@ jobs: --push \ build/coder_linux_amd64 - # Get commit sha to be used as package tag - new_package_tag=$(gh api repos/coder/coder/commits/main | jq -r '.sha[0:7]') - # Tag image with new package tag and push - docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$new_package_tag - docker push ghcr.io/coder/coder-preview:main-$new_package_tag + docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$version + docker push ghcr.io/coder/coder-preview:main-$version - name: Prune old images uses: vlaurin/action-ghcr-prune@v0.5.0 From 71ea5ace07bf6ad28cdba9317fbfcf55afa68723 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Aug 2023 09:34:39 -0500 Subject: [PATCH 021/156] feat: add login type to users page (#8912) --- site/src/components/UsersTable/UsersTable.tsx | 6 ++++-- site/src/components/UsersTable/UsersTableBody.tsx | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index 6c991ee2a1dd0..d84386a2e968e 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -15,6 +15,7 @@ export const Language = { rolesLabel: "Roles", statusLabel: "Status", lastSeenLabel: "Last Seen", + loginTypeLabel: "Login Type", } export interface UsersTableProps { @@ -69,8 +70,9 @@ export const UsersTable: FC> = ({ - {Language.statusLabel} - {Language.lastSeenLabel} + {Language.loginTypeLabel} + {Language.statusLabel} + {Language.lastSeenLabel} {/* 1% is a trick to make the table cell width fit the content */} {canEditUsers && } diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index 87fc5725a2de7..ad25de524f3a8 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -155,6 +155,9 @@ export const UsersTableBody: FC< ))} + +
{user.login_type}
+
Date: Mon, 7 Aug 2023 17:35:28 +0300 Subject: [PATCH 022/156] fix: update tag name for `coder-preview` image in ci.yaml (#8945) --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7aa367acd3032..dcfbf9fa409d5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -741,8 +741,9 @@ jobs: build/coder_linux_amd64 # Tag image with new package tag and push - docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$version - docker push ghcr.io/coder/coder-preview:main-$version + tag=$(echo "$version" | sed 's/+/-/g') + docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$tag + docker push ghcr.io/coder/coder-preview:main-$tag - name: Prune old images uses: vlaurin/action-ghcr-prune@v0.5.0 From 82e0e2e43c59020a914145983f39f935bdccc3ce Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 7 Aug 2023 16:26:16 +0100 Subject: [PATCH 023/156] fix(cli): clistat: accept positional arg for stat disk cmd (#8911) --- cli/stat.go | 8 ++++++++ cli/stat_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cli/stat.go b/cli/stat.go index 3e32c4187f93b..3657a4f3c71c9 100644 --- a/cli/stat.go +++ b/cli/stat.go @@ -233,8 +233,16 @@ func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd { }, Handler: func(inv *clibase.Invocation) error { pfx := clistat.ParsePrefix(prefixArg) + // Users may also call `coder stat disk `. + if len(inv.Args) > 0 { + pathArg = inv.Args[0] + } ds, err := s.Disk(pfx, pathArg) if err != nil { + if os.IsNotExist(err) { + // fmt.Errorf produces a more concise error. + return fmt.Errorf("not found: %q", pathArg) + } return err } diff --git a/cli/stat_test.go b/cli/stat_test.go index 7b37a98ce9113..001177f9d95dd 100644 --- a/cli/stat_test.go +++ b/cli/stat_test.go @@ -170,4 +170,16 @@ func TestStatDiskCmd(t *testing.T) { require.NotZero(t, *tmp.Total) require.Equal(t, "B", tmp.Unit) }) + + t.Run("PosArg", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + inv, _ := clitest.New(t, "stat", "disk", "/this/path/does/not/exist", "--output=text") + buf := new(bytes.Buffer) + inv.Stdout = buf + err := inv.WithContext(ctx).Run() + require.Error(t, err) + require.Contains(t, err.Error(), `not found: "/this/path/does/not/exist"`) + }) } From 67ff2077a63e1cd40814b00402361db668c00a1e Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 7 Aug 2023 08:52:06 -0700 Subject: [PATCH 024/156] feat: add derp only text to proxies list in dashboard (#8932) --- site/src/components/DeploySettingsLayout/Badges.tsx | 8 ++++++-- .../WorkspaceProxyPage/WorkspaceProxyRow.tsx | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/site/src/components/DeploySettingsLayout/Badges.tsx b/site/src/components/DeploySettingsLayout/Badges.tsx index f9d34bca918b6..465422ef03a3f 100644 --- a/site/src/components/DeploySettingsLayout/Badges.tsx +++ b/site/src/components/DeploySettingsLayout/Badges.tsx @@ -23,11 +23,15 @@ export const EntitledBadge: FC = () => { ) } -export const HealthyBadge: FC = () => { +export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => { const styles = useStyles() + let text = "Healthy" + if (derpOnly) { + text = "Healthy (DERP Only)" + } return ( - Healthy + {text} ) } diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx index e106eb61ea973..760d6f5ca8cd8 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx @@ -169,9 +169,14 @@ const DetailedProxyStatus: FC<{ return } + let derpOnly = false + if ("derp_only" in proxy) { + derpOnly = proxy.derp_only + } + switch (proxy.status.status) { case "ok": - return + return case "unhealthy": return case "unreachable": @@ -189,7 +194,7 @@ const ProxyStatus: FC<{ }> = ({ proxy }) => { let icon = if (proxy.healthy) { - icon = + icon = } return icon From 00be8ab875872cebb701a40597be8a7dc5c22835 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 18:54:20 +0300 Subject: [PATCH 025/156] chore: bump the github-actions group with 1 update (#8942) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dcfbf9fa409d5..bc1af416e3901 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -137,7 +137,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@v1.16.1 + uses: crate-ci/typos@v1.16.2 with: config: .github/workflows/typos.toml From e8627195a2923295cc96784ad77d87e2694fb887 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 7 Aug 2023 18:11:44 +0200 Subject: [PATCH 026/156] feat(coderd): expose parameter description and type (#8944) --- coderd/apidoc/docs.go | 6 +++ coderd/apidoc/swagger.json | 6 +++ coderd/database/db2sdk/db2sdk.go | 71 +++++++++++++++++++++++----- coderd/database/dbfake/dbfake.go | 2 + coderd/database/queries.sql.go | 10 ++-- coderd/database/queries/insights.sql | 8 ++-- coderd/insights.go | 38 +-------------- coderd/insights_test.go | 6 +++ codersdk/insights.go | 2 + docs/api/insights.md | 2 + docs/api/schemas.md | 8 ++++ site/src/api/typesGenerated.ts | 2 + 12 files changed, 108 insertions(+), 53 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8d7920c2d2e8d..61a6989ba27fd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9585,6 +9585,9 @@ const docTemplate = `{ "codersdk.TemplateParameterUsage": { "type": "object", "properties": { + "description": { + "type": "string" + }, "display_name": { "type": "string" }, @@ -9604,6 +9607,9 @@ const docTemplate = `{ "format": "uuid" } }, + "type": { + "type": "string" + }, "values": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bc4c0e7b8a958..982ed816264c1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8665,6 +8665,9 @@ "codersdk.TemplateParameterUsage": { "type": "object", "properties": { + "description": { + "type": "string" + }, "display_name": { "type": "string" }, @@ -8684,6 +8687,9 @@ "format": "uuid" } }, + "type": { + "type": "string" + }, "values": { "type": "array", "items": { diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 263611d5b168b..893cfdadea336 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -3,6 +3,7 @@ package db2sdk import ( "encoding/json" + "sort" "github.com/google/uuid" @@ -29,20 +30,10 @@ func WorkspaceBuildParameter(p database.WorkspaceBuildParameter) codersdk.Worksp } func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) { - var protoOptions []*proto.RichParameterOption - err := json.Unmarshal(param.Options, &protoOptions) + options, err := templateVersionParameterOptions(param.Options) if err != nil { return codersdk.TemplateVersionParameter{}, err } - options := make([]codersdk.TemplateVersionParameterOption, 0) - for _, option := range protoOptions { - options = append(options, codersdk.TemplateVersionParameterOption{ - Name: option.Name, - Description: option.Description, - Value: option.Value, - Icon: option.Icon, - }) - } descriptionPlaintext, err := parameter.Plaintext(param.Description) if err != nil { @@ -132,3 +123,61 @@ func Role(role rbac.Role) codersdk.Role { Name: role.Name, } } + +func TemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) { + parametersByNum := make(map[int64]*codersdk.TemplateParameterUsage) + for _, param := range parameterRows { + if _, ok := parametersByNum[param.Num]; !ok { + var opts []codersdk.TemplateVersionParameterOption + err := json.Unmarshal(param.Options, &opts) + if err != nil { + return nil, err + } + + plaintextDescription, err := parameter.Plaintext(param.Description) + if err != nil { + return nil, err + } + + parametersByNum[param.Num] = &codersdk.TemplateParameterUsage{ + TemplateIDs: param.TemplateIDs, + Name: param.Name, + Type: param.Type, + DisplayName: param.DisplayName, + Description: plaintextDescription, + Options: opts, + } + } + parametersByNum[param.Num].Values = append(parametersByNum[param.Num].Values, codersdk.TemplateParameterValue{ + Value: param.Value, + Count: param.Count, + }) + } + parametersUsage := []codersdk.TemplateParameterUsage{} + for _, param := range parametersByNum { + parametersUsage = append(parametersUsage, *param) + } + + sort.Slice(parametersUsage, func(i, j int) bool { + return parametersUsage[i].Name < parametersUsage[j].Name + }) + return parametersUsage, nil +} + +func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.TemplateVersionParameterOption, error) { + var protoOptions []*proto.RichParameterOption + err := json.Unmarshal(rawOptions, &protoOptions) + if err != nil { + return nil, err + } + var options []codersdk.TemplateVersionParameterOption + for _, option := range protoOptions { + options = append(options, codersdk.TemplateVersionParameterOption{ + Name: option.Name, + Description: option.Description, + Value: option.Value, + Icon: option.Icon, + }) + } + return options, nil +} diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index cc0d05aaee9c3..c968a52f8d00d 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -2186,6 +2186,7 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data uniqueTemplateParams[key] = &database.GetTemplateParameterInsightsRow{ Num: num, Name: tvp.Name, + Type: tvp.Type, DisplayName: tvp.DisplayName, Description: tvp.Description, Options: tvp.Options, @@ -2220,6 +2221,7 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data TemplateIDs: uniqueSortedUUIDs(utp.TemplateIDs), Name: utp.Name, DisplayName: utp.DisplayName, + Type: utp.Type, Description: utp.Description, Options: utp.Options, Value: value, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3e07fc22f1aa3..8748e4d7ca9ed 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1582,16 +1582,18 @@ WITH latest_workspace_builds AS ( tvp.name, tvp.display_name, tvp.description, - tvp.options + tvp.options, + tvp.type FROM latest_workspace_builds wb JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id) - GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options + GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options, tvp.type ) SELECT utp.num, utp.template_ids, utp.name, + utp.type, utp.display_name, utp.description, utp.options, @@ -1599,7 +1601,7 @@ SELECT COUNT(wbp.value) AS count FROM unique_template_params utp JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name) -GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value +GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, utp.type, wbp.value ` type GetTemplateParameterInsightsParams struct { @@ -1612,6 +1614,7 @@ type GetTemplateParameterInsightsRow struct { Num int64 `db:"num" json:"num"` TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` Name string `db:"name" json:"name"` + Type string `db:"type" json:"type"` DisplayName string `db:"display_name" json:"display_name"` Description string `db:"description" json:"description"` Options json.RawMessage `db:"options" json:"options"` @@ -1636,6 +1639,7 @@ func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTe &i.Num, pq.Array(&i.TemplateIDs), &i.Name, + &i.Type, &i.DisplayName, &i.Description, &i.Options, diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index 94a80117dcc1d..ad8c581161e99 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -149,16 +149,18 @@ WITH latest_workspace_builds AS ( tvp.name, tvp.display_name, tvp.description, - tvp.options + tvp.options, + tvp.type FROM latest_workspace_builds wb JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id) - GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options + GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options, tvp.type ) SELECT utp.num, utp.template_ids, utp.name, + utp.type, utp.display_name, utp.description, utp.options, @@ -166,4 +168,4 @@ SELECT COUNT(wbp.value) AS count FROM unique_template_params utp JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name) -GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value; +GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, utp.type, wbp.value; diff --git a/coderd/insights.go b/coderd/insights.go index b643303dd0df2..509e78a18e234 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -2,10 +2,8 @@ package coderd import ( "context" - "encoding/json" "fmt" "net/http" - "sort" "time" "github.com/google/uuid" @@ -13,6 +11,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/db2sdk" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" @@ -244,7 +243,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) { return } - parametersUsage, err := convertTemplateInsightsParameters(parameterRows) + parametersUsage, err := db2sdk.TemplateInsightsParameters(parameterRows) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting template parameter insights.", @@ -315,39 +314,6 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [ } } -func convertTemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) { - parametersByNum := make(map[int64]*codersdk.TemplateParameterUsage) - for _, param := range parameterRows { - if _, ok := parametersByNum[param.Num]; !ok { - var opts []codersdk.TemplateVersionParameterOption - err := json.Unmarshal(param.Options, &opts) - if err != nil { - return nil, xerrors.Errorf("unmarshal template parameter options: %w", err) - } - parametersByNum[param.Num] = &codersdk.TemplateParameterUsage{ - TemplateIDs: param.TemplateIDs, - Name: param.Name, - DisplayName: param.DisplayName, - Options: opts, - } - } - parametersByNum[param.Num].Values = append(parametersByNum[param.Num].Values, codersdk.TemplateParameterValue{ - Value: param.Value, - Count: param.Count, - }) - } - parametersUsage := []codersdk.TemplateParameterUsage{} - for _, param := range parametersByNum { - parametersUsage = append(parametersUsage, *param) - } - - sort.Slice(parametersUsage, func(i, j int) bool { - return parametersUsage[i].Name < parametersUsage[j].Name - }) - - return parametersUsage, nil -} - // parseInsightsStartAndEndTime parses the start and end time query parameters // and returns the parsed values. The client provided timezone must be preserved // when parsing the time. Verification is performed so that the start and end diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 2469cc0f4b362..be75b31f5e80e 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -405,6 +405,8 @@ func TestTemplateInsights(t *testing.T) { // The workspace uses 3 parameters require.Len(t, resp.Report.ParametersUsage, 3) assert.Equal(t, firstParameterName, resp.Report.ParametersUsage[0].Name) + assert.Equal(t, firstParameterType, resp.Report.ParametersUsage[0].Type) + assert.Equal(t, firstParameterDescription, resp.Report.ParametersUsage[0].Description) assert.Equal(t, firstParameterDisplayName, resp.Report.ParametersUsage[0].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[0].Values, codersdk.TemplateParameterValue{ Value: firstParameterValue, @@ -414,6 +416,8 @@ func TestTemplateInsights(t *testing.T) { assert.Empty(t, resp.Report.ParametersUsage[0].Options) assert.Equal(t, secondParameterName, resp.Report.ParametersUsage[1].Name) + assert.Equal(t, secondParameterType, resp.Report.ParametersUsage[1].Type) + assert.Equal(t, secondParameterDescription, resp.Report.ParametersUsage[1].Description) assert.Equal(t, secondParameterDisplayName, resp.Report.ParametersUsage[1].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[1].Values, codersdk.TemplateParameterValue{ Value: secondParameterValue, @@ -423,6 +427,8 @@ func TestTemplateInsights(t *testing.T) { assert.Empty(t, resp.Report.ParametersUsage[1].Options) assert.Equal(t, thirdParameterName, resp.Report.ParametersUsage[2].Name) + assert.Equal(t, thirdParameterType, resp.Report.ParametersUsage[2].Type) + assert.Equal(t, thirdParameterDescription, resp.Report.ParametersUsage[2].Description) assert.Equal(t, thirdParameterDisplayName, resp.Report.ParametersUsage[2].DisplayName) assert.Contains(t, resp.Report.ParametersUsage[2].Values, codersdk.TemplateParameterValue{ Value: thirdParameterValue, diff --git a/codersdk/insights.go b/codersdk/insights.go index bfc51bd208dd6..24e2ba140dd76 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -138,6 +138,8 @@ type TemplateParameterUsage struct { TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` DisplayName string `json:"display_name"` Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` Options []TemplateVersionParameterOption `json:"options,omitempty"` Values []TemplateParameterValue `json:"values"` } diff --git a/docs/api/insights.md b/docs/api/insights.md index 11843e63f0316..3c421c64173cd 100644 --- a/docs/api/insights.md +++ b/docs/api/insights.md @@ -80,6 +80,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \ "end_time": "2019-08-24T14:15:22Z", "parameters_usage": [ { + "description": "string", "display_name": "string", "name": "string", "options": [ @@ -91,6 +92,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \ } ], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "type": "string", "values": [ { "count": 0, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 192cc9625eddd..2f09bfb6728ae 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4321,6 +4321,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "end_time": "2019-08-24T14:15:22Z", "parameters_usage": [ { + "description": "string", "display_name": "string", "name": "string", "options": [ @@ -4332,6 +4333,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "type": "string", "values": [ { "count": 0, @@ -4384,6 +4386,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "end_time": "2019-08-24T14:15:22Z", "parameters_usage": [ { + "description": "string", "display_name": "string", "name": "string", "options": [ @@ -4395,6 +4398,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "type": "string", "values": [ { "count": 0, @@ -4420,6 +4424,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { + "description": "string", "display_name": "string", "name": "string", "options": [ @@ -4431,6 +4436,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "type": "string", "values": [ { "count": 0, @@ -4444,10 +4450,12 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | Name | Type | Required | Restrictions | Description | | -------------- | ------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `description` | string | false | | | | `display_name` | string | false | | | | `name` | string | false | | | | `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | | | `template_ids` | array of string | false | | | +| `type` | string | false | | | | `values` | array of [codersdk.TemplateParameterValue](#codersdktemplateparametervalue) | false | | | ## codersdk.TemplateParameterValue diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f8b91abb925b6..7b9b79d69b3db 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -987,6 +987,8 @@ export interface TemplateParameterUsage { readonly template_ids: string[] readonly display_name: string readonly name: string + readonly type: string + readonly description: string readonly options?: TemplateVersionParameterOption[] readonly values: TemplateParameterValue[] } From 9f76381fc1c0bc986951dd86c7de32413895a597 Mon Sep 17 00:00:00 2001 From: sharkymark Date: Mon, 7 Aug 2023 11:31:43 -0500 Subject: [PATCH 027/156] chore: add install more providers step in jetbrains docs; update images (#8943) --- docs/ides/gateway.md | 107 +++++++++++------- .../gateway/plugin-connect-to-coder.png | Bin 12715 -> 43688 bytes docs/images/gateway/plugin-ide-list.png | Bin 20753 -> 90242 bytes docs/images/gateway/plugin-select-ide.png | Bin 23405 -> 87207 bytes docs/images/gateway/plugin-session-token.png | Bin 29095 -> 135600 bytes .../gateway/plugin-settings-marketplace.png | Bin 58881 -> 115955 bytes 6 files changed, 64 insertions(+), 43 deletions(-) diff --git a/docs/ides/gateway.md b/docs/ides/gateway.md index cb5b62be53f3f..3f2afbd9b55c3 100644 --- a/docs/ides/gateway.md +++ b/docs/ides/gateway.md @@ -1,43 +1,62 @@ # JetBrains Gateway -JetBrains Gateway is a compact desktop app that allows you to work remotely with a JetBrains IDE without even downloading one. [See JetBrains' website to learn about and Gateway.](https://www.jetbrains.com/remote-development/gateway/) +JetBrains Gateway is a compact desktop app that allows you to work remotely with +a JetBrains IDE without even downloading one. [See JetBrains' website to learn +about and Gateway.](https://www.jetbrains.com/remote-development/gateway/) -Gateway can connect to a Coder workspace by using Coder's Gateway plugin or manually setting up an SSH connection. +Gateway can connect to a Coder workspace by using Coder's Gateway plugin or +manually setting up an SSH connection. ## Using Coder's JetBrains Gateway Plugin -> If you experience problems, please [create a GitHub issue](https://github.com/coder/coder/issues) or share in [our Discord channel](https://discord.gg/coder). +> If you experience problems, please [create a GitHub +> issue](https://github.com/coder/coder/issues) or share in [our Discord +> channel](https://discord.gg/coder). 1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html) -1. Open Gateway and click the gear icon at the bottom left and then "Settings" -1. In the Marketplace tab within Plugins, type Coder and then click "Install" and "OK" - ![Gateway Settings and Marketplace](../images/gateway/plugin-settings-marketplace.png) -1. Click the new "Coder" icon on the Gateway home screen - ![Gateway Connect to Coder](../images/gateway/plugin-connect-to-coder.png) -1. Enter your Coder deployment's Access Url and click "Connect" then paste the Session Token and click "OK" - ![Gateway Session Token](../images/gateway/plugin-session-token.png) -1. Click the "+" icon to open a browser and go to the templates page in your Coder deployment to create a workspace -1. If a workspace already exists but is stopped, click the green arrow to start the workspace +1. Open Gateway and click the Coder icon to install the Coder plugin. +1. Click the "Coder" icon under Install More Providers at the bottom of the + Gateway home screen +1. Click "Connect to Coder" at the top of the Gateway home screen to launch the + plugin ![Gateway Connect to +Coder](../images/gateway/plugin-connect-to-coder.png) +1. Enter your Coder deployment's Access Url and click "Connect" then paste the + Session Token and click "OK" ![Gateway Session +Token](../images/gateway/plugin-session-token.png) +1. Click the "+" icon to open a browser and go to the templates page in your + Coder deployment to create a workspace +1. If a workspace already exists but is stopped, click the green arrow to start + the workspace 1. Once the workspace status says Running, click "Select IDE and Project" ![Gateway IDE List](../images/gateway/plugin-select-ide.png) -1. Select the JetBrains IDE for your project and the project directory then click "Start IDE and connect" - ![Gateway Select IDE](../images/gateway/plugin-ide-list.png) - ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) +1. Select the JetBrains IDE for your project and the project directory then + click "Start IDE and connect" ![Gateway Select +IDE](../images/gateway/plugin-ide-list.png) ![Gateway IDE +Opened](../images/gateway/gateway-intellij-opened.png) -> Note the JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist` +> Note the JetBrains IDE is remotely installed into +> `~/.cache/JetBrains/RemoteDev/dist` + +### Update a Coder plugin version + +1. Click the gear icon at the bottom left of the Gateway home screen and then + "Settings" +1. In the Marketplace tab within Plugins, type Coder and if a newer plugin + release is available, click "Update" and "OK" ![Gateway Settings and +Marketplace](../images/gateway/plugin-settings-marketplace.png) ### Configuring the Gateway plugin to use internal certificates -When attempting to connect to a Coder deployment that uses internally signed certificates, -you may receive the following error in Gateway: +When attempting to connect to a Coder deployment that uses internally signed +certificates, you may receive the following error in Gateway: ```console Failed to configure connection to https://coder.internal.enterprise/: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target ``` -To resolve this issue, you will need to add Coder's certificate to the Java trust store -present on your local machine. Here is the default location of the trust store for -each OS: +To resolve this issue, you will need to add Coder's certificate to the Java +trust store present on your local machine. Here is the default location of the +trust store for each OS: ```console # Linux @@ -52,8 +71,8 @@ C:\Program Files (x86)\\jre\lib\security\cacerts %USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts # Path for Toolbox installation ``` -To add the certificate to the keystore, you can use the `keytool` utility that ships -with Java: +To add the certificate to the keystore, you can use the `keytool` utility that +ships with Java: ```console keytool -import -alias coder -file -keystore /path/to/trust/store @@ -77,37 +96,37 @@ keytool -import -alias coder -file cacert.pem -keystore /Applications/JetBrains\ ## Manually Configuring A JetBrains Gateway Connection -> This is in lieu of using Coder's Gateway plugin which automatically performs these steps. +> This is in lieu of using Coder's Gateway plugin which automatically performs +> these steps. 1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html) 1. [Configure the `coder` CLI](../ides.md#ssh-configuration) 1. Open Gateway, make sure "SSH" is selected under "Remote Development" -1. Click "New Connection" - ![Gateway Home](../images/gateway/gateway-home.png) +1. Click "New Connection" ![Gateway Home](../images/gateway/gateway-home.png) 1. In the resulting dialog, click the gear icon to the right of "Connection:" ![Gateway New Connection](../images/gateway/gateway-new-connection.png) -1. Hit the "+" button to add a new SSH connection - ![Gateway Add Connection](../images/gateway/gateway-add-ssh-configuration.png) +1. Hit the "+" button to add a new SSH connection ![Gateway Add +Connection](../images/gateway/gateway-add-ssh-configuration.png) 1. For the Host, enter `coder.` 1. For the Port, enter `22` (this is ignored by Coder) 1. For the Username, enter your workspace username -1. For the Authentication Type, select "OpenSSH config and authentication - agent" +1. For the Authentication Type, select "OpenSSH config and authentication agent" 1. Make sure the checkbox for "Parse config file ~/.ssh/config" is checked. 1. Click "Test Connection" to validate these settings. -1. Click "OK" - ![Gateway SSH Configuration](../images/gateway/gateway-create-ssh-configuration.png) -1. Select the connection you just added - ![Gateway Welcome](../images/gateway/gateway-welcome.png) -1. Click "Check Connection and Continue" - ![Gateway Continue](../images/gateway/gateway-continue.png) -1. Select the JetBrains IDE for your project and the project directory. - SSH into your server to create a directory or check out code if you haven't already. +1. Click "OK" ![Gateway SSH +Configuration](../images/gateway/gateway-create-ssh-configuration.png) +1. Select the connection you just added ![Gateway +Welcome](../images/gateway/gateway-welcome.png) +1. Click "Check Connection and Continue" ![Gateway +Continue](../images/gateway/gateway-continue.png) +1. Select the JetBrains IDE for your project and the project directory. SSH into + your server to create a directory or check out code if you haven't already. ![Gateway Choose IDE](../images/gateway/gateway-choose-ide.png) - > Note the JetBrains IDE is remotely installed into `~/. cache/JetBrains/RemoteDev/dist` -1. Click "Download and Start IDE" to connect. - ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) + > Note the JetBrains IDE is remotely installed into `~/. +cache/JetBrains/RemoteDev/dist` +1. Click "Download and Start IDE" to connect. ![Gateway IDE +Opened](../images/gateway/gateway-intellij-opened.png) ## Using an existing JetBrains installation in the workspace @@ -121,7 +140,9 @@ cd /opt/idea/bin ./remote-dev-server.sh registerBackendLocationForGateway ``` -> Gateway only works with paid versions of JetBrains IDEs so the script will not be located in the `bin` directory of JetBrains Community editions. +> Gateway only works with paid versions of JetBrains IDEs so the script will not +> be located in the `bin` directory of JetBrains Community editions. -[Here is the JetBrains article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F) +[Here is the JetBrains +article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F) explaining this IDE specification. diff --git a/docs/images/gateway/plugin-connect-to-coder.png b/docs/images/gateway/plugin-connect-to-coder.png index 11cecf4ad69feb1e6e31f32b48476c955fe855e7..295efa78973866b35be36002e30832ed35cfc317 100644 GIT binary patch literal 43688 zcmeFYRal(c5;h2d;L<>FcXti$?(PJF1a}V*+&#DkcXxMpcPF^JPe=AXXU;!&b2Asi z^YzoKzvZ=RmAq91%gc(v!(hRHfPlbDhzl!%fPm$IfPgAMK>*);_WGm?0s>QIDkLN? zAtXc~Z)a_6YGDKdA|4!<0I95WfZV^gb{z#E00pJ|!7jiFn&XETLVznwN&+ef%?MCF z9@>H_dyj-v@uQdsScs}M=nOIFZX*Z{YNA84P+xnGpzplydX~oias9zFY2+^B;k40y z4E)>JmQ=1r0Stc~i5wc{es_eNxMA$$H#iak9|ZNQ_l=5H!on1v*y`R^*VaJ|UD{1~ zN^Z{D-_(>)L+svzh=cc0c4-T+^#xY45=0GMg4l2|woj&VA28^3B$9T+^xSVUu^x?OHbnz&1=ismF=_j@K{XbE<-OGtq$6U;1hM7 zfz^>zzmTSb8DcV8cf9Wg6pG+TZ6u&V%+V1;^}dXycuk=)sw{JS3-(^ zd~Fzafd$D1WLByWBBKJPz~~~xLjFVsMSxo+ z0Ac?Ll*XTk!n?KtluDoa_#00JK7t?$j_$xDIy=L4cp%1QJ=6 z|2{EU5UN3>4M463A6kSc8k_`ELWm>?N8=lfm}Hb<4)S8e3{g9QSoHLONDf$Ipab#C z0QL@Zh>$4-?5v2)i8fE5?-L-{ zNOHiZf{tPJcRp-&*#G5g_nev#5g6I z1P)0(NT{Q425)yj?1(f3bw%mO&qy-})(P&S11YFJpoCzPrV{0hWS9In&Z&_vQ*fgc zQdTL#ob|<$8O@~2Rh2&~+$%sVw2u4!9WG8gE{`%eE}lY#0$08%bEfnzrXPTAdT^{+=)qe3o?KSgt|8?Y$v=O#ZdX>CurCpp|_9gtT8XQcw zU#HQC;Rl8x%s`BJ40;s`sywPTs;_Y(R4Pb(dHv9Y#lTlb(Sw)h2li`!=S^3N> zYoBYGYUfN@O?jsJ%(cwqrqi=qMV?Y`hl5LkPf^~Zgp%W>y68d9R?Y;^B$ZbS?S+hG zmZe)|mMJ^Oo?@;hxM8`ixna5)xshK#-XuMyKN`F+y~I9_FCJRZafRx1@($xMOfW(* zMPXa8e__X9Vqp$r)HYuDbUzSe*25^tO2ur(ip4O;j=-$@N!#S>x}QN8qXhfnmnq}R z&Y2OGF$aIs*aK!}8Rm2iZ;iA?A^WDrga?Z&tSc=vVb}{8gRY`M$_AAI;((^WWo zP+T2aJ<_<=xZb-!x|q9^8{54lY@=#R*MO|;p^e^*XaBPE^EiIbY2rlA+5ArWuJv?0 zFHr_bX7OWgfA@s%S>aO_FaapP$9@Nh@ynacJ}&+>rQe(P{^q!9BK<}=poCm8x%P9y#e>IDiH!fUj6=qC}pEJX#qA~7@5!)lhunj6!X1Koq& z1Nz>!Ey;_cu*|ShOnjzpW@i)6@sMG&0WuSE6a2UW2_{k#`)j+OKOMwNb3~@f7Okq; zNARNj2b^|^8ky}-UQat&XV-v-fU%$Q-$nR_9TDmCYgM+-fd~~`R0?c zx4j4Zv!0GYy~07@PVltrBs5BdGE@^Co0d*{rJ2NLFg3H?BVVBmw z$zKY_-4p8hN11q|xav$}mq+g${smu%mxbOzh28X!b#K>}6loplE~%a>w5rbI;AwtH zMq9b8&T3nq>u7v!Pi;)C^PFqxvQm+zru4B^f=cX=-b=2W3{}Bjxgv*9wh>hd#73+8Zs$`gn_%7MYfl)7y@daMnE5 zU9KRmOK0v?%;xG7ld1O-gdM$89Jr4QZb9}1uN8xvhr}bqz2V~9Eq&g3*Lh_>{eDJd zcZz9gt!PC&n_V59eun5TiYUg{v-fe!I`6rQeNPT4Cn49$o#xb(@2Gm@%Y0#Cm6qmI z_o6+}2)hS1mSMVGm)5djySX4zA1X#~OUJ6?)VAW%doUDTEUOk-!&T3&Yu`Bcb@j$g zs{eZg3VVxHYTJSH)ido{>2Q7i+Gzud{hpPHqRG6@=BmkE)oE*6hL_iVXH1}9U~i-{ zUL4S=-O76VH5zX&AMM%nS+R%l+p)SA|J%*p zbjDpOKQ1z2mjQF(j2#*OOH$*FV1nILiS>% zc!?R`=qoKkR-B}j+8gZqek&n8N5oFsep9^TbH-40^z%A>d#Aw!olAFgP6;$(RgKgo zjAdj%sDNcC5YRwV5O81#6!^gcet`CFOaKTZ@EaNU5zc(~Pc2wZCfGk^P=#L&1(bv& zB!J&apY4o{tn9y7J3wT@Z~%vzGgVf1P?wSB{A_JWr*CL&U_|F)Y4fWK2)7F-uxM%I zpikgpX<=p0>B2+wy9Fn({OdJ65y9^!4(2>W>N4^KLe_Rh1Z;E+bPPniFa!hy+;)b> zoQlGtf4c*J@eqA+aIoQ|r+0RCrgLVdv$iv#XZ-l_BRvBXJrffxum!EXtCfSk3$2ws z@t;Bd8AsU2{ zBOL?%zheWta{qeEDR1gxWT7E!Y6&C{a1LH(W_Ir1?f;LO|IGMbJ=Ooy^CL6UfA{>a zn*Z&oVsB(8WNitY(t-CsC-b-Se^>tP$W8xi=l?~CKV<&>7D#7a7;gH1&x{x56B|7~ z2naujgs_0J3+QngqzlfxW8L^^)}0^=8UUnojf+w-XMrTx$eCG_nDXnH~9S7NZu^OPenyVA?%jb@~7PY(Q1QGUPp%( z32AeyL|H(R7!;Y{e;)5w5_t^`(y_d}yjPl7qi7K5sNWAbf5)z!goTAP2xXEVxcIkU zPnq1ii7frsBD_;TBQvjPe4nVKt{>OKU;vqGd6TYgVM%nKs@KPwb^KGaIIjHp;TY(f zn1neCr*s;p!KWwU-8kInsfCjFPW?5Kwbder`8AUEc~AUmKg4&e0)?$-9S=`VCk!=_ z3A~TKeVab`@KsamrfvSXEmCt?Q&SThKe66&Eg2mhoxIv)bn02taCFHreY-UmzSdF= zlhwSr`2k-O8*T>)4b6BAaxD0z<=Sr^>~58MkdpH9lDd4LH}pNv;Lu*>=H_NhQW7C6 zYptTDrrF^sGo$mLEl2U=1NIf8@)}bweZxpF7s-T6FZ;y;V0o#xvv!U|j1*UnJ?|@T zpB`mHt)tKQv>;i7xixq37vJ^AK;k#ob1%%*##k;!#RSRf&kkN2j=bg1h+`%pvW=iQZutII?4 z@)S$w70(nnOm21AbWU39)|a|*vi^h2cb`QgnV#mY7cgdgSeS{0s;UE%>hL)o7EDs! ztybFg+F_=r(Kb7sMa!hHKk-;x7bwIeCT6D6iwk&bk!Yr#K&mOL4!yoWvaGM(j)Ky6 zb0sGxQn-jn4`srZB3K1)0;$xa=Q95Mj`Wp(>Fkd4sqvJ_tzl<0<3kXv-GcVeI;pbW zW%|Wp+UwvUAtBOWTeH={NQa-g1K^q0eXFa$4Ip%zudf}dOAExiMFo49n5K+=hG8;! z9M0&PB60gwR8(}lW+yUurc!z}gcuBWELBZWtL2Ea=0IOu*mbXbj9GQkEqU?w?7n*G z)3P2sWHsNtKA85#LzCyMd5BoHVs#W+1@83y+mWcK@W2#l=zBqCz*Qen)OKRPUp7Am z|LP`w78H59gISCJg?6v_?t`gbK|w8>>r2~bO%L+%f_lFPOc|dVdW*=*y5;IpCsmX2 z0+W2pACyu#R!+sj)-e36%Msf|zxD}E?QOp|jtT1Bj0L`*yJg_b;V%$|fR{?@=y-!% zrk_*6Y%(sflqtaEeoF&CKEk5kc7AHP&>+Du)x@5{Vw)9)#i|(iSuQSlN%Lc|tHnyq zU=X@r{#dhq85}O>3{?5naKi5X6$bpD7uHH?XFKJ{L^sb7EF*Ap4u;}zu+oMDPdfXh6IScY_Fm7=vK2ovpO~2)wVTgc8@@sj z=T+Cds^X5P+0Ww(BBo1JfYuLFVGx#eVm;_8c*keGh*TozH@T1FNz8q=8S6k?p(z8T zE-fk%v6g>)jM=O$aal6wg4*AI?wb%s%3v2Y|?Q7gv zRwnyY)JC;0w^Bk6NRwNPUpIBb zAG+sbVuzi*9e8RLs{kE@@7x;94Rn2wCqLVC7N_QY4I}s>0oAsK3AF&`al4^2vv?+M8#L6u>AKy9+E}v}| zl>BvNDx*g-63_XoKv>t>+XmP+W^EU@SR@{4$_%l|A}oEIVb8n};)*p}=`3jM1mDF) zKVBRLs>YzipdDr2LM;9vVx|36l~G@F3ZH>6mq5r1KoX!EM^%#Hdi!xZ{J4mB6)ESo z&b4@W`K%~q>lorozQ4d!DsmkLNsKF)M4WL_|&zfd~H2}lk^(ie)VUt_`+2tw) z%zSvmwBH)jpr))%SHwdh!3dw%PS|)f+4RuQJ|nf{oesPpo~}ODZ_}gWMx-O3;6#== zdBpwf6lc&K7-JOe%TF)~UXr6S434`b|T<7{FFst~(`wi5c;17wT#uidB z#B7Qv2{q|s!I&@7veQ?ijk32WLq*r7>^4@3I*H)jcO1LwG}v@nbhAI=3NEMz@CHNe z);@kQ;1d4O<-DolPoN@}ol_S~e4H`&L+^`X%<^=IN85;JOZ_9kchu}P2!q>}vRpdA zG{y%QdMJP(T*R<)X4~_jdHPnQc|-99-67h!&mP8Rg*TR_kGzU{a6||!A$pzx(&lKa zBzdwupK~1oPdP;76&d^+EtQ#1lg2@fNS+~mSr>A*4w|s^X{k{?zAN}kCPxI+y zebbU?>=#WJdMR3pBe}1m>Xu2dl{tuJW|7wc*0(Lsv!vWD)j>3LZ5D@N_RO+EsgvWZ zg|sxYMeg>?-?`P>1w^QO_Jd=|S%sa;X>?i!a6W%;a^zQAXZsOeSE^er+PVf!)`Lzs z2WYWZPBo~}YyM$P-?E0G&(2QXAEXgB@-@^g@2Y0p`u$jYz$YzV>{6X}-Jxsl)y2+G zvt83l3TkThl&>#i6}b3*OIyC{G@f3796)Bpw8=OIVo`h5q%)e2L(Oeq?-@SCIOT5& zif=put)VD`bOq@X0STUcy003--E?D`~3M*ggA+jMOgEEg!WHyc=!MneHMb#t(|XrPz{atKNH((fr9J`=No7=C)Q)W zAVa^|0?p&P!RsVNX}QWCkNce|ubNQkhNIY0#9VYQQ0+HAr)_cz_-PU&`ocqet%7^Y z{(Md&UHmzzyy`+rs9kfoIn2BCnEpe__JhoCZwBxTUKXLU{-dm1d2ZkWggb+r1%B$W zTFps#=z=KYao+wOTE@5LmBuU-VOnp4K8_21YU-DLO)vwsfd7*aP2VN6{~MoiBh1kgehHhw7_#W8+Sk&{&4@H(d0dxIjOs{uk4Om3#wVJwXXe%| zrrhP{U4mZ;aYYkXn=DiEkK@@Y-y`PU98O#!Xx>!Ml|B==+1uO8tatbZBA7y5Iw_^U zATB|BhXahRy?Zw}<}TI_Kl7mt@87{h2Qa7wDeP3+&(jIKi6+J+ls6qKGMWUp5L#xf znNdgdx0C~%P*G7iKTUvkQq(aNmm24uAhy&bKB0g?3v6WL7gn-GX7K2RCV8a9nN*Lx zZQV2aOc$UHu6PH!y)Os_KFXG~H-8qNa9LbC3P6L>B5` z^x7mkdP|h8m7TUJEBi_0|LH|jA?d@BSAe&==2xi%`)I)m&lh^EJe30_@_On#5xp!Y zR&+{m^R6?Tt&%U!7;3-HL~0VnMY4Zwl+e;luc-UVpoo>SiiinHJWVw_ zhE$kQbQP1=9BFrO4)ZXs+F|#fBbt7aga5&u%YG5^hv?A$OI3uCjeVD*)LnulVGD=$%IeY zN93Hf3= z3kG?Az|XmCLH(Ob|6m2b+#DoOm?;8I%o3RYt$`zX_aO)Dz@obJe`f*+gHwq6?i*H6 z{cjCJ6qI}vLQeH)UCe*Y@oPK$0Hh7>903O=^*C>#%)F|v8!Yj(r2jWe10Oi5$G1&m zz-g{HOVwEl(+8b07* z(v9QF0lY;N0F3X+RI&+5hlhu`Sy@CMl356rJFYkg z+Cw)2jkUEZl2TH;*Gbh=Gc!9^7TU)!04MNw; zNVyv`$C6vM`I&*{vsUZNy1GSJs#ZIZhzt%wbb7P}_9tm;QnQ(-4s7c-qtd#kTUFVL zN^7-Eyt3<0z2~f@vWfikOUF zn0a_qh74Q+t0U@Vtj>j^>e)VubhhZv;V{f;FytKSgmoe$B&5sB;&Ru+s`I`F<3=ES z+tuAY^4gU}^l-&IE@@;GN3N3zONScbVHC;t7__tdQ(Q%*mgB=->SDFw;rGGIt>N-S z%h#csIZv;C8!sD_L~hdM5JX zxw)G4(|N=F;DnK^|?YS zF`vqo)xSyzKt6HgGgBzIPaP_(x|1X6k|u?tazj)qYdRanZQAFSBkt=aazsp`%}xs| z6{iuaeQ9Up|NJW6P*Zao&H%r|O;$(_?K5NLcqp(`_adax>Xhc^??0HT!E7|RCBkHN ztiIS}+00Y8zPq(Gd|9pcJY?NuE&;-r1jL5cT#hUsZ2Dzmd#_EEcIQxsjn-BgFUOkC z9!BT1o?h^Jz~|qCLgjahMZo9=@n>#n=jY@M0NrNtw+p?uX?n|`kdW^=J;F+8SqQvl z<`x#;fxwy>xUgSvR#2G3{8^5>=KuLz9t$Pa7NoWvi-M3{bI!o?R^JWjPQ-r)4y2Bb2_0Ey#%E zLN=V-v3pqUb$i%QTvr$y8v3pkPa9h@0#{gA7zY-JFz9~fMS#e6{|(6j!I9SUhT5xG zw>M$e5E2og%i->LnVfyKM&7`{fP|M2UM{E|~2L_-XQs_MeM7Fu?Ub|fFkAHLrzSq@;+KkPY9V~KQXW#53U~nF@ zNGf&h?d_NHce9$aULgU3!WS?mKrjgi7kWlwh!FWv<`?Y;g@@nZY!_R#EuDqVTh^D! z+eVYfy*<#AS>xarTHbdn$)(&to;HgCJU6Ql)}`JUx@ z)m!^$)>|wJ-4^q|G`NL6pNqYj*1e?Z;=6pTe(M00kd~eRTzn13gJMsgOVD#w0SHV? z;(gul#N;u{F}d=%TcfV6t-Z{6Th)bIcRpKXG9JM|A&ZQ6B~?q#mntWDSW*5+=sG(-8*O`zH92K$be zn#Kv!r^ZbmDY5iH~A0A|=fG&_cZVbmN>% z5>v*}^dr`8pCO(q!oLYy>Md~Bsb?3KblL?xIGN|HFy7`o-z#UQmK?1jxe`L1weML? zTc6I(uYY$qnf~at29GtSNqRJA4RC=x{}dj0;Ujn_!4i!Bm@JVFp?0R7G^SjEAvi@5{s1=Zom_KR&f%^J%(0!>V?5$)8B(m7nGf@Zmm zU>GBuzTL=ri09qVddI{l5{pmHbMW#CV9iV?Bqtld^ja@5U7z$gU1!wMkdiKHlUK89 zg3JdRxwfNqpuk*=c0CTy`10EV{pxd$09j6FkSuFqGife z6-WS|V~KXBF~qafhfBn%g$Xz1rp4wqL(#Zb#3Sh4{m8?6`>zo z5K=a#KyXui*~|X8^$X!&WU)HNh$QeA?>##Rxn0ctH#ips0(Gq(Mb^(UpoU)(70qcC zlC8;uIi`x5O(mS7Y)Is8;@8$YZ*-H4C9w1pwe^tFF;tq`DVAuchX^OFHh0ds9mbp5M}9~SctpvrOP>OZ36gX!jSBZOI~pPdm9Zo zhN#-W%sso?71!;_8o$lHQ7vipa+OK^0&?gd(HD;u+Upaa{IDU^1P2VomYY19FDwkW zdQ8X8El_i^)@H=7&V#J|;M+6PiS**jbm=ezeQ2M41rHuu3~+x0oUd$k&a16Doh>dG zzGTm2?}pAx9YQyYw2HeF_}9a&l|j&&j3iDTx>7Q{!#ufBBZG+bc1HJc$b!*!iABKS z_`>SD-YVSjgI_Ga0~Di{dQB&?)&TyDn=4?pq$QQ)A+`;`Pd zc9QRVTLX7L*CRx~xn*f=sYFSJ!c3u|W@f!p5_$p8Ft(VFBozG{|>I^ z`*YIWhb3F4fyd>oHJv#Ul9}3Yta`HU)0t4n==nWQ&SoIB+DaCpKYh~o^9oHgTwL1v zBO#&|9+xO{c>$R2?o{?_IeYJ~8yDV$+DQ6s7~j7P?MU=xpZ^Hhuu{BOo|KeYU4xi` z@9&f?xa_>+6A>Al!2}U5Jcnfze1sK%WX1rDViG*5u zp4oAEbvfc|Rvmyc<(2^AiT!pzYF#t@L3bS2@+Exv1j^<;z17^2^@o52wrQ3SkRLhc zI~wg0)WyiS%24#QG(r@n`uabBcl!d{?ag=A_@k7N9a1X@Qe0BBscxD2OY_ClJKwX8 zl3^<5Zd!+tKD_6`%2`HTo+c-OkfXs%A}x_jo!1e3_Z8}<>AKJTaxS{xMkgy?!>%2a z0rtu zwuE(GxLo*MK`*!VyU&AF$9nzsOTU3beDKDDRh%%hvWi}Bcau$brsp#HOqHH!oy$Fl5bdjvPJXzY~yj|0QUSs-2b;Dv%(>VF2SJ$t3?j^%k zwFe0#))_$=wWA4%Hyhm^q0rk;tj{vRQ3OP;5V2E^de@_5)m6xZMtR)s|6} zOx-bOtpY83@t;oX;OCQ!MlGl1I(;oL^Rg;ac-{0H_o+$cMKR*9LN`5~WiQsM`QJVWj=7&34UJVpp+U&BPkK#Er zm}PjAZnRygO72x@<+(=uuZz-@mHaX-ZIGSY3+&MKi# zbVEP*#Ig6)rnWv|c$FN)`|bt*b>JHFdBud+l3V2-*S&hbpxA@UHXPh{u@|LmwX2SI zt$_?1JLUSl`grRhR8l`ITH-3RSuOsZFBoJqzU*nFhhF_?dONDOHydez$qduA*PYSr zc5}C9IRc29f@=z;NzUsWft$WqtryQ9jQlqML-VecO1xB>{)6fexn~?9Y+ghRJ`E?0 z5aX#tYrqA>7N$RjWytsB3W~Hk*Lyd4QT>%kSc+FU!Cg(EGg1lPx!Zzc#== z6M6DIWzG&Z;73Zl0Wr^I#lDHGwc}z@AxEtZH(1^0XM;l=URBfkrMnbfuKX+)#8zGxUx)>sxkr4X&5x zPmN-ZtCMnRlCBOaz<9IMiIp>r+G7BSXmlS%iHo=Qq|ya4WIOvcbO1UmF}K%)lpc$} z7QL)nHW{R22q~O{Zvr<)4b(K2$DUq0y8IlJMBpC&<|+!gEW?XFAp~OvmBny;Dd_HJ zIlpMn_VdQ^CHADbLRGr?#xf*P|N+?>MLFp=fkzD4^24v5cQ4~7Xxr&!p`s|IRoso#1X#5yY(FxOh0=$|9 zE0{0WeoZj&%X-J$YPQ#<$m7whcY^^y8tA zL^delYrFd<)K@{?veoym;(Y8?c%46=BUS`8h9;78)AFnnk@KYAhs6#H2w#U5?exd!4GkNm67?9@T5fz< z>!O?B`;flv?3kQm;;=SyYn(stDK~GPHhkOLre#^(*{E4vECn;7Jlw4^fBcY*&$zIi zENbYl^Yw{S>~`Cu^6tAy@%rTFeur##AcF4bEqYN_7{(BX7=t1}x>d9Z-lC>r5(Q zqEUN6trK#k%sI6ya&I*eR`ByUsMUC7vqH@}DqY~I*5p-bYa)RH{Nf9yJz`EI8dVAi;dgsUMQ<>bs8V-2Cy z&k<;vxMz=Wm zjEi3QBjCofh3oZlU!83J7dHT7f6ve8?JO?~LbN%YGh-;r|2Wn7tlqk)IY8);$@S21 z5SLrAPxm>dXGSk=WX)_Jtvc$Pz^B|aun#;MqBus#T}KOvW8JE#HA89~95w)rLri9~oPdl*bDRAE zMrJD0{zr#6vHluu@?RkudD3r4LH~6E8BA1R>!HrjYQU(SwaCA+p#TYkg znAT6Rnt{VTePS&qmrd}*;t@E}Q^~@W><(MLZnH;W)hyj?IIkmOE-L<}P9RkNNX&I;af_&~*q8jZ83QHjyx% z19W0v6diI(l;T_4A%@xvTw0SSjgP)1l4zhXBr2GsT=JKYz}>Fuu~Zw`?fHJSux-?~ zyS|5x6#JlrEi7v0rNZiWU{KRP&3W>kJD*%GBN~YQ;FeC4>fi-8M{;jaZm4IoIy-EyTy>QQS*ZO3Q~v z`;$xSv|Csn=T{#IFmDcv0l+p=Z@|I=MqzRBoivJfl#a9rNVKM|!od~CwYLw(=maGk z!KYXhl-+^K?X<&5mBln8wfqT&Hc~Pzfjrn-M$0VbZP2;KR zH`NRL^dejvQP9&{B-(Q>Kn*SRMV8V6xIUU2_;lON9I*tW{YU%XSo(3gsgc2fhYJMk;MKtfQodZkw4O)nu6a zej?FUmvyRB{!&2K<)q&h@yqDeV9t}!P5}vzh+vCt)#``GP0j6^TjiTm!^;Ete`GA1 z5xoW|l(Oq0TGM;>&iw{pqJ~wJmiC-VK}kuw<_GRvmEKcVT>?Sk?NmjrIfJ$oY%Nl% zPD+&o$96o$pB%(zVqcjvImz}3qyUjl=4NJdleX-MG7?K~_FeatTT|X4$t>W`<85G^ z0HxY{E(9VrdXt8lFJc-l>T;>vqc+!fF>%JEXg z*)O39-ZFlrza+&!4@MC9qi__0WfE;QcKeDscYVN{`WRRJt)}My1t;f_i*$2}si9#G zQu^&Sfjrl(Ozqho6SM;fzra#7)NhISC-ez)F^Rl+p%?vKR*-K$Z!cQRDma(-+Td_! z59HVPQXKl8ubMMj$F?ek>B36j{UbB~igJbt=xdiHbUhIIhB_GD-?=RRuJ2FbTr^XT zJ7*1Mi|s!v|F=m6N+v<=^v3Vx-~St{@V6sn5}dHl&;!lRY(h8(gs+XByBme zID6E&Tl|fw8d@YWj z5c7_t{r4Klh8mJwN%K_CNVtRnE*z$+!zzxujM!Lu+}OUNz6r1{v_GigH6^H;lk92z zQw_MW>^qW)VE)fStxeb%jO);FzpnPz9dN4#Ldp2`c!Ka!I#Z;Coj~@%%YB$abLa$O z$I7X!^eNS^nq4`S9|d7>2kjU={2I`tW1j~Hr(8~}Sd{m9BK}!EC?Jyn9t1fyir*T@ z5+{~fBfxCS%zCJ=m(_(aNw}8ark{&LWM3Mt-!IJ+XeJe8w9Y~+E`BJ?nlTcl>?thZ zC{?p{V>Ad-mP?_1P=b^=>13YLUZ2sLHQ@boXoQl#(p9qN0G8M(BJTvGfN28Snz&f# zcSwsKkzPe|Ql77c>$j$_BdNf7t)V#55L^%AkdRS|@#JZ*;rJRv$J!g(tf_{z{}`ZxJ|LeVgtl0!kpAHv z`*(|t(OY4`@k?_yg_glzo@!|8o90Ejc0!Zh+sPf0suUvJL~yE<9BzYy7eCB7qL${~ zR94yF>%=_9(MzgZIE+*G??U`)7!bDT(>RT|2Y*sP*(c-_`m zELVsE|C1v}SL_|$N4FKnbuzYJ#+uDLMqh(4wV74tnM3v>{YJC&*PNy38O^@i;NaOy zn_E}cN0+uWt|fP|)pR9i3hUZ{^Yrv9M@%}tn8O!H8;XChh3j0h-^K6zgS!TcO`f#~ z(oMUX zne`Q$(eRh5rJStSOV$7geX9s?YT^XK(0=6o^*4mASXIliVMQRd>gs;m9~VE(W8IF&OQQYz_C*x+H?$m5DEsL zYm>0B=wLcxJr+G>vitM7fN4Q_0_GK3kkTGe4)8kr(RsUx`LWdRv(PP(e=zp{X*MCe zTy+!9puKFTwG>B8C#j)P;wGr&K$U0=zuAkE8~!+X@QFZPBtLNSEWj+EL(Nkq`R&p2 zgV3Mk0*nrz5QID*G?|v@!1j**+$SY0ob$E8G>zA$i6iiW5R_Nr7C?YV?k!Kkz)i2mle<}Edh>**py{%iTH*Wm+Q2aBhrJAZo)&9cyiT5C>~=E%~JS#oz093XRNtO zvHu$RU}^m7y9M}g4iFZ6(AQj`^AY2B?n&Ng3sXx(Tx`}+%;XWZT!vHE8cb#@Jxo-nSRxjEzB`v@#LLg-(I?kWUM($b5ccYxU!)$4fyIqaU={yJ?4XL~)={sQBGqs8<8Adzh_f-_@xS{aky%)&z*eSVB#QH{tnVk07atoM9$Pa~5 zz@caLlkkLsjmJk>B)5Ljk#TRo%<=*l&h+D~eWTd2vjvRYrX_0$?xWNCIkg z{(DXf0Vm{$!UJ(rHnt4}qRS&5QTbD&JT2OU6#w2SBsF5UqU7nhDt ziC@nnK4_x_CI~Qj4UWddB#UwaSp;~^M;D0-TeRu*DGyKl&ROtLvn}--;~5;wn?=le z;}|~Ha4BkzerZbXk}9yWF!1fMe64hsodWHNmHCsPb}k3BX84Hn2)sds2Kl#EOYh9yd-6=4zaK^-S4Z<%n6>RSRw@*yOdbY1fAK|APyBySH&{KpW3K=6Ki z5zEOd(8e(}Ekbkz&fFF#17%3Ysm4$(%j`o@QFigA*L>2; zov{h<9@W3r2^G+E)9oD`0j60KoIfkIxAIaAX#zR@D+zcqn^jET@@d@M{sNoAAznv; zNVfxQ1V_#ia(01^RzVqaiHc&mMZ%8rtBoV^?et%pFB2B1{B$}t>HZo~7*XJdX1ND9 z^SPUA{X9M&JVfbnQ48Crrr5qA<$2qRs|y#Cpq_o7ftL%ZE_37GGRXeB!?bVI_KzlQ z9w&j#k=ne}B5f}HfR8HbQ}vp8|AO?wc1rYPj7#`JU*04yR`*_zD5Hz z={&-CpHpA;`M0ezaRi=fA}cwX(>Mi4AacCXw>wBw!YZo4xC!FCadeWFs)XaLl?-UR z0t@D8)PbZkdwcULR|~@YBO?4rymc?4cpa1iqz5Bo6;ZR-U#63sG6QnOBqUp!F@CSN z%Fd~1QCVl6CVErW^@~CicO8pR+w=5iMTguUpD*!Bo0TCqTFcmlbgHL`TTlKq{4p57 zacD=8UyiMFH!9!AS$k!Wb=3$jjP^)37sw=*+TA*AJ_-FH;A1?0&VM7tj^=Zu98!6@*8f#?cSDMjYH)G;>nHaQ@j3aSBSQWO;8&MLD zA5JgWca}~&XIm>_{|Q5UD&Rh3yd&XSbE!J`uq^rC8VWv=2?L?KiA_YfFI-!ANVXp9 z1@=N>7P8Q5KGU;j;X0X7Tm~2vjI`(9p4GUBZHxR+l$Ej3=bPPAPxVuTl7-h1(r%X)oD<83baf!Bo!QWwoersxI?1ly>m59TWh$RC01Nv9!>a>_D3m*_o!Cp`9d{r{bx*ldPr)MVVgPF&G znjfE<=`vC%o{M~O$l46jtF!x_4tKMOP>LbNM^&}6-{|)5x5x?? zjO5Kbr=pOMF^=N}H7qz2N2ZlVi|>!MVTVT8m&`M3ca-|F2(^bA1P<(X!j;!}>LgRQ zHY(YBJ~JNimt<;YP!8GZo#pO)8zy)wuQvp@!D24u>wYJIZ2mkgrS^_XJVV1xJV8b_ zAtZ(Bc1?wGFsN%X;{jVrg2j*E@xIn1e4;PnBF`qdJVR|kdjC**mbd!3`-dx+eS>^-rFNm1K^d|8C#HU%TK16U+Jl41r3`)9R2B-%| z@X>kRFfThJp+VjkUWalgZktFdoB5C0^$7<`=$ZOG?6u2dz=uUU>EW^s)o5aMhf9|e zF^3%Dgd9+MmPoE^t$eAGDA*U2tU7&5cAdV@i^&JWXl7w`86=n2UerBZ(^mGT_Qvao z(AbhCL&=Tr=Ll;5xl*1%-1_s4dk3Da^I-Jet0q|4)$H67$J$uAm6k1Zp)4s zu(zj@J3K}jpQcDQ@LkQv$;Ys@-Ku-(Wk3(Hx%d?tlSj^y6pc$ow779_j+I~cSNQ~_ zk0w#IU$^cHH-4P`x%Ee#!rxJJv$0UfQ}RjB(^Wa-ZLsvu$Dhw(EH9q~Ute9>(_ftj zd|HiMi=okJF%SB>(!w^w@qSxYDlQh^hmX8dqCeTv*_C9?1x+PBJLD|q5rG>U5(Zn^Rqnf7S^n>+1Gr?wQ0AiXyvsZ#GNPgVo`m0N zkhb|trcN{%qq85gB6vvL^@ySqmnKPM#(5FzOVfMS5Ai*Z^NhbXsgk8V4v!q(?0;fW zBtF_CsOY|RIPaJc(05gUW%My5HQf5;ci!vRbA!PbZB zlz-ntH3N1s!iQlseE+^N=Kd0M^wZX8RoV&@&IB(z)Bj@og_DBZjk270jBqSHUBX3<`q% z((cx3ZSP>Aa)0+7e6Y{IptRET7uQrQ%MGf#B{A(+>|LP(iRyd3`lT_ipujoBX8Wg_ z%T@EvkFy^m#u_B19ruiD94Lv;=if7j14425TprH_JS1~+a<&pdcj0M3Xxi8e*UxufxxG#6?BFzUH+TLnh*-umvaV?!FP1kU+d^ zsr99pAcXv~Gn&wP+@cV|pcV6cGyaUcI}tJe3c2P8M;RtOJX~H=lg_&Bw&698=Q4Qt z@VKMx3bGl+v9+7`KNizT?NfD=t8?8P%L7=_-B&^BHNz#N3KTwP5CsLr;O#k1UQv;l z-Ip(h&W;WI_N%0zr?KPFYM;DQ%uO3->?L~V#Www0dKT+dfXX#RW-^|E!5g=|y*((6 zfD<&m7dOA=T&soM)xzR>@fnYuMF-&ghLP}jY==Dnr7uxHMZ0b}%Yg{w{D;m-M-R8= zG>`3hpfrt|*X1m3W^Pvesw(?eNow;>M(frqfrn^5@y3MvGw{KjN0mmsvHpMdgVs>H zq?e)rOJXFwuv^<(xXj??WjfHqr8U&5xPRMA&X(qVgf*hm+>HBctaNl6qnH!%A_Z3^wj`FX#C+5wY9lwVi>@&wvs&2_=X4P6NaPaRMnAps-X( zBCGUZVCnL7I`g@*kg{@5vJkX?Io+niLp(!S|Ke9a>mVe3w(h_N$@dbpyz94iQ{qV2KL zrd#tCf6!w>Az`jLE9+DLLHjR^0GXJdjB)=FYEEgT8dU0rbmi2CfB#p6j4A=8xyaSL zhMW&mO;2INhx*x~2*>t92gk~pf)xJr#XG%jXK@8)aLMzk$Fav^yGf)FwirHzCQzYw zYDVU9Rizh`m}t&VF1nR`$m4jV=;PG)1^>jG%izwQ+}G=TY0a&nMD>|4?4A2DAs=ZY3Y?oVE{d-jM9 zeIugslUIO#Ssa&;Ad$@<^q$GDR9K_l>JbK`Whs_poite#vPgWMT8%%rw31W7A^4MVevW+M%Bg3J0+9mXY&1QqL76*!$5z$zk5%`b(t{j(3HshQp7)=zsq35UygvKcs31DJGyH zco92FGCDM@w5j$?O}ANPr-fg1y$pW!O>k-D0{6qx=Xqz>fk}yF~yIfIv$4( z;?XF;>1ZW5Bo_Cv1>dFNb<+dKyePZ`(J2siUN16tmeO zUy|dL*b!uIWi^RofBm#|HflzcD5acXrbar z2`JRCRxzpgm5=DW1kxH8t^$5#0&oB)G8CDl&f`9iMUF3zt0xel7X#*9%i&yne^1bp zmQP-Fh+mY*V3|>)IX@s=Ggq>6AA=kM89M)JG~%lqogQi|st1!Z_36at1a>wQUpC=@ zEK0G{&Y%f?)#9(tmdXnw0I})xVZruIYSM%pYQ!e(MM9)=4 zLHIJ{3w>_%K|3S(XD3I}Y~yVE+-+s$!E}bdC-DtvwkxdYBVySToVBY(Xg|1 z%>Ow*pbAh2$d06gqG?Ng+K<4|VA8oxS*$flew-f$zaISyjk6&HQbO$!GT5aOkn<9{ zKqW8BmwtOJoBaJfH{l-pYqOg@s$1~dbGpldq=ZaN+ZHCs)QnwfwgzB_r10y_-ANEO z4W^Rt0n#35Nw_{vr#Y!o0`jj}4er=n)}QXfiXK-%9&4;q)^_qS@%eCvJrcqE@2+Ob zqW#e-?ni6KlYgRm5)|l-CMx;M%vb3rwLbtA!$pB?{r*3}2kYi{@TG`uL3JSGrY!KM<40tg23vEWB%kR88*0T{% z+MNdpi%FfC{6U*~r@PhP3v{UW13S`S+l2{jAEI+VQody=(dt;ysy_RebTl@SU`AZ; zij_T$7)$(oVZsgF$2>ibrM*PWe+#F;4@2AKcX0slu0m)Tolrd4EcX@34X=scO;q43 z<9W<}bquf#fO3s~@g6*B&Bn^w^WhVT1*WkGl`6}Cnj$Sn0)#YlGH z(u(Ph=ehM)K>(RVo(G+oLog`@E^> zOdp9D6Rh-wR$oyvQ`~c+Ne26dJFW$+alnk;3kpO*Nb}5gHzbtCoB3JKSdgozHRw<| zncFDD<+DGhK|JzLC?RCGtIF}zPwx>8{R4+-jf}YAcg!*NeBcgt$IO-<)L z;km6=sKrXd7$P@^{Vpo&F4_lmb#PV$=ZjeCr<>L%W*a=wi*!J6XiqZI4Td+>+I;?H zYj*n6QM|J2vH-(=yFu95`&LwTUk(Frj^!TrS;E8PeQgX_^HSu$s!4o4k*p_yY~=2D zT0ecpS^`+aGFhU~rlmcX(KF~oom+W^9=NR}Mn|N^aR?{AuC<0p zq+dzu?m}E8r(-Q~$5_dDyPY}Oe)v72P9!>bQ(Avz(&3%BD-np;JK%6G#c7Ndl!eos zAVGBhn#k7CJ^H96;G;?GGl${9g4V8wB!RhW+cA}Gg*%A&lHhh^dfo$-@q+(8SvGl3 zzoT>o3EcrdxxXLgJ48bwf!NC9iuprzL*l?+IoT#{xKB~ql_n9F9TjlyalRKVFayK? z5gd`q$|ZRWD(%_)h0CnBP-^t0p<0rsZqysPQXx#lz$=2@>>QOLD1#x0{(~p}*;0G5iCh;kLgc_@CP0k| zNg{rKAHVDFrrKZ&a42@WgOf4=(9>V5b3cl!swVy9v6)by51h^(xtaLg;NKT_DZgNg z8(RZPc1kr4>lFr(M26&+2>fKm@BLv3n=$Ck?#+3X2VP^h1O5FH&Ns8$^K-L9DGq77 zcPTz+e<#WR1{~0LseDR9%2HpE-Bx7_Z}k>W1Ne*ONksoZpSZreFIrx8H8YE8Hir-x z;(z)*udVI9OgjH}CxNp}R`ahjoNU8$hdjSnh*U-M*xeW1eUQmKZUt-{>_b=SJb>Gm zpfX!k6|mn*3R#0iT69Q`S}7?p!zc>%%#$gU6yPi61amo@QE<6mBmp$usJ67%$z?fs z+J>;U|B?3jT^a;j2%9R*O--sU*M61-ku>Pk?WUMp9k z!OGzB#-nE6d~L)YZYnEV{`sXCw!WTo_Xo(CkJ^$fR8Ir8Ze zJaBLsWCZRcotT=+x$ZaPs+=TOEr|B!glzlhoZD?l(&+~4^$IWy59#$3zPuk;@dZ|> zX{f%dbV7alz(V>lj0|Z?)Et)<2D7HDq2Zf4CSA|H8K=%Y2KuIS%`NBXDaFSQSdhr# z*w`2_;h-q<^E2$V=r8J4ao6GWhJe6D!c#kAu24QA{T-#rc=|z!>JhmQKRJ_B-eL-W4d$Xg+J5$*d-R6`p0EY*5$v$liSuW}5T8j?(3&EyIBG$Mz{L zsY*R8QHeCPv@Aar&9N?6DKI4-qg5FWzwvKG)s{oVhw@wS=fue9IXSVp*IUv4T8PoG z|2RuUw~*15HG>J%^Qmjs4TOf)^w?_zz<+3TA(q1-n$4KI?=4mUgRbJx`pM5f!X}Zl zHH#)sie-V|K80Ozp03lNtYDsyL4qLg;cmz}-TWC@mcIc#_wuiv%lcUDwo4xD6W$+A zSnl_tFU7}u!imLgfr*BeF%ekp3Iukx#4*MMKLSCM(N++eUGV~8#?o|uytdHOC5zJe zfLB3qFkFr96H-CuJ{D46fQiF!p17q^VUh<|Ppxz{>dc8h6%|F&iMpGXtNJlp`pCt0 zeyCEJi9~#eah2ej+}DvsnlH@)a3rr$P_XKJ#9!=WWP@LxFTVntt&&4hvNKJe60tCi z^JqvzK8pMr=c4uG+MUr((OnGgw*qT3crW2C?`;Cnau=$MmI93xHNpx)QPnT7R%)F< z;%=9+13sRR8XrA18qJ^H`X(;dO~8%-L}y?A;)O<|d&guBmm0bbQy-cVu@Qdv89ssi z=5w4dUCp`^QMFdRT*X~MiG*YWw@=nmPJXVVNgT7hP#mAVW&!_$J6@#(^LM)PH!P`} zh4HJ9ttj`dm5M59ADT!Ik!ovFhV&;o=(u7~R1w6Lq&F&C4*{WvDSWPm z9W(QsgL}0e0(P`5(1--wiYe?V8xMV<1+9Xu!)}`E)$wGh3rKJpt+?+Fm{LFUluXt~7N%4DivV3Tz)^u$I-Nyin0EMH zF-l+YCm^;axq~CaX~e@m>e+HNCYt$k_F(RsHt-0$;d58GF~zCUvDZ2mn<5PMu=<&_ zscBj4cTlUAdMW|$VbA;hx&aE?pAt0A$2v4(OVMyE@t;~{crs|L=j{gAp60BRSeL?R zeSDCaQSJa|J?b6B*na1_)VBL%2V4Hw7U(~BYiKT(RS1BlZW2)gv7IkSP)il4=%>26 zv^^fjJS`8&2+QcSK9*}=uD$}6Tr`C%Ul-0@LDSgq!i$Adj7Rm%Bg}*>a_6POhPMFP zBl~!pd}N4LXR491--|~?372b~aOPL(w!arPG5Jj?YzhY3=TnPJdwO%kd`%;pmSs<9 zkvIkw?2(Ffx$8UyAwbHe(`iub-@Wbt_(9yf6xXLj=rM)y;bcDMz+T36Y}c6t4)HZ4 z4gx%d(~V7X4!QdAYVEvv&-C<^y;;7Cs9?_V#V_W2PXWC#)K7nZXCLYpjsxYi?1uE3 z8D4u>wVnrdNY56g35_c4CUb{gXLerz(1sjvryf8pbWC2}hb}gG=gtu^r^Hl_&r!*d zxY>Qj9#vpx2WG~V=o_99}hpebbx1ViSKeV`Mk81 zE43IK>9q6etn)4ItQPrpwW>P^wJrc&X(~4Twn!$HSe|tKVmG!)nDg<)BmUWDAJR4mqGFHVWcP z$c{9tfXL8>xMFN`iDH?Rh5ahUZYP!GYaUVfQ+f3-RELOJD^{MIv|X}SxK-{V1m9>C z?p)wE5FDu_ts)z1jfhRpUYZLKjU?$au(0H;0J!3Ur;A9y4!`~Gpd2f^w!M8E=0V#J z^Lguu`>$>(Z9^E;DR>A>-V`vA%avr!>Q}Oe^Xg= z9ZXIbNdRz6lf0b2CzKn@YyuU&soWcS-`1Wg zzz%@uN3%m8{Z!G*29GQ<4DGE&R+A(LsgD7#CaS7z7VC^c`8BaqO^fyG7;qO`3onlb zzvrql@|=|9NqK(CnG%)Ms$OzSf9Kj9N9?JMa-*?Uo-$vp#Gqa{H^tT4d|fJfG8>S^?Bv@CvP0n9R|&)X_IfRcQL|-oBZs0 z%c|*bty`PtzojutTgjp912HrXtIpl>V(r$}p2D`#+~5P6uS46yd)TVV`-wa0RFBm} zr^D6h>XgThZ*F2BauXj4)H=Wi_+8CZ=~JL;LXor(wHy<%Rvv$0Y@4aD6Mlt~(Ax{8 zR<8ZYnTQ$38i2q1OX2mLb6sV=)YSH2ZL@4vS67RRZv99JWaAA=f9F!7<*6#JEQdhb zIEH4yvG63g{C=@w)DDTrl2iUX`8Jm* zl*WUK!BS(a-P7oKlgStqyn0odfNbPjj+GtYw27#%m#g54VM=0tnjeA78|%!%h2kHk zn7Hnz67!1hYPc9U+!}g+e8bW4oyjIzY`XDxbg|3#g7pVGfTL3y&GCpf1dn*G+O*xy z>9O)yFP~VYPJEFwMggG=1nqUT29^8%W3y1fV(X&BC6$V(vQa4-0HoJ>*lyg;mrM&$ zS=Q(e!_W6PV7;K!O&`Diq;g%Ui<;LKsA`h&3uwX2dq4tKLZ}Z}>TA7~S|B-PI?!TU zdLwZhpg^E}TCmD;k@?{reuSSOVa_I6MnEWZz*Lr>E8;BEyP!H?&m%9dZbrC$+wgu? z-oaj-H_D}UCJysl*Cbygg;u2kSC*zNKS4=JSuIpPcf-oowmQRs?gqT3)LK?U&m7_| zleSM+R^6VfHgHw9N_c*F?BFasz3nc;VEK?t5e^bIV>pVwi;2xJBhke1=%Zr}Aw*L7 z2LGX$_N>I)VhYWTK2&OmHB{JI0mUqpt0xy4rE(Qf-+p*>F(!juH3XhPA|AOe099N4 z8MVr@C)+vwj%@e%u8*%E7myquxwqWJ(5s83mYEO=?@z*Ub_0`r_<7w0*)H1+v)7K5 zn?}BGa8sp6)eXsbp^N{5Oj)aKB@i_q^4w8}gU5Fra5h3V=&I1@u#iA6{^znu8}_w; zi#+n@HA??>GAZobpWSqf+tcH`)L3ud^WfiQ85kZ|RZ$tNf!&P6ap4RI_ z1`UCS%H$ft*Ao%gQv@7@QEnpKc0Ar<4>fCRqpQA#GxFol?+!6yc=z~&$C`yg<{pZ) zn2~<$^xPdg!>UlVzho_)SYG&|QAWiieXQTDlOu*@b8A(lV z4IpG=E;n_d4cUXxYHQFx9JH+YWwhF0;4(lxZ7W__UVKO9g5mV ziNuG5#KvJOn4}1?6o~Et-NZS|g*W;80d;{{`cWuBo%x~I5(V(Gyxx3g>o1LMi0gJ| z_E@)lXsD>vBg~zThNi(PpFU>1&dKgDv5z9v`Fg_cKR`b>>Bu@YJtj!jSQ6+6V7;1C zUHsq7b;`hCb+tAdA2>?_E`9%GnXbgaOQnd)L#=uQqxsLoiy{ctab0T3kUp;SgpK9` zOxNq&MJn?8F?guZ-Ah&;TGnoA9?i>OMMkUv70LaJf%S(_+X`RU}@oR~(0IkERMHMsns}f!M*wNIE0@6-$C_PZm@qK>9b30$Mnj<$M`d3ub z%P@6)OrD9txH&JV_m|xDu#RmBt!|IXW;!lmC9M`i`d^5Jn;5nC0Zkm2?nk+~Jh<(l~DgxF=ucrkt(I{`yd(_qKNtm(9IlqCr%D&60TxVAt>0}`CZ$hsM^KW8wZiMoG~0SS(oL{4^74Aw66Sl$9@Fk; zhx57ldeySq32il0&QWil*8JLX%~+>lYOztTxzurXc6MaxxPv+=_k=&47K;+Ah}!vl z&hp(1qN?wc{xhik-n(L-`yBd7iANoYbpZb9!|3cAqj2q&nXVPk9V)%R*ujO zv7A?kyNW&4vni509qW)9@5x3ror1Dh&CQrn)>dP{j>rd^>dGTqKOfC$&UfUuIj0ce z_u$>p`U<^FWGwIV)jDv-i3H4^KFue{6=b8~iVGArva%8j&fwWvdL^(&=4P&aqn;2C zw|BHeVga3Q!hBH$IaO*loArn7`+rWUG3;)lR%YOAr?YSBd3qT*dfo7?qm+1bkA*p< zLXx(a>M_FU!fkN!nDL7*=_6VF&T$l}2V~nj=&!NfOF8+)50y7Iu4HAQlCS_(CwI2o zpmb4p^#;)X`Xqm)s{-z7D)78pqs!bnSeW#Y0>dxP%q;8eK`oshI2=$cSbqLowZf;n z6mcXO$-A4o;yP3B z5+RG7yxWT&R>DZIQq8rS*!h0-)06i0aa6@jb0j1tE-XYt6tX|rxDW(<9m1|WgI{#r zK}s&0>U@30-tNOygeeZs$F8D`C2h5X7V;mnj+;3ds|q^97Z=N;obMpXFL!2E$L*}W zJRKg2a`S}DRJw{1Chc+K*0h1x{CYJn-*ICU4k}mmxrFig+7{Y5fpC=cf^b zfdAwTG{6y(TU04=_U$f}Dxo8mNx&Q624d6;4Y60SF@@PLpnv@i{9`-Tgzc?q4Gay& zufM2Z=^_-D^1G}yO2U11Vr5jr_a0)0zF0iCet`k~6Yj*xpAYXy1>y;L;~E0`4O0>% zqSz~i#GQgpCbS80CddaPJ~5d5o~>ZsHnUyUc@t}EvbaDM3~keQ2q#XxHlEzV8k|qa zc5vs9om3bq&ckgV6LAKkjZ`)bEhjoVU#Lw-WC}M=ar!s7n^tA7y_CYNqV8Nn zyE1Ib1PR~p{d$Rp7cZx}u(xk%m=K}tTb|3@4jhsF_N(B9NjfjvzrI)rW*AySlewt+ zT2%4?#Gxl`bkLfD-qMr|Cs8h;m=l)+w*&qMp2WMgphlSX1G1BEgz&(N2@@p>46czf zJktxlfJZD51ZjxPx1Wq@E7{C{Jmwk|;CFf5A3NRzp1SrVWK*vtd{%)j_;5U?>I;dP^#b~i zapKr}rUCv}9dxV#tGm00rPH;u+igw0YTbz~fL-QvWJ7#09`7zorn%|iInEZs4m~=$ z8U9iH&Sf9KsP_yIh(yH2XJs zS}s@d&|;XvcJWk!8&pY3lG)%;`Mgvs0|Sf-El>m@k5e*p-U{Koxiqc-k$A8;A;ZDJ zfuAOvWAcCB_4lK7Z(tEM1>YXL*5-ACSHsgGe@B6N4oLscS31QKC=D8l@;DxHa08fj z((@p>v3zbw2)w)FI!Oyp1vH4;7Co@z-LlZled_A&Nrv_RdG;oDn@-^YiqE>1ojr;@{iL*Yn^ncs@^Tmg&a}<@mJKt6vfS_LS+j99 z*L|mQZuw1_|2q~hpkY3CT24;Q=;0La-N*^3K~nMX3`iNz+|IS~ta1sgDd;x+;$r5` zju|v5go7uG4!aITJ|m{;wr; z21DaUai8`W%>M0Y|NEJDwJ%=En|QjG@%(GvQ)BB2?)RcZDi!`VroTFWeH3t>>9>2; zNB*NI}7N>i4uM>mKc|UEIz;MQbf(PS?`?s~h3N zbbUOb^QH^k^!4+o`*Yz-`3O$O1A^ZjqXleloqk@~3Z-Ee`Mm53pFbZe%0y}%{=ZWI zaJgjb%v8*K)_3xsM#@wPg8YYsiY!lk5>u(M?^SX;77-F@yC;{|L)!}8U{a2YOyx1n z6s$n1Yq+@=-!#3GN@5}c{1YF8L8|TI6lKzzALL|YsGP2B4-f8ga%#Z|3D`>(eBg+% zuuk*Jwy?W9S4`~HL%HNtL;Or$2wV>i?O1n7D6!JnN%GW#L7{YX#KRS0>;&>jpuE-LxA^>fVhYqDX8wnN$J~o2dg^ z3J)i;w$(?{u?m8su3*!voIzYc{ml1Sf|6A&qx{#I^n!kY^{>03&&uRz;55k5YFn>yci z?!RWQJ>(<27~e;6LGYF(MpR4c;UcNS8zK)J4XdwN^+O2s(iu&wQT|PrUcp%?PY~r( zQ}3c^*w|*E(i(QkN=tiydLY}~(4Lcvi(WvvXUHewN3F^q1_lOIuCs4@`#+2j z57vu;?4a*aUiWh9fP?aSt8HTO5c}>$_ zy-MW(a2Bydt?#{0*GYu5wH{J%eAZM%py^YOZ+~PJ79D45P{IcFO_YDd$P z%Zvm`y~mE#hoK~y!)-@zD*H?>ZeR#x|McR7P?ipasZao?`+FnAVwha?xP4yUhORxM!IJ|Tj`Im94^ke1~OWCLX?>@UF%w37D+!3u>o%!e^9m0hNr3(V+I z@6{DN)){stR4jw*S~eSSe%4jr`k_=SLdvHvyb)>HGn3q|KbWsJ*r~WmEBuxgfc0jd z3sik7LeV=wPRm1ZwO`g{UR2llv_00Cuf#c!R+^XByX@tZafq}1bp)=rx4&VnR;H}} zwtZ|WcKz!j=UewUK(Kgt;*CMt7rUj7#&4Mdk+g}7-}g*1qvsxqsU&rEqaI%043C?P z_lIlj{P<(L>3skT*!0+l>4&OO_$4!S(fWW3ZOLM&qGmPELBhi~^r zLBF<&fpr9j4h|=6bQXrR#{ZI?$u8N@RwJ2W7fUKE0X}vKxKC7;JNULQZI=fE+{GJ^JJND7|SiZkiFlZ9j&dd$q1Kk9A2V zgAHxT@h4j>BJ`_Q$+o~*Plr}(+2bJZCjwfnuf!TvFG`i^2FfczqC!G?fq*}7{#w_4 z6w6Qt&MB5$-n|6MLpA)%F|BF|F$gUkac~loI%l=hL${h_w)1Ws9QCBDnVF~F?+0W! zr$9bz+Chqlh`a-iO@c~A+V;cN@LoFOOLMENUjbK^pGZ%Y=qPDfI+{kAr7kbGJ_!zR zpfvumNz+vMWmS89JW-!ZEr)PQ;}u(uw7E9|hf|cB+$0W)?C%j!=-)3?uO(T!&4Ak- zC%;E#H#dv4w9HB1%o&u0IKnm7&^d~jiq@j7VLZ6`S>p?#I%f8udhY`4erz4lK7kVcgNZi zvYh52GPs#x+XW5l;460{mW}9dB?pYlQlA$-5I)ZPL-odC`>W$uJQsBpBP#P4K%XV8 zq-Fr8vxp8fc#JO^msC`AL);P>2s~gT;~$d#TDs_`DA5%IDhr zgAy6CJaG-B*n#jKU`IrE5ay|CJU;8K=*Rb1C_bJT?eePC$d{Oqpm1+Bba5`(f?7IF zH$iifaF2yZCN_S)t=h#}V{VI8y32A!zfdMQtiFb$1jhTarZ*fW72MwNWeNbIx=E2( zINMKKxR&Y4yKxu_HvGfFXM=;-_R6}(M;3TXQ_mU*TPNszYIC{GLl3Am8k1e!cj{Hf zJPgH|Dj+5lMxmoLX%hOWFxGCw?=~ttDFzk_#uFT=EKJ-p_*|CCHG+sb~5u3j{o5XW4X;mG9enyAEr88!O20Hm2j<4$ZW@Oy!H!! z2eGP8%CKEsiwojU@q62brki(VIv^n`2n3IHI;?oC1*(IseY%MhU}NN0wEL#k&0C>e{)pr{DWUIH-tWhlq3jjZFICGlk>9Dg`9s z>H2I;y)UWVe>w>iVEdkMiCxcr5oe#t*rxHupty(GdX?9I1l*EhmUrF72q>zbn>jAr z=FsPX<;_x`uB@NuuIT@CBX4w}aHGx$d?1BMm=qz3TU!H@-niJ3gAS%HY+tX zb`~o&eRPy;rc)GjnoX!+mJ*1vg0wV?*L<#+TH!Be9mGI1-(}fB1#-EfLx$Sd)S|9l z=l3^>UXLnY!gT&vpx#WVWv#{HJMfdP0g-u+tnT(`@?qC!ztN^PlX305e62VKFP@4> z9+?E${kMd?&Zu{?v5}3OG5wT77Ok=plIBsSmRfDj%vZ;2j6TKIftyaD$Bxbb^7cnC zRrL$gEQe{NVR{c;Ae|2OG|4<#1!!91e5he&Fi0G&k}dudz@b1V8y~OdaWZ&K?KGGh zJiBr?9!&3(yTs6j4v+WLEJs$&l zT3>ZpR5Et|^GR<=a&>Qry&%uFHZJ%&xS9Fc1`^~)X@_c-}QgZ2-#UgW% zj)~9Af8rfkg^;g>!3s3nx!xEE772LvfR7BikW6%S$Kgh(%#DkQfuxlcx2;aT7#wJ{ zv?DLoG!L!sQ~6^M;d$S5dS3`(9A$R~VGNFs_&ps+r*N@Cb1g0ca$K%is_yT36X?{Y zI-Y@JagBLI=Sb?M9URyjted6(mvIiKn$9)yDph6YDFd5+r* z`t!{~iz|dn#0TQkM|u}%M;`wiJY57D(5p9sK8`-!-S68j=QNyuRzt(WB3i7ix#X7B zNhNqThY)aDC9ShE)1cANu;zzaBTGnx*xvnAGhAt&ZpQsJuYNx;%kQs5gXX)cmvC#J zbWfno-i}P}&v6JJuIq%W{4r+$L=jUrab@FHQIBpdiAym{p&`L0&wHR zouLftf1wK>G{#PE`(6S4Rg!<**q{M!IP>Goh5XaRU#;y7!ofZ9w!he&OyaNvzqBzx zM~_NNLqw6SU^{<-?DUU`{?!5-As>ivWB`ZuGa5+{nvVvkI>(({ZdVpz!~#TWi)uIP zekeSz4<3vG!Co;5iT4Tyw%AK2WSj@=rVZt5bF15mg28(a_*5Ns(d_CZi@krSZAJEOIRth_Ep-7Jj@2 zzT^W&ka|}UHu!$g(?2kf6i|PI%F8t{sJ;u14+@SD_3&tiQ*1n5o?YGKW}rc%O(-OL z`xZWaYPiIw%rYcAylY>nl;R{@R9c#TigIwpYn{Z`7xL6kI@RX;wzO1KMISyd==SQ^ zGSn{he@7f}^q+JEA*XQRjyO7+(2!WRYs=~sTNe2vfMj*sbJr^|##VuZx|WuHMURc| z2)66%KFE#P^tqlL9q59_jtiC6t8O66(c;KRPHlHRK7giK_2s&7;;5-uGI4#+K~{gH z;qc`Ayr*`YHyoUT4`lc;NV~dJTMTXQZ>4T8$8(j3t=F0fY7ld+UwHC7YJ>t#UzP}+>8*FeR_hmmEJg+t8jy6(8< zi%X2(7$MgoC_VN;Ne|B3N0G8&BqsfM!ruFv4hiM^_5_ty}^>xZJ zor_;~cwVpGbi)8~Y3lm4q`D6K=XSTo!L3Pb1ww=pBS`?h@E30w=rQCijhD#dsV>lY z*q?tBo1n3jx8nAHvK{Lia2H%8voGYRU+kwX=RwbK?A0))EE_t=%SJYg)HfW~j2Slo z5{sr(6s|yo@y_W9Qk^oF;XcPa*et4K#onAENvqKq3HkNiveXAQ;CP&AiBjspw%A~x znqk+X9YDxfVoAs1j=Gdv(xXj>50a*ZGrb_Jtr>Z%?E>sq+G*)qGBp-&BmjTO#LtCC zbOsS!aSE04JoVxQp_2FqA;nTj_Dkxqbe@3=&k2Uxj}b%%Geg^LcdCJF5*3xu1{=@FI%cj}H3&wlH8ryNpdQT|ce8=!QZ_-tu{p>~oe$ zKD?T%2sJx!w0nQ<`OZcXRJv&*gIeHW2NiJQfHx~6BWQ#dtkl?0q5PC}#N8B*%MNe! zAAf+zAsEvoT=T{7A#>#re)hQ5!6O@T!KtF7PuXf$dPPq&zt+s6KR7Ozyt>rlzxA4=Gb`ZFV> z7XIF>wFd&|Vf;Dn#Vp}LrWbNYjj>I$`i~8?eLy_ zNv)h({G_NW2PWkC>Rcy@;aK2lGLuw!ST>n=(f)c?w@%Tx>?zUf-f7zCN%naOg;~8n z+AM{l*3{gEaNI+4?jru_9+$@$QFP=?7HnrmNvQIay4Dd;X}#9qcpCqQ)j$Iot(JA+ z9s1S`LEtQp=Dn=W&R15R7rUypvaPcKTPF3eyVYrFKnDjKGsRm`gN?v&xtQv(zjTD& zbc&%_Sk(Do!7VSI(|9>084UUc*-(;+N;-}N+B0KY=tPx*k_ETvhQ1y{rfnd(>YFm!FPUo=5^0J z_j7O0eZAk;`?_x1x?oQre(5C+9iFx#!pO|X$lKNSC|Tm|%a<>gU&-Z}+=&!EUAc2W z{}~e-8#|AM@VlN_{Am&|i}Xz+Z~PZgv9STwew{c~A2iJ1?Ccyj4SnBYn?f0ofNGEj zP8=WTdQbB$o(^n*zsg)@)#%Oa>+9Q_*sVVSgw@KnBeX*khjY`U=9lV?m>%CZBWln* zKuPy}V8s&8PYP=WlWbP?dsZSQ}MJho%5EJb8Y1 z`_dX|vsEaCQ^o>2qoH#nACn54y43kOyvVl?4d2X__fSopCptIe0xFq@slVOh=cEy zY%zxe>;FtQR`yzZp$U41iac z^Xr+WuHWwDWeVJ>WcNF4;>5x~zKjI?!}AQvr>rtgJ}b+c(4w<|e|S)^XAgkfCm&e+ z0|0!Yex6?2`0Y-XrcII{g#pu5BuPJvm$?Ha%w6HS*>dt%?4o2D{<@u!y3l*{zqN?P zi}$qOsBc!(7f9}J6{Z99!ukCCX6$dKG4KyVHmuqBbxO;-O++H+I?X!uS=vw!Nuwqv zXBQV02UF>a%8D$)zwf%qKY>OCCFAQ^DI>LbRKokfG#W*&3%w%)Qh%T)9ip;Blk)R- z-l#Tef+SaI+pkv`{CawX&FPy7h{>ofNOtJeCYym*ldevu4?|D8{3Wxz-( zxp`Z~0%XV=;=@sp9L1W!1MP!x8#50<*gcfM|N8O-az42{fT>^(G*+%F-d6DhPkY?` z2ytKl`If>q{7w$Wv4$l2l+&nhqTolkvCUJ;S4{0!c$K)mfX2n;W!O^uD;wA1Aw@Z&kmm&*)JudA2D1{V(uGAhi|ytW=64L<9?dWso=S)HW%HK#=Z zBswtNNJ$Mo-A;DwH=ABS|FALf;Z}mvW@Yg{1q=p`I1Np8TObzmDdH(uO^v+5=Rx_= zB(IdT3?C2AsR{VJ`dH1jbendywV0Ua*r(2xqzC8Sy_Z*Nti-NYEbnu$AIFDEdw%UG zFWtHwWv7or{xtBrc|K_U>fKUHUMS+5UdC$Ux||ZRn%n%r=bMU^E1yI+WwG3ZjUm5~ zm@Nf7Cw$88+y|-4fYZ)4*$z2gkC!al_i(aTt)op!OaM66iJ?5IswxbR{RaO0=|T(w zb5n-QgOo3}Mq{Wt@K!m(bHKhz$|S|(J*D+NZLx^{+8Uk8SDA6u!Qm)ISq~Zx9Z($l zpndJU?jKvW*O(R6u~bQK_ji?8GT}uLNy};9weN@rZ)2>hmngl66OOaTmkq9 z2(+;1qP~&drxDhdH9{@-`e3oiBtZjYB!KZ06dSyjib}nz=R9U1U^5uDF0)(kA~m12 zHMc^!Z7mH(IUZ~kv!#Uk-Z7aM$n)ks4bA!kWvAPg#v$m;(6(vsOGH zUGR<^X`x0e#KOQV4OEhy!qy3}QCQ}k60vh_3 zxlyBmA}8-NzR~l>=IBJTyaFTRW}|BxYC*bAr@90xv50z+CRN z3gpfa`I^meBjv`W4}6wM%7+v?JCrjf4i3|6 z8$VuQS#(_8v)gX^3EC`NkHz@?_#TXc(?2%apOdAaqOzII572Aa9^=IkA1%Dboa%fT zMwOG=ovs*r&c($sKsL}ts7n<(1>~p*Vcm&3ym;tcIVE{NokpQJbU@5eu0C9o&xvHM zQwAfL5F~k2vD+n!UgtjMPiX8U5FIl^_Aft*5}52xR93m;`|=u__>2*SfJk^l=zzY3 zJ$Xd9e0uypiZFJr<2|O`$3{d*T$mDd%E#mp`^Ls*9qL=bnwPVg`HfP?>mnl3BDr{r z`?>Erue&l!OG^u!a-HD4^y5YBD^XNx)ztwVIU^&OIwi^#(pe#sg zu|sTw4jyXOo(7O1T+TubHS%VA|D2;d~?tS0@GuB-a25!qkWeah;Og>Xw~)JZ%U0%a~ki;X(9>`6!0wzbHQ?%Zaj$2 zoZRY7X|^>RajUM<<%E?gPaffEN(z3Z7vVi2_$d(G^r#nUOS(8`h)LY4 zx!k!#MBOKdQehu}vF_)j6BkGDcgKQB5S*%oTfkh{a(DCGlRMVh(XgT&64-I&7mH}4 zMX}N&ECEiUk5g7+D-L8b!hKokUcOWVTLy)PtCc;Pw)m+l^FAZz8FQQ6HgnNucBzAi z{siNkeTf6jiHXO6&}Fm%$p3ZduD3A2iSwAT;_97N0HRuE2jbzQi5EZ1;jN+w5n^5| zE$YrPL(?FF$3v6XDNWAARKYmmMSE5qHYhSMAA@1-N)lCZ)1wvO7qr{0D(vV2QXHYH zrH24FSq(G=%OWAs=eUuXWQ}(Q!W&(OJMQ5Ia-${WZc^>;eXVuMXxl1uXC85GP*qd? z@V+=Xxee<*=D)$3~)$Z)tGj`9m=aIQ?VEyje^E+-61K**qL(#MnU5BdJQ-W%B z2QPxA%r*`~MjRyxD=Qvr11kg^=ssmO9NhjI^b357D^O5jo|p^^2D+8&2^q&STXU(Z z1tDv@5D=NNM;|%#v{ND^+Z(F_X7Q?PyRM}?2I1!JG~V%>B7yr7=}JDVbq;}eF9W&4 zXbi0cUtNp#kEHBSbVT(1sF- zAULCJZS5QHWn_nMSq^`}#SW6b9{x8pX`zsFJI@;i!anW}RJKt?SH!Xdytc z8dh${RCYM(R0fD-#=1XC?YTETOla>r7)eB9_FQvBKAma&Nfyl=a#_s%UMxJNP8-ZonhSVn6ZYCC&{_W zDqq`xp?qBa6p61SmF9lV)J>V__@EhA(!^#Ixa;pei`l^oS1M#G z2D6BWh`3!qdr$o`^<-&kL@ZL_fEMNemG}b|uM7^)m3~`NayPsWPVvdl0Y;D&LK1Jc zF9%}mqKW4e0bWEaMFM*0HTL8j==Afv!J@mifDCGVsvQClxoZa|d()ov_GGx~U{XLG zX*h`-iX@dDl8#!siJ4!4(8-`%xvik1O~E4~MNX7`8}2Zt8D$r;AZ^(>^j8eQB5Pf9 z!MDF+2!_h*T&+v8i{te5zu-(s9!Vd4K|Dx0v!vjQh@4r^#L2hV=#?WlDyzJiq7XCe zmqh>QVcsMM&D-Ux0kGW%$_LVC&m~NxB_ue0N-tSh?08eOuSjI$;y_eYZDlxeuLa7b zc~nxQ;osr=a~v!nsrrkqhF#*^zih8oT^q247U0OoATpF;moJdWdJ8LOY?o@*amH|l zs~hb_QBi`aX{$kU9_==-s+yd6fEl0aURo7AH&O-U5M+*f@ggQ+rTzvZAeBf=>FB7p zBfE!W;1?2NP#&q$9@48nyZoa_B+D#p+{a*&{0p>xT!*?tKTJVnZGqVmuNS9g{&rN%=#?($F(UYbybc z!!o&DS+N7F#S=%(^%Fl^;Oj0o!)A`9X%QX>(X?-UHWbJoddC568F{jXNS?ESFDxq4sDSYK)jzv%4d2xcGC7A?9Q#1dVT#~ z*d4Vq{*2;5-Fzg3H*CO0Ky$X*Uk}oLnKIkLIsmqW?C(?jF|0Mr==*xtSWt8Jl4!e| zpy+}Na@RO7f7bCIc*^wx+(>Rrx61{sz&-3AOf=HxKnTQSKD}b@mt`q%Ydvx3S^1}L zM#*Weq>_hADSqcZ&%Mo(dQ0Kqi+f@Sp+*z#ze7L*ELf{20GS8MX-;%+ z-{G=3=zIrobXTZ|c~!^fL)gzQJ|dUQY?5dgUeTbAH2+;FnpH_OdcbT7@TlQ9y(3(9 zMz88hQs^+0K&jEL$PCCZ31U6=G8BbNl1IV&x5$SL5Lv^Y>0!34MH!dn5%MGY_Pz zs7dzww1k_K@?HXb7b~-6{(-1;>tU9$V?~;gND>$W7Vyyq6&GU>>dwy8!J9LLKw7$vD}$%j{O4?kLY4zl7*R*=8|QgT&)(_ zeb;B^>hVxgz241iZ6`1~@kv>yMd`whq4Gh>#iNV-r?FJM9PEo9)EfZiVsrF8-luBA zIj%UKFn6ytyZr^EZb#=g_({Db&lB_Xnfgp|ngDwY;)T2CMSxeUt);;u&7zVrWa}rU zYAVJ8bfxCpO60Qb&8}SsG2u;z&T2>-8hMwS_uOVnsD8DKW58nz&wVBc1hi*Q@26V9 zz10Hz+_wkb7?c(h!`%h<<}ZUWYzwy8#zQv3ajbT9xGHJWRilUzewuK!L(RPvwf5=xeItmi zSG3S-WB<~0K=Ly(B;{6sTBGK_;;*JwR$z!1coz}(Cky92R5B2Jijjfl&s88A((Hp0 zxlyE5mrU=zaXMCAS36Tvbd`p|Bb$nh7%{gv?^inlKeskE9#a+juVRXC6^P_vmm(6; zY_CV_9A^)%;?BOCXKUP*e(0`3kaF0tXYQRJ8Qc%Yh}Po2j6f1udPl{jec8SdUk1wk z=%1LNx8_oG#B!AaY%NN`yWN0cJbp7Zny~X0>gek?07N)kIoP-`-*(x)Egb(z=}nzj zUM{b&6m5?dc;9bce!0gDEY=dgtpyHI+EPmclC@CfDl+*X^*9hzKlf56vPyi0>+k&H zS0bZbPTAk|2@B z_F3uxyA)m0o)0xaJD4PC-V?NA@0rI{-ZtLTB8AXXN&2mH~S2=_F*30pL zu^I#NaS21ojHcy8_qe_Zp4)77yp~DVqZlO0ny@O<*9?1SyrEptO zLfmP_JRJ%Sk(oQgx~tgouR@m#bKE~S*m`W&JJF8FJ`@7tmGlhG6fc-8k&V|MrbR7H zK{;P~M+W2I$)KP}8Y%QWU*DLNCM|Mh{gY0S@b`e0)RF9pVxG!vLJ`d6$hNp=nCMM+ zh|#jCaooP{6TIw~ibUE8u;C;!fjH_;M8Zl>>&gv*62$pVCV*CYSU zGrg0QKDZQrSJy=VqqJy znE=M^#pC>rfL}1>Z(r`@V*rFA*RskxPgHPxsSyrD+r3<4fzkg~kbe6()<#L{ZpweT z)Fv$kfG1(fxUw4d--1x)W0JKcs_pu3bS*m#P`75wo$rdjbDpXI4QTMQ_nyPA!TFmG z(BgFU;wCH257iBicZM&j!9=5erkYypfP+&I{x-|n~yJ>XpZA~rqt{b zey1QQjw#5gYa3j@Q;=BzNhlcTi~{|#?Ecp=VJ~?AH3z?f`Z)Q23;w?j#(oHh%2~8$ TXXWq|@X=N`_@hMCCj9>Z|D)AT literal 12715 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 dEqHMslcuDQOz={{Y0EHOl}1 diff --git a/docs/images/gateway/plugin-ide-list.png b/docs/images/gateway/plugin-ide-list.png index 40b9f2f71bcc8657795c50c6337318d0304fa301..0f767abfd0dd1af251fdf090d996b357b26c5a03 100644 GIT binary patch literal 90242 zcmeFXWmH|s@&^h8cM0wug1fuJ!JQBYe(>P#?gV$2;O_1ag8RXOySqO!bLY;dDVSaZp^tu zkLvQ-w8Q-*ofSO07Lr8^s{}fRel$)J`o4M-1AQ@Hm*gE>NH&Z-gnmT?J{lTya^#73 z{9y#30I`nB;?D8In*T9OMNXSSrnq=`O-F6Coaop?k3uSwGGr^6KIu zH(5k=+n3P(Ats`!-TWtU>SLQ!8W}lX61ib=XIu^zg;?4V^q3sJLy%cDQx)N0s_W$r%6iuIoNrdPm5;Tr2#cyD|Nhip^aUM!>0&rAOZw?*rC zaW|PqH-;2~XY4AU&!SPXoW=VC;$Bl%+$3Tui(!ZM8euALQ-WI$o!2t$t5Xwii!+f^(e?IdxpP^gM7S7rb5!V;5CaAV@#i&s zg2oJ5>H#de>6rOfyE~FJ<2(9{TMLV@+pIrP1H>X&&Pc#|s!;F^y5P8e`Op*fZHMk6 zw&(rIB2S}D$}iZhZq`f+UGylPu!dNgT%Cmb^4(NYH?;9vZ+i$Z1Lt!43xA($^z|@^ zSx*L6$Pg{@;U#(80Rb!zH9JTAd(3Y1T@VNkRtO5ZN(ctA3zDuIi=Z2muN%9R3$lk4 z5B&=?EGc1)FmkQF0G%({2VeFoqL2EtM?U;j#KznC;vgAR7;%oTzCI6p*w9zFequ(i8a34X-_7 zRf2aCdk+lx>qjp7bhI(7!S4Ib9^23+)pUYJRE-GQLAA|;ucJ-s%W^u@CzK}`nIWbd zi!Ptqs0DC{{joPp*ReM=HfYb?¥F@6zrVJOp3xedQ;}7f|_NJw*6OBbm|^#Xe9U zkr9xllP^O-o6yP+` zs$w81kR*?Ei|_%dCq-!t(s0z4kR6$pjIIm=^(kc*$r{OREDVkLd)6=*%5<{ap9N)w zNBL|j6-sU&{8b%Gpk~AIVOjJQ>#Vw*}NwmwTmq>{qN;gICcd05x8aGk?9nM0qG;+4G zIA^~Mx0GA5R_3zFqzFrXL?^s5p`c#-Fnf-$e0=k2&~`9%upCE&^JJh21+-c3S>F%p z2VH|2>je+193dQ79On+$4)~_}548{FXEVxM#c$J3M$XI5GdBY_!v~28y!B9KYv;me z(ux{IPs3JyI()riIso2zFSyT1(Av-)(74dX(A0UU~i>a0jyy2g_ z1x8Ldr&!@RlyR&$%~|o-8knM(bgKLGaQh<+ z6RjJ%%Vv-Kn*&|b@9i!nE={pOSRUaJ;o9NBi4L;FBU*{GiH&6;>I9{L(oyyKawQAw zLx;nUhZY>)BTFOuOlsvGA$uTi;ilkbF?cM^8g!OCmb8}`c!_{&z5-#| zxMsvI2`j0x!n(q4o@ZWwQ>D?UQO=r>0E^nMHrBp7?mYBfVZLI5JmJd3)6s)KCpb4R-ltu?eW z@ohp@d)D%4$Qq0{$}7}MkqehDYGv{~RyGz6?Snd^w^gx(Oi_ibZCGJaS5#^+hj@iAuf-UN31naRT|g(7sI$ae#3m~) zD-@AfyS^z=`)H|A|csZJ95$|>Ab%bw~-vKk!H|R-W?kerdvMN~xt_9_? zhqAZQj0)Z~=SiwEx~7n(mZNtuxS7qom+tC1>Yg0jyxfo9RYg;2_|ptySgDIXn~>AS zI*wf=*L$gV@~L*vVh?9zF2xV_1m)4f%l4y$Rd}2}l{`h)8k!QP<_Sph?EcU>Y^L40 z!Ggqk;zJQs;Hot4n0%tVY{mMDZK)SR2hdum88T8@&&})?4!@<#VHno2b1Z(InkqO= z?5DS2m|@h>S$GzmK&clm5x>Zd+RhwldVSwtPvqQccee4EX~x-z(*P5-*G>N&~?LK?$#=lgl*P&bJNde0^(CZ$mdj zQxy@F$H|yH`q9i$Nw4}x$6Mw+L$yKn=t==$ekGTU$6>#?cSS};x|#C4D=RFu+ubR5 ztc_#x{7X&@SIdEyzPsafcApe<&sx47U;sLtM#IC{iXAFN)d^Mb=9nlDWy?VZs@%Hc@x41t%wNdYwb!~mjxNJ@I4e=d3 ztzAR$`tb7o(srDBtY~iBHp?Bx)SJ|^_|E*g{{GWmWAIvSr*{(EmfyM(STH>sJLa!Z zYcMwmuuC2=N6-*}-*RZ^)koj&wU6BXwyGK8Xb&tT69&}@^NS%R$CSZC1ceVQ^v*5h z%&>H`#O~UFGE^Yo{>Aiy687litD0F0yFJ*xC5Ap63}HKEfHZ)0+*e zZmcP7A}0s-@vRIC1|Dn%2KiP3e|zxWo;PzhHV6#n?T!BSh-E|kRr@YG``uqF8pMFwBUIw|NhKKM)F4!2Mc~OO*uspQ5!pB5)K9?1|~8AcoGs4 zK06~59%V6!f04ia<0k_;IDF+{WOR0RW^iU@u(2~`Waj4PW@KVvWMQFyYe8@CYVDx! zLT_zP{%0qD^&@6%Z)j)s)xpfhn&fxC`UW!G zr2I$Ce^RR18{3K6SiP}y5cqp#{zd%H%6}2^G5((UKP2&|nE&|tCT9V7KE{8qnE*Ve zsVNp1m=Kt>n6Rn~_>m`!%jZGI9pO`w1tA|KOEC9S@mR`l2criylp}WOT*D?9y!eBm zaW2P}>1DO&Jo0nw)-^_-W)C9KDLJa0(T|`=7QkX4LsqMIvmVF#+kzigTcaUqQ2Bz1 zZFl9fymwYPU(;FbH?7;B$HpFlpp}&%--C+_p(*~yivb$~7kB@C<<_NLcj^5{h%g_r zY}gO~=LHLKeU0>KWP8eApu0V6|&8>DJ4ACl-H$8H9a5H8ycPRi|qVYB4QBxcjo}cJa z%yj;kx|<-x`T2RkYevS?s)h4v-p2gor0^Ze`NgIAXA={u(JapB%fsx1QuWsT_Kh3% zPKF3j^9u7zHdL1oQD|srq2L6yQ>PBRE{fnC*flYekFMu2JCEbRRe=#Ehn2E2`t~+b zPEe2pkM~PkvHQXh-oGVzMFNot>F`yA&(l;28526BD4vs>GW6^<>Zj_1wkx|@18*E5 z9Evkx`mFHlV>RAKf(<}2#p;&6RlsZGI54;K{xgT#+=I%HlM%*emoVxtqLkFTAdXnq z-647NRMDB!B-{6T7z7p0e@Ikk6qZiADW*^%O{U(_@#@etlU=TP{oxQe)fcMjv;o=v zGLSZKn%R1qt1lWnHEBqBHP(6>WMyX}0WF84XVf!a_S~d_G0{dU3fvk(D?EO@IYwEc z>I8RQ_QJ=EAdp^iKFW7IoR^g2==dJ;a6Mv@t}wMfjZE`C{kV_4zP`>>fqA#J_vYp} z=%!Y$(<^{-F(Lx6w*MpnkdrGgmbEEd?+r=`gwMCIHp^kRn49wThw(Jt3=FIcz?1%K ze|~{%VHN-iv=0?UVukE?vIvVW10hC>uEU8R$7 z4V5jJzjt_33)%-pMoy$5gHdDXILV<9H1eO0KraE`HX+)iv^0ySA0vx&`O!l$q|I#3 zV+5)E_9oGFuy{gJ2DzQYVKi~nDHjOIkl|n!-!9eEX1cW}j>_n?>%M|VB3^|DJhqF} zKF{%sC{Bd(P1!d?5D#k{NlQr>lL*_R`8CS7qx? ztvlUZzzzoc=xKhPqkv|O2J}JE@AE0dD4KmFmO_f?QtJCvT~!@@=mX@I{oaI!xw&~E z1qVW4JP~7Fw?Fjc)BWYbGRt`Kv50%)&8DO3ptdSw3WwuIOiawFY~OdqIo02y?1_jg zKba1}|CGaK)RyWEM&T4Iy*Se1aXCXh2DPVU=d`6rBmVTE{rJ)BD0y;bZcZYt(Xyc1 zALjfSM|1-9b<=U8!AV(JnJlhe91s$u!h>-T76uRM#v3*y?L)v@U92@pIS}E!TO8+; zpiRk6uz&h`eQy4VqEpJT`7&yWCTR%X7+wRCquTRGL%Y;Sh;9pZ;kPgn=n0O;TvpCeg71cxmF zn?Z-^){hp8WPmA3Zkh4%hB&)?sX<9chhW|;Y|?LcYfF(lID^kIj9GVlr@njbE|bZN z@3#MoN~c<>hRXS>XQ6dg$EhyfM5o|rGV=;h7{+dmQHLqZj@z0gZo1^a9MoXNOUUDq z+@p_Xo$k8@@owm1`xGPOp%;Z8h$X{exhy3aMNrUs($)*~A5ah|HA%p6 zVW?sz^CZx|;tdy0XsQ+$7e{J3sj0E&SPfaQry-ge8{?_iZ#kYEaqFo5>mICtfNVjV z$Tb?I8&+6(?xX*KuzHJ2qbF#)By!^^@uSjHGhY!&<@X)>>p(O+qTZY&``*I`@#d?D z)9Iq9N)dqBonkzzlEuroteNh^2c~>5qQiL`CNZ?aL?#SE7K(9HRSfPk)8!`H0mr)` zG*PZU>^~( zT%Ky<$MQ?O<-JR!DjhCGpM|ZhwHkf`Z3ZI=@x_GDkPRSn)kee~ns@b>Km$_Op@BXi ztILv+)>HZ-7y-{L(n0qo27C_d*uqrW5V9)NVv|wh?>FZkEVzVWg@0w!ml++iLqeWf zbA$)Z;o%KRLFB^bzTS`X>q}V)cT3F$aima?G!|*Hy6jEp=apC3d_PT`_3?cdLCltl zin{iA(!A8PJwDym=rTG;d}$#3arT_--O%ReGVq{mFI`O`TEq*ub3D@mKztD(({g~I z3d@&@&=7a4wzxS(+QYT4=9ZXC=yGS2-BD!i_H;ZT9jcE|TMzS3tH-zWov$?{TcUYm zisP}SvRKs8PFAVJn3RY+kKnLI>*MJCsJaW)mZi;ovw-XR(D!*SG+#1#viuw>+Z{DDlP3H*s? zFlK(-40VMh*=17szThkYXIi9>Joa`3avytoLNiuN1(@Xei|f4l#tsb*ATAvbh{N zd3i@7w1E0K=b2o#el!pWR54P<$$g+D>s&$&=( zO+0l))Z||^aZh3ekN8OaQq>{FBM}(yyvtp5hTA|Nr_Lbeb&3cL7FCBL2A*VZqstK> zzxbM9W*u}1D12M)Zgwc^=;J?v-KnjH|4Adh>UzsBS|TK0!&0kroE@+5_>9sQ{*}MW*ml#S0&3G!P7R<*>EfOd0i{Tp0mgiSR~(u9y4vB9SM zsfrU@+bZ3M*KOv5sIINuMR}PkQLnp`#l)+nGJP;wxpr+(Npsc-(qybmrk}XuHPj}i zb$U1Pt3FToD^4wrH%T^0dQ9wQfrKbtx#f{36gc7dqZacE=|n`yzYJv^epmdV&kpuq$u>Dgu$MMuD!f z1okTbWE_Ug_K1lAzb;Yv3;~RphOdso_X{F$elWRDb4#a9v%MYaK=5j3jBEcPB1O*F zZ7^K_uJ_L1wqvuK>EBGsCm`i6bBL0%C1@o~PsNXS6+ZblyD)*e%9v7Us;>LAbR zT>b%J)f))E{Q1&9Z7`uz@$5sydm%!-QmgD<0k$+rF60e~xcP>{iS-L>zQ#{AxSuD z$%QJied7y6CSRnVw{$Pw4HNRa)h#YHIKQJr` z#D7#5{_&*&{vy<)!D>x@Aq)0fThFX9ZyYD7V=n}SE-38k?&M>Lj+`74@tF#M%NUEy zik;I}aH3~pDUIt6CV=QsC=!y*&8fpL<(S&F;j*#Gxi-=M6@2$GLzBzr1|tAPdr}eH z);&<+7iOGspLj+N)If3b!Ye#I4e|#FfeA%}2r0pahWb=G(drP6v6wxVuK1-UR}Vk7 zjci(2KzC06tqKPgN;WqZH!84o`B3do4U&CJj@q~tgumU`Uw-_BV?6ANUJ-SslcBOu z=H>j7+`pEG%nRW?FGxS@-3gLNQjTl0}^LB93vBLn!awqh`^zf1<>l~%pe&g9X1LuX^f7Y zn9_aD>3^33mjHA;|0u|YB6vFl<2eNa^;n(UupUu|XLFIPo0%0ao0bPDLPNPNXTN^Q zMQXD!XK*MLC9}x&g?ivhchN^1;X+8VX{wnVINuBhB4?3BsJK;wUHq)=WX7BP>RR0S z*M1Z2iu1`$m43qa)>G$MiA@p7izPrZfm0AmoM)ddExWgjJile3$%#8jn+A;BsnF&@ zb5jXWnqurduh3i_B))mJ&QzEkq#GaE8A!}Chu5G+9hE81ToGuNHa{Sxj-On@uW4C` z{dvXx_I~z0!wMNbZDB;G$q0)+PD;``zM7sMkd;UNF`y1qSXm*@f;p+bT<_oBz79BK z_UHcMX8N*wzTUOo+4d|AFb8^(->c?3;v;l5nv#^GII8@YYxeuICTbVaT!x2vRoI{N z?3#`A>6A;-aK^8{`DfSvcKX}Fz$vravoY%6|3|eB0_+Fyb}Toor9W)i|BFBfF$xL6 z0U7ro%l-c3^?$18A>Ocu%0S&Yj{gB&Ody~tLVyr>giHVJi2r*)VO{k;KIjt`IZ5@A z|6@p35RmAHhX-!L?EiDbA0Rvl!DoGLu8HPuX#Nja@&|(n+24$1c|=Nc{^?1ly>a~qpiv87I2 z$R#4OU%0tdJ=c4v)uS^R?zG)FlIV2B<}Wo%Iz$X<-Y-o&PCQ%%Rj)2(zGf1grv76q z^hF6797(sSd|~1L(gg-;wl`G_c=JS*>}22x1?&PqOCY}<5t|?+LYX&9U+K-RmXVdq zV>KO{_+~iBHU3#x>GevXLo6jFMYYYDnE(0E)UT&vAPv8wa}4#{8xZLB>W^6nw_)12 z>!Fs=eLhn>-Qc`%hhCgu>wS3aymb?%I~YYM>U}n|E0 z@w`Xe@BZ}-QmXf_On&T!5s<4j8KVGC_n#!*ZhG<{`x^SK@REA{RHJOQr@U&YHG>} z!Q;H@k;pHN;RwQhR$-E zk@?F*of(_Q9n)_NPFARYoYn&PGt+xroX2kKJ3ax8KwV$fdvHhE1q1>|M@NT^E$JQ+ zNi#FE=sDO;=l!WPKxU`=i7Ss&Ss$x+d}I@9v4S4qd?wfx+W5nR8;|VZ+&0c(^^EN1KeZ2Pm{2t8MD^wXuHD_G-jiQVZH?L08*I{TVMEYB6t3v=!*w2at*H`1+;wKJVKs(7aWdR0*Ce-HLG-%3vO?Uc)_~~pA zx;K+!MPAPF0qPeOHDzGG%U#I2^@0Uort=Btc(ru%K;##U;KQ(ZBxR=|r8=9)p?&j! zTiNy0-9J^&0_+tm_)3Q%{_#R}##p+573>H2km&Hd;kY72+jF9lGCo=Ipy=%UK8jVC zg%!kGHx6MrcmPPxds7b7WL8^jC@(MgFh%@KiexmsDh*I>hyCyqml}}#h3dJe>1nO= z;^SL(K#P^m8^R`+#*=VydHFpr&tE=EK=ch~%pU#J&|>@R3Nr631LSIFQKFIBJ1$+MIvD1F1KrW9Tecd2jz zlEiLj;2dk|`ZubpHI_8bSElmGTNZ=Zol^64Gpa5Aa&Y<|VGIxqoNkV^+-}=%_lJ}u zdmjWlE{tnks*0MDu_0^BmKSUbJLPLQ&`j!0e~hd=ta$DcvN2~qRCOb zZnV48kn?B2hr`RoZUWjgJepz^k^7>8Jqw_qpaQr$pAz!4Zl{C2@3#eaTc|NWWZL?I z&)3EH6-ClOz(|qoqv*t#!d30p?u#Azl?KS$#G-o)uZKGd^&I>)B+K?!pTmn+;2=6y5R>(q$Dx(8hlHCvS!9CdNaG=dGtjebzzvAE{MbD( z-qiIte0oV-Uheej&VQ(TtSI=mjnc$}+GGTgn#S9QiVk3VyggOG=d{jOrhRzey~(S+UX*F?r%qy= z%ELrDj>lZf+ErUXlteU<@z20U%M~n@wxUcAzn~g8N=KA;xkSfTG{*^6fkqLS61Q-$$>X zJ>}e=cya}22%e#*u(+~pUo70MM|+DPAw{+gHkA^gD^z$r;yuR4#l_Kt-};NhFlc-G zQurM$`JF#63bK$O1ZhYr7pK<+%26mIccYSeXMl9yJ-!q%4=y!g+?a$ z(sl;;vi@#U_o-?e&)rt|*Ltux0v+hefnP{UU?kt2;uK}DXS$VGf$mS3pwN zGZH#53Qlm7Q~OmI;nUdL~ivL(-H`FEy2zZv8tvCf`dszgvYUr~8e4Bg` zj=^V43l*meUv9GNGMWI7u--M-U8{4B`5b^^_PURd3f@POd=8rWUgmpuoBK!A-l7x( zGAM|MC#HKF{aO{ECzDxAmphf-mknt8!`?Ds!0v^0=dn5jIMn*Zo1S3IVo!~kwV1;@ zUR>s`)e5hhxbyuY(i+w#Bp}@5{n)yjoV_!F5$!z(;IZl0FO(H1gQU1ZOnL_qHhe_# z`)-DyKc4S+(6FB^N;(8JQZ=7mK+H z$2c%rpu7G~tL6SE)p4?q1j;=LJ*3gOKPD|su#2-^ews8S3{`R(hfQIgq%3gV&ozi- zduOM{9o#}h&`yPH^@fnw73B!c{EJ-Pnt%f7}cvnZrYkss>rqZiQLY-Y8T#( zyaU48CMRd@elI%;_yovjuJ-B;`o8JBStzIUh!UJ~y=9L;*UL|>Yq$+?4a9x-ZD4(y zYYX;t9BBBdDwKoaN#K=b?5TkrgkujA#`7<5^f&5RBK#{Cc5k`a-ld0sJUM5Msrm8P z##{E?-SmgIP|DR=Z}f!TQ-4x%$pK$n5T=74b)4j4y#4uxIGg)TnKk750O|bdV(Ijb z3S$r05LAp9QI8-njDj{&*FuKmFTzZV0uFmsw*C$q({Ra5`Vh$U@mVf!k;{PqZ0(G< z2m~(rrvVIiAbXCmUl*dvg_X} z9k0ZNV}>bG@+mBfbxIpTyoMwC0#|RriRj4r4|AELsqE9~s|Z$5SmaE_0cFp3hA*9C zP194#qZ>ZWp%*Zq^(0-d{yx9^)3v8-y%ZIRzz@QwO!a$jbu0|T;1HNUkX>cs0qVKbfFm`qzIqp`0Jiy zvvic?DH&yi+#_~#zfa&MVYW?xi3nOQDEiZ4F_R3959CrGFsFcfnE~!_JG~ypt=$#)8oy;?R8|{A`(}Pv8(@s(RktLIM9{3hbA2)F=K*O(&j|d9 zSN{<<_FWAX`U&ZPob}7~@wgE~zMlW<7eT9)mV|m8S(+km{t6pNR!fKB9lpfk2>fDlJdiYqdic3qK^LmdIAu&i%$ty}CnMyH?F&fFs!jGY^3pOyL$ZTa!DGUC=uMJnG5pE*TNO-^+GyPH4 zy0iu%{HGtKl}oxNmF6+Qj_;;h0zIaxVn}Ger(sZAge|O9M3~A^4@JS=Y=jB*?rMxN z+oJo<;ce^tLahC$>yLu*#=n*F#E}s?c36KH(PgU|9IIGqg?nXGJL0)3cLzSUA1FGt z{k(!3;sxZ8zQq)#_~u^xUF5goijCK~EM-0FZR=*;$9|)7nxAleM|0Xr=U1nAPsCV%fO7(|R zz5?3^5ZWEfl#@gzE?ldcQ_0tJO2V?oNNW>jVVT+=(R+a>Uhdh78>Y;Us`tx*DBr!m zRAX~FBUz>8SVlz#u!(;K4o2)xN6c-#Ug*=W*SVtWRd8r^5dqK$qR5q%w9Js zQFo7>R}0=^V=28l+2TE%^7>@Zmm`K{;x!`y+=nTw+f)|7piiHcToi!8?;R)SZ4C!g zMPp)o%kSe6tLKq}bMMfA$ko9Z5OTdQ?_eURPN#2I4yj!g(@RP!pup0YdgwrvM(5q~ zWD`6+s(60AnU?FUmVSK${My7IxZe^-fI$vjaP7UqHH6#QwUdhN3*R=M?;aC$NC&w( z%?{llIf?oshWiU!rcBK2X<~K3gF$}jxg+C9$}~k!=XK5$`k8}^xj*biqLA#GE5Nma zhkncrYBLTG4Ho=15Ri2qeiOQIT=O`7;{SD1=%R^WUZLbkv%2~mfiTO;>X?D;Tjs0g z(t&AND^#iI-xx;)JmiL9`S!31wb?Y*ZoD=xJzIK1*UzzU@&eCu>&;xxQ?grW5t`!O zN8f@+JD87Zd-o%zi{#irN@XRFlvy9%J-!pagpi}W;WZfF`iaX5z!dUeCcVM8>A5?< z>N6D-ZLDg*4sLncNK#0%v6XTE`mrJdo9czj3i}nN#+@olhd1*wL=7Ds9f^p`_d06H zh81Qlq~W5gTXdx+?k-OpXq9=oDbDBXyz_lf`kc_Q_-TSZ}t z*n1ykzr+2MWATS(FB07{R8&N%^{TRR{QFAA#Bj zk8I*tisUmyykD<7nf1BBF_dYhDK73|9+`Y4zgsSK_`k&=+7)dsQ{pG??&^LBK8@Qr zhP$VavWXL!+C}n*Rs-oM=_@7-PM5s9lAR1t4zivb1(~}=R*#z2mvI=A2}U4s&xO(( z>_3r*t_dKH_WoWF#TR6OGuX$^=l2=jcdec5j}gWMq=*nbF!qXfn?iKS?B0jop1gHK z*0)ZCWRcU>aCt5ydJ&WBI;MP*)Cw6aG{^*eKxE-q)5E<^pT+q+(arP5xd+mmwV`xW>RlB_-MQd3h z;&T}K;`;zjJcA7b8;FT-L>qerBk#>W6sNrf)2-X|mIw5SnVh#;60NWcbt`n*)%*2_ z#fh`T$_&wU)$a@3ng~2NgF(le|F$Q*ZQeD_4CR4n^Lr)=#+&=NebNI9h_RIlCt#s~ zWUu4vhJVR;LNS={)>z>O*|5OhtC-U>4H<}PEyA^QZNmEX`?h-+R7fez^jhg2Y zw)~hEQh`$}9!M~1?RL0hUljvUFF0>S2_v3;~B&OiDtefkiV<$>Hwsw{e zr~H1JzSLopsKGlQ zvY3iYRDee{4C@t1>O|XfpMNov0JV~qD~G}Jn{$q+#qD@dXMs$ht!vX1Jb(ui`^6ip zt{~|nlS&}f>>-6_Wn1TSXNNH?Q=L$9-_F(*D*`IXGdB7911g9(nMCBwj2IALbv~_k z_Y>tNP{JHv>f=v3D;3$+bg7fCj?wWub^)J5-Nf9Y3~({3Bi{Dqt|`IQ`yE}w0_q{4ou3DtA3R{q46PZ8moZ}=I|0?2!%`?v0P8-mta+W zy28f7$PESh>|-1{Zu5?XbUQdWuo(j*Zh@`p5I=?gb0zX>QN;L7 z`bK-qg1?3`$PuWlkW_I0nFm6{0HEVW?{13BGv{vF2`vBHobD3N)=2$jzWX<%{SRdP ziXYN#3pKX`CcHuF?-Be>oLz+WhEPw3n0x)LZT|sMXTAaQ*8$@Ee*<*?$Q*RTdxNO? zBfszbk%;sUrhkm&SLj>5pMvI$%Kw+xQn)>o?6QXo>4>0H?nV^_l##yAN*(6v`&_FD z?ENnxbRqc49{@}8Za{YC#au!e$K|@;_@3i^+((eBW?-_Y{H`T$J6`u(I$CiR5pICIXI1gb^ut`E! zw9%Q-_@CP&`1h{t^)Yw)A6}WYPW75OF=z6>mhRVqgb>``?231L;f-FCAs8m2bgpDdh?Cbfq7sbK zJMXdWZ+$!^GBVlxdJo{na|HR-o%ft!Ho!gh>$$N1I%!=AqS>qQRci3Szute)pGmLo1LiU)LeJ`ORlO;BnhgY;SKXz2!8$0fC@N{|yqN z7ZgH{c=>cL-1iDL0$PxW6eSE?E8qd-XW-CiG%AZ6{UoTES)aOPhDR=?Zf@?|M|%gQrll3<=CXX3?+r%Ed$R`}9@s1#`THtpm&qj~Nb+%G z(`4mF<#t@F;udXDQC=_ao4Fky>Ig^g=(U-$p2rnGefp%BJU{wN_+6wU>jp9rTgVFG z$`lgL#M&>=6zCu!@O^_ z#8jDPe;6g?qF^?AqHh%1UO8rY{n6@Pbyqpt1$OQ}oM$1MT@yE7EOTspwBGU*XM=&S zd4ZC>OBD(_I_cescjmXB@_9^5j1*sAxC2U`6r|;IB)p>C;XqS!bF1ZR?HeF3)*=P= zPK2Dh1zWeCl}bGhIRZT?(;u`9Jgr+>Myc$csoh|mrW(qY>6XrPj(c#31~dwPg#~n$ zU@4GN#H{E?p#u^xo;$jlK42^_*|a7I=r(3X5ev$HHH@7Bfh1^Xh6CW#jEyPzAD4Fb ze&U)=Sr2~GX20C>=naarE1fUJ@m<<6Roq>?uQR=Ubf9=f#awAi{wPxlygcxnnV6L3 zT(%{qY`-F?cdk>?ai^~rc%kHZnDx&0vw5OLT7J}ROk`}|mjKndB4lvkdiuVJ+&h`6 zLGTX8?g1mv1pBhb)m8x;4#br_?mCr1fbp0(&3?BZnC520$kC0!zCz*XJ zA5PriZjYH~5eHz7Fc^_zzdY9~8RcdyEP8o4y47A-n24T27fHyP>U6?TX#a+&-T|k3 zTdT(N#ywKc*ks+CP5Oo~X!t)V_+poIJ0C0N; zA05=V@={l)n#yB_Zft5g$vkj5RPBaAP#z^3yfevk;_OZe?%2<|PKjgceoL5L%U5Ce z4gC30WieExnxgr8)3Ee#Kn;W1#>VDPzn+2}RuffUS84+I%b$TeHk(GRqMPC^r!_ILZ%@7V%ye(^*P#U+g~GicB9o&qfNZ z`iP~*@&*#qVG+GLEvN zwyoalx$c7k)6)q=Zy2EZfBdMu@+W@1Qm*+rU9l^k5UpNbcy;9f;R?0xao7F0zi*mZ z00Qw~C~P!^iV$cHf#3TBQki_8P$y#O7!I6{_LNlZ+9INuTmw0*Zhr2UO%m>)Pmoci zYXU)C;qM5S8(9nG^n0|8PL#gXT^TMa=UyCfgKFm|E27sggnmRN5Sd2O1FfpWMi{wn z3X+H|T=Nhll+_&TN%z0Me$`!OqKL=d(bm?6b8)PqayxE8n)wlq{_I)?G3{Fx3XOJ+ zK?G-G<_#TP&t2vj+%NQffI*v3SY1AoM-Jv)zSc!sVx=a@VjZQ6@SefBl&mZ(LLGLR z?)1OI^PXtFM5Nge08dYTtJRtWIeUve1O(qL5f*ZHZB5Nj-Z}^;6$oP`) z+^8V}Gqh$N=4)xV3qq8&FTYd%=g4ys0&2EZs2%QDe--pZLpCy$dZ?_ z4z;9$u^(deJP)t4dG!+A$wuRGJHlafDr3@a93*m0mZ%K^H`=d!3n@K3msa9MYI}JR zt+ZICFjHCqf%zMrk9+=@#HnsB&*1Bir``|LS8ti-DK%{yeFreCYlLq}!c+H)gG4!R zIqDc8MZ_rrR(KX9aeEoJiT1pSQ(ri=prkv}W0RAGw#y3m`;8zWL;`nRP}6k65Dk#1 z*h)#VVhkw<%}$O|;>Y5S66X@X?~iEaeg5nzT*ErfSJ!uEWAO{MTfuWV!eby>RRAUD z`*q`pKd&z{?>MCD5GyRnfPn~vZfUt?5P$LwF`v;8HQOdhY3Y20;k3!1CAYQ8Ibtcjb9*5W8{a`O7Ya$pEKy1+Lk*kK8qe_IU-kUKw9WaFsh zZV>%xd|`l@I-ckyD1}CUQjk!>MchfhXw|!%FlDb2r3XUwSG#twU3e zK7u>0x0n-HEU2EQ*}nw7xH6wUKSQTw)j+dRvPTB(M&!qr`OXlm2{V}?&|nHsw>Yvp0gk8!y^3G0LBAsx~z1m!w^6V@j*gtLX!PRag1G78lc4ZX}Bl+uiN6 zZVwn-Pp9h>@1l;G#QUdNaZ$c zSx#*UU8Lfpu9>@pROo25S`+3xtMv^`zZHa4$X3JhiLN@w2D>$0Kkp67(`?}J311ymJw13c> zF;rEuZ!i~G3iM$$DJuqb9E-NTE@^Fq#H<=NQ~0QzuYZZ5DjLr{TOS@-%S0>jS^kJYViGCPPod-6x;%va#RgRybzViz0sP7&F{;YY~#Fi?>=%*)AjhJHwq zr+2xzBl1E83{3_-yjDLb@{oAl>iQrHY7%iErj^lvB1)2|@SobIWL@AfGHoq&5&fX) zQ_9g2xN_#D?gIzUvPGw%=f6q#XQV>m%Hi zp*isHlKE$1RQ{-fE@>h9FJdL+vDssLy@ajKY5dWFPaf-Fne~UyW`_lu1a5U<^{uEt zIE9OKEuf9MNufm#y{&B^6=499pJ(aBhShTGY|OA5Ri|ct7X&Oa27D4D@~~fn<5JY; zlN{;&Hf$`D6;wWFl1!3bN5{bk2|Sp{z@;^F^uT+6&-z&8V0ol1aSBw$7}*Hyquc(D zy?4RLX|F@jFx=e%c&F4_W!c?J|1CQqjQ%H(E^^|AdLIs@|l-7u`mm16P4at zRh_nkhSz)S;?M-5(em@Fv9R0Xlu@f{?5J__TsIq97XT_9fX8rvWG=+ zNt95N(krY z9z)8Ri}Q%~n#nJ^5nrmsAStkd1%wqjewO|A5G6BYU2QQzPK=>5M3r}E;Q`Uah&|h- zFMl0jpGYt)X^o1f5bO494!j{OQ7$Qr+nVbiQSQYjBO*d?R;oo^)~W@oK>HEg zB^etmZK*=x_$>AWXN_xp=Q$q|9!G59=x0Fr@i{;j9k@2k3TRMgp0_l`1Id>U{{*P; z(&UhLvmv`mt!^Z;pn+om9#@w}soH%#vI~43g9Yyy=PH=@jX1G?LF@~N9Ywq3+(&M- z>m;A)zhXmDg{{|J#;RX@b8lpF>YCvQ|9{x}#=ttat?S0NZ8eSUG&Y;0jn&w;?S?zH zn#Q(m+je%0o#e~8_n!0W`@6aDthMGEW6UvWZfbBQ7)kC#@LOLoQbFC7K`1r;TP<$e z;Ax!BGF;%F&Fz!kr2?h7*%c+Y5%=|XA_IS&j4D7r$r4`$d7@`K!1uI8KUDT4V)~$( zHR+$gT4UX_ia5A!5FkFU3 zXGkyMrp&bN{IkRQQh31!Wf_FDUkYOjtN=t1s(=icrzw5^j8C3q08+{Zr|VIonIZ2W zCzqz-&{sRC4Q?GAHzT=xudQh2v-;33B;SOX%fc83+r??hEg|OcB;KAjXWI0TY<}+* zGqYb6?Y-_BA~*2ul0Gg^Q{Q)P1ekLLCL;w%JGs96*jtbvNoiTu_%Zdp)uz2xDEZW% z1pzGgZUo0*$rPz35{f#CZVoMb`Bxhnj2JP#>l_M#M*quw@2WSG6aisnS8doeM1UQi z5O7d!CK-6xcSUlEgsL1xrvE7;lUt`$DK=LwtnubXt^Xxg`CU<`aY-!BVej)#*wQ!L z&>z(c8jwJJ!DxF}#JyQQW)heF9&{fCZ`@VCPR~m;YM_16Td0=U%nm#-f?_|Y2+emG zV!LAXw_`yYl}`b;N=))4DJcMs@uGdFayK@c?*{So=J4%J%%gFr7K1K^-&AAurMSF2 zNxkaYY&wtX7C30>_O*HRx?D@EZfx0b2w2lwP!>_nO+qD*_M zm}KE{pxnme&yd&bOe>Go;e=Ic&Sm)>ft*yLiMP**6eu-`R4D8{%+2V&YrWBS;epa- zJ{x96CI}*dlOA1{6?`_uNq>I!5&zLj-mt1us%*RW(9rdVtJtG)kKF-AUfQ};wKSBL z{u&<`OdY1|(fRsx5xpxc_=pk@!)74&29%%rh65F#(7dxe0~HMkW%pz{c+TrXol1i`aep3U3bip#()rRLOg(Zf1bwrzm4R z$n0FwQ@NA1f&|$tzMU%UaJKyEC@?5`1S+5uzTURZ{Q3=j7=G(OiK9Mco?X}$c|IO$H96b%X2 zFE4wj?zv*LaFH(V%G*FQ&(j=(rm@v38d25enwm#tVq z-{-gLfxsvNx8d&&m1nNimwIDtf}ksivI0LgW6&~M7m;02gx)b4IIr!6c8{-I46wbI zL5_dZ>xGC7E+ImSuvv1q132!>-pI_q_psd^14_OIKuLT zcYS_>J90vf?i0%0DiH!&^6f&Zdgk~#aoiLcfkbg(4YQ8BB+Cjmi3#U?^gnl3C3SK#!>}F z%_!{14?i+x#^Krk0NIQmRu-`1C5b3(9q%{mm($-kk0IR|L+-Ke5a(pdS7UI}%05QRG4<=w`$i^%o z2T#N&EEGh^#3z*CzBO5xjIx7M{#rs3MtK6$-7ze^J9tA^dNU>v_DSj!d!*SU8nyTo zXd<0$HAqwxiZ$9h9Ca)Fs+nJx6kS(uQ~#MHlCH~h;>#dA)~43Ft6wdw z1)QAL2ptf-fTqah?Bduh&Mtjst=>u0>``_xb8iM7Ae8 zx@Cyi?No(ZO8jQi{1uXtFW1yQ1T_=o5g^5=yQL2&Zw(h~bXGo2pz`y+P^M!D<~`14 zF{^z7u;bK)LNh1QOMBOg70KL%@FHFKR)kLGOF1@|unK?t+th5e89IY23+&6#Rw~$i zNg&S?AY2IYuA8ozI}Ub# z*Do@7vExmK`wbUytxc|Cg2X(3f1j61Zsaf^;40qk(be-E(t*W%naCqaWR1VCw?zh! ze0kgXads$X!SPd9Xk`m|zkk1!U&VXgNb(HdjUfY8RiL|=U+TT@VTccoX9z4l&DVZ* zD9i-#=K&vC)SA&@b4FVg$ykdwEU7EaQzU z>Dhp*6SSm#HPhs5XZ2>Rr*TsHkS}EL#*Ld3Xg-N)Z!9j@`(LAYi@9rU;ko} zudh>{jrK;Q(1iFp^r3{p_UP<;lbC=R-_}o|0u0!rhS*Ty-18V7QKHCl_aT^2wavN; z5K9quL@k5urrCQ@0=Q8N2Bl|6&!}zP8~}+%hvB5aP=Wx@I=uJlE12rhPy$bUw8?o? z=pDciNb`MBSMA(YMd2`jOho&KCwwvS#{B5U~BbMYA zKT+nTiqeN92A`s6rYVLJO}{U#-Hc*bH?n{;#6QuQAUWHRhbfp;E*%nxMj>^;UR3h*p-kMq`>$wCBvrx-lpvsTz;AahCo|omT-1Q=xD=B?@186GiZ{ zb>gE#eJ?0EHk?*UUTH-b>f__CX0L5McFfAAmKyIA@?XveZx|=NXa`g@=rk&JY)^1l z-#lu(5`^A=Ief^G*f4$T3W)$xJ6&oshD#O^ECPas zkl^-Cu#lW>Zu5YtitxSgmYv0)usSej4t%)_NuVfUD^h$2g%dr=T{pNtgNx>zo}Flh z9k;ZHG`)s&qz`7Jj^eFv!8KL(6e#doxD}inxc~WQnttp(MPVP(crPP`3P2}hk4l4Y z48fN{513MoZXP8LUpol+>ovJS5+Q4{OFi$mit!=QF}h)Z&_^3E5LKp zQVlcV!g!C@J=CsG!x=6l8aW9wtS?aPIe?k%dGf$f?)}%(wX4lJg#{DfgWykQa$#sw zPs7hddbgDOw%!RUr;7N#ZEbPkeJM>gA!IRAS6v_)TJ;BRou^eaG{sR3kTb|xfFyS^ z=C!TMI&`-{?~6rPgT~Iv2kq@gtkj$bD5Pdju{zam(Jz^EQexz#si2k&^M+ep!n=v1i zNQU92J)mmn@VP$u@*Fx4ksU71(`%J2qwj+>d7mjyTsi@Cr@4{l;E<_gQ6jbDyq9P& z2z?gQMH2xcw?D}29v44)_MnfmVO~zXm>sWQ$##*@EJSc#f3(jUf)A94Qe)*?)L+mU zbU&!sLA2>?`NW7>%`Ni6ce~-L8_{W7au$~B7P_%&Ptk()9(EOkfY{G>E2!OK^A_RD zB?WQ|+9C3|q(Q+bj@`-TH%9km321}^b}qmlDv2`51=*5*X&}niZ7$tZ3;je)I4ziW zeNAex1rX(W1p8%&G4K$x!wlf&wQt#x!4F-6wv-PP7BL9{K|evSJMGUW#h!-_X$n4z z0#oeCH1M=^9=?hqKx}Mb?+`dy7GpJPQ{_2UJ|ZwWsDUycjB$ur4Z$^td=bU2Y^7mE zE-q0(P?F6u+K0N?LfzsO0oIG{q}WX-kG&=EU{sF_0}5*nNq~v=nE&yn5$W^DR7&^MGQO^s z9dKTn7^8y#{(zaKCCy2aogv?F!wqGBP)3Url37A=@}(hzH`Ezn8@@Ij3qtPp$r1aDu@|7M!*|U!y2F=Quq6w}$8#$<|B~eO6@{PT`Gw+A z!Cm3@VNtv%3T?~CXzuioL}wnw#h!2M1;v*_;NAb5MF0V!UUqY@{=g?Y8W@* zKS}6+V)A;K*&}L-Wq)FRYC;Rp@G3yWJZkx{Tv`CEnLS1LF471(fsSuXH{dQ>$+xbW z01puzzL2Fk4}V@J<{xZP89$BoQ*SLiRCf46p!|BeZ|v}WcG5h27G9IBVv*?h`4`d_wue}Op3e(p1oOO#e$(3Bo0A_{pCumyUUuGt#}$&g19O3@@fg~a+vLqHi0aB(c2SP3~`I@$dg!i>P8uXQSGYO2WOZZoi)KvBU?^`m^n) zfTq+)AA97>pnL4;%i*kSdh`@YVq=~_G5*j_H5lt9m2ef5TWFM(g>H8 zGGsX7mKh?H1D2)|l2vI=FdBJa0{TXcH-`N&jP%pwwm5s2AiIOVT0ql!ZIK0G{Y-1R z3haVdzRKZXe6tL0MS&QxA7!Ka;~7L&pB|W8`8YZSkBAV3L`;Kgv+x)%#ryWLmN$tP z9;Y4@^`{j#J3$+9n|!aYESK^)DF5lt^A2cs>=Ni1_QaDY_DIx)89}46)Ul|m^|d@H z6hKqQ^R{4H41@)-1SQUr*INi%8$SmT&^J$X2|F6!foZI6n*SE%-`-qUz=pNWJ*dev ziSzH}G5=tlh1f3iXxiCuGjS>s@va=nha87ob$GZj-i0BhL$t(L4EO1+Sps=bf6PD< zLo;mlEF80gvEr&i@WX%!b!%3M!2=WcA@wj!lE};{*DeU8BiT{;6d5+V#ekRiIIPJc zgYa3NczV=wgV}BV`z#K?>BCsbFhojXn(K5l-XhAFj|(nn_%a?beX2U|lEnM{2Y&bZ!QsUG405 z!N`1^c(kyzF5Erm`9NL%;ky1#&)UKl*H`zs7Ow|ci9=CQQ>E7BRGO?$lIPp;5P+>!ax{@;@z|05 zL1~^|mKYtjM(5&PdrW06u5Qu*UkT;aP0#yZBBfhq@N-aeQbU9K+gXI8iklbsm~{dz z85vmjyN=~HI%Vrg6y6PBK}m7+$nsN3Hn!t`bT zYba>JQy{moPi35s+uKCq%0q%0qxdu9w=MUbnD(`PU&=86T&lH}S*{Sd$kG!kTb zm~J`4qR0Q%jYV|ri+x&AWNE$o3}aBkgt@I0!z06_>;i|z;m@5RfkJi-Uc>a*{2~Mq z&B8nJiLbuCXuvxhe(D9MXyyzkUuS4ak;$DdTs%RoqGOce(9T95x^u!vdwPxlA-nqD z)Po<`xg8SOlzGQFUPbrg1&!i$FBIR6FLmY?p@_IMikVwe#YmjePPhaB`H&dq-Dzzi z+ELCS2y7R_zfy=jEFy$z+E*`LBvVg7wC^SEG=A6oBpAU-ki|dk{a!NB?d=oQ+tEAf z{Nl@)TK+%u{@;Lo*L;v4Gr~6E0r4J2x)6j|ovgewN4?zXN%$vT{P%XqjqlFFo9S*~ zTsyoG4o!wB8BuQdDcMq)&PF9ap91H8yu(gEy(RwO3Cgm;Q#{XpcRh)kEQ|pB^(0ICmo>uwFjM@nehO%Y4WoGs{q+e=eu#wkEFFxgUHd2O&`7|y)nk39n%f!- z@&^U4!AYv${1PjCE2YUZROx_<*!4JR*RhGBhpfnA6g?gJ(^FaVpRz>d=G+pJ1oWl) zH=*AeY_X`yJ=1wlEK7DzeHHC#VpZ3GyIK2dHEs<&;-}iq(WZ}uZf~O+S$S#C(7bB) zF@2e+B#N8QwKOHU)u}Pr6|f^6W^;CF0?lzTiJ8dPDFm$*c8Y|2@o^@`K8zgf9I85^ z31S=?e8@Z7??>D2PJ3>4Z$?X(8|gSR2KTn+)(9iLrzU4rUrm&AOADh1s#`NjE=*ij zRhW%GQB`xc`zbmzm}wqT3aD+C}qWGNMT4K=WaK@mNw+3YObsp z$z*5#lGzPGDcTNnlxyJ~;Exw8Q#4yK1Lh}@FnF7}Q+FCDcwP+!H_-gzI>@)Bjv34U zHc;`qfs8iNYg-&$PR70rhMk*O)$H+M^B{2&C&& zB#5!Xy=;>`EMJ$^MVfrgUh!|?qwuCUsnFJ-C*@Nb&a*3(HIl zdv}E$J5(Oh`E8mjRg5(|DP5?yBjNO*h*hc-f6k$HMDSK~lW1_?0G=`;s=Sx#Q?}BZ zGq07}7jNEppRfoUT{)~mwL14ZL5nTb3Y(@~K^c!@9_nUm%tc*UqFCSXqt$mYpx)ai zX5;y~jk}QjgZ%CN$+%P1o>*g*Qml~2k;{iGUf#Rc{oNGl&@0|1lk3;Q8LvpITaWFi^X#C? zaq11Bx~XUDMX6E9%nZ-&@z@i-jI>Nazv|$WCCOz`{D*Hfq1>2|?~7N!GAHa165GJE zBYng4vE#qRT+>ew#~0$NB^{+7VlDzx?CIwnql3<2(mlmGkVu}M!&7xKm&p-ni{-X4 z^=DIb^;Yr6z4MqTm)!8s2OlBuwyTbz|;p4eEe%Z|z7J;fqis3f)hbiP&X z-b|I5-5SvLmZFU~)HZlZdfJLEO zBAM2=n10nzgENy0yYCo6E8YYj*odTu6@aZh_Llcj_UPE=$&@yhWt|Z-45dQ;yfuaGgZN{2oo>hd7g=1YV&c@ z^avEJnNZdTry-_k0WY7;JOIg*A=r%rhLoc5gmeMwmo%?*$=)Hiiy@yd#IYixg$C;|np@z2JHu>=e^(NP+Fji<%7 zf7l?g(gs2WIT9pT@aVUL{r+0;$3>3P)y29hmTyn+NMH zp~&6ty2*ZDo$~$~Q8*kF5FkrW>*P&WQH`Xn=ZMvuB+5OxuC22i<+SiG8ryE%!*%#j zdO23zk4r0*rv;4bh>j9WF-3N~)C(2d@!d~rQ#($5zTmfu^0^yv;MGzS)q9`GXRo(Q zB$Mp~9d;I63cSLf?_5<=TpyRd*pdONd)8dm8`^JA7d3)RE;j_g50%kmRv6xyqL*>ilJ-aSH;Hex&89#vs93C*-?PDK^b6v*Sh* z-mf&D%aESLo^LpiBxDiK9bwY_R(Fa8t3TvlK87ONbN8$J-ALmL!0XR9==}ohgKlr} z3Hk?e-?+(B)U5>chiK%ozpDG*uHY|var`C!GMM-rKajS-05f8Sf$=oC&a2lp&Y?*( z`=d;!Thj%$&PNvkjCyJ?J8!!g5*=j*YIc77CHBUR7b_9{?g?~`uq1YmD6e0;C&T!H zwE){I_gM5pe1M(6WK60tX%#X&@iNCJ)rlg(2N0!k!{)k|m|rg00+k?9&FS>%!%&2C z4Zk@BM_wPgoi{)No!bjIA%1)CEZ~$}-(A9V0x|3K9hz|O5ONYsZjKBCA!G`C!RdaN zliQsn*T{;)?gb?M4VO9mEx#3K_EO`rpcdzZykXGz2uSmnEbMAXrv0A>OA!?;G)cds z0P+^&EJT(8pNN*+crGC*Vnvm_RYUS-PUn43BJ9X5frCfy`=j;qIT6F$V>Dzwn7p)n z{x%N<9hD_1$DpecA$5bcAnkx(i`ZwyU5eQ6X!wh`?%CMX0k5oVW(x)Ow`T+-GdUSn z9!CwQOBwYUx?ZkG?|Slha>S|tz*|UfY;xwdEoR}=yW0ua@5A;KA<%kyNLzPC{qF|B zKh!K)EN3Z6-7cpwnVx;ngSbsb@5;aV_2i8t_L}K#9AT!$5losM)Hs~$tZh@mH+t|e zj=QQ^pY>l|JRTcPkBgvYkB>Ek>~{5#VeaqB*ER~FAe3qGSpf13nQ#L4P=?n?H*#oA zEPtS{-?}XptR!S&**#?^aA!iigO-m=*=-HMtc?=XDqn-9Pbi$@%Kjw_Q}?Bltw{eq z>%KcCLOpFS$GRFtM=!-aL+l?b~NI!eZe6ynziS;C>Ij z-I?;C{n;rlrcM>eIYNlrn{7L)^16Am@lGY- zj=@gTO>v}^sTszGU=EGb>=EP!ugOP7%M_aI+X$IE#ECGXe{}2;3$dnbY}Rbj1%^Ag ztCf=#np?g3uC_^Az4;^}XO$GB27e?^3VAxVvxENZwB**n>N$022T7NtX+l3o@W$Dp2Z1=)!3KN!rh|S;V2RAHs~ZT zb5!8_Vg==VVOum1fGu;GArp#%J$vM?T5{fn)^h3?JH_?>e0rcnNEkJXy(o<*Z~tvE zTTKi}tz*Q)a^6I<-n}uLFJbs7(LVo3TvIeuMzLDjEOPqFX4ADVNK`eAt3>E^7sc18 zu7HYW)O_V}I;cIv=U(Ocw^G!viR$BXVd_cplF_=nX$Q;u7I?665`I7GC}M?9!=7-s z`w0li==%o;45dcavrlB5kq;Ik1R>sqsB%MOoT+})b-@p!*`~mJnR@$(CGgOZSjq7_2{#hpQq|Zq1eYBA;B+W_Xr@L?05dsSd+R21tDKFY!@_USmnpl*kjjy3_3+_dbO7!46HcRi!+#`%j`*V?ma&I0{k;=?Pfa+4w#o9~)?BtiIWAx%;$72=2how;Jb?w_wMS2MgQ8R>t*`QA-Da9(6;ELIO zxCa*4xS5CuxDJ;oJi~!#n6OlCW@fjiNUr=HJuC9rBdPi{q_T}3^RQX8ehaK27}|%M zaB3e?JY@M1lq>-bV|!v`U8jd@K1(~4a+4|WTdm{_wI({wqS@23{cB&WddKGV)P?8w zmxfSj*n7nt9{m1-J)ecWZbS@9cyQ}OvW2LxA7R=gfIYCSOAG^@vF#CH1W$xyn;;Dk znm!5hXm4`ho3Cm9Lnso&E9g83F6xo9e?!kbj-x4S29MzmG6X{Ia3F7^lo!G(YzFgt z%zgo~YW99?^E{PTPsl?GTz#8@)enTl)jw^I@$yp#Iu1lQ(KZGjt}W_z#T||QB^*0w zKn37eE_uXNrMO=Nfg2e{3Utvf%97U)(hGvfw|8Lz?Vf)B$NTT&65Nbx=N2}`*NQBM zBVJ4J5>eJ*vl`=g+Rj+Evijnqh!)-0~jKbAd`7ABFlz(y` z^|ikf1o%85zE}~Q%v@-PiKK5mcEaE(rv!~r(Qv=@8lG!TzRsk@9tnLv$(gMhj;YwA z?}M=HSu&^nK*Es!lFXP#aG-D_FPv*Vx<6UE6cBFv{>O>a-T8ig*`R?SyQ<58hm=}` z9W7?kW9x=N^eIv8>}L=nf|m?bQ6c&E%x)C{VOXHI|I%)#XjB6*O9>zqvoqNnlwK7& zsyKqs9uj%k2(mjC9raJ8g>_>|$3n>MHt!~-nK}wTzII0-*z<2<2L3q^3h2Ogzq-*YUn!ifU{i7RqkJc>b@{vwts}ozVZx!zPjN^kx#Am zbIiy_d!meu-s|7Ogd_2qpv8w4Sl1m_{5wr8EX41Jp9?GuwP1)ZX>=`fbSf_$7zeHP zov)iF0pNHkh^_7i>yLv{JB>JLvvt?tM~8XmK4qwz%=??+xdAABVZu7)21MHYhV-31ke|#IxellnCs}~teAm{=nq5>QO@A+cA^1(C-V1j) zThIgN^}amG%T5{H`>o^W0UPb6?}1t@VL*EkCic{;EDnKcb4!Rs8i)O#ClfQfKUc1= zsck)SO+uCogR#V4v(xyJT&mo!;05EP#CC?f3DPgl1>K^QIN7t_foGO=4hcr+;oQgO zrFDLaz5TkZ(_f@DE9vjo8FoaKKbM`1`O^)YIj!Ihw6L8Wa>SvGF%?G>$=H@NapKE} z8uCN~P-nzV!W+vWQKI^z3lZnZ zU?!$FAaCE4JO~v2&G#>jXl&lub9Wntu{j*ZQEZdIH*G z31Pr6ZYvQ2j*~NZa5xC}#d}sO5OzGI>v#<>&!xN|5*4StO?P!~B)YQH<9ISk2qME= zh>G8MO(rJjCVZ}i1*R{#mGMGfgaccg6woqxG)9YR6$IL@gF~>LbvR7}rbQrLc&rYr z5ZF(ASyk|{xS2jYeqvg0rTjYZU3a`Un%*J9s7;kwv^2-+yV_rt~lQpM~ob|qX6cHUPyy=IgK%{nexF(i{Z>a_pl zTCZ`#*5UG$LH&gkVXS6Mk*$zEVvM!41n;Bsuq(2Wm@-3Vu?kWetl1{=pBub9aZ-+1LK1#~z}IrB5M`{EB>`!J{~k&(Y4FN9uxSutxBj zyRU=vW+z8p!zElYnZ6|Pdg&DdVMCt ziW{8SS^MpsKK3WSX)e=a{bKbHTdq}8{?5L&dqV5t2WOwnO#{T#mPH1q!S{XWb{bD{ z?yeW5Wt-$+0TY}na<|p%h5SHATcXCG-H3PV@^YuoHdEq@e=K%rPyXP^*?SJop41uK zw$uH%?Ot=A%Ub$+{-ce=hSXBxREsu7{_Cz5lhSp}eqz%-Ltb*lq_nox_7Yp_a8V5o z;L8fDo3Id@f6}Gxiox_6#y}J^#>h@@;dI{piXHzMgnKq2^?_mD5=K%mHj3uw4=v6C zTn$XZ))a5}4~fO^(Hy3}z}^p(&$;QDtn+I^>?=6sv{cmtHMZL5 zZ68)o$4K)v6(`%IhuS@0teI!8cqm$R<6I*nl`<+FKocr#0~7THf10R7Qj`? z_?J19bhsCW_V8(I1)Op|=vuQ^lwG{GQCGe^m%q5y5?E5T;>PN62N0ihE6TGcd$=}J zNA-FQ0M^n4N)_59o9(JEOs`FS7UcxBfj$fCXJCF0)#OAQVA};rdI_v(dGMpuS*lk6 z34urHfrozYl^Zd8_)wIS@F1|6e;apykOuzW(U$6_R#+D|7iEKkgVAai=jW9*TbwII z9aSl)XegNY*`rga-$TQ2L4!ki*h>Be2fhFpd~P_$Uy5Tsz^UbPtk?}ZmGJnwa05F> z5#&3Lq$x$MikZ%z?eeC(1mTaQLqQ$Ud#$IjW{K-I30iKG+M)venU6eF>{qfIG{+vN zHgta?yZ11T%Zc84BiEj%gQ_1KiVz0CkR4Jjz;dZx#w#%B2#PCY(7vWZ3|cGgU~Ytz zZI(sYo!5@kBCo@#Nib9?SgbjE>3u+K|7n0YB8Tb_=&DtiBB}I6O=5MKQpDR7aIPd$=Ha& z4lp_Q^*z93aV1HSoq7)lWQ0vYssj)lu{=#|ZE%2Wrz0o&Sg(~pGVaEW|3l~ak)v{e zlcxQ#nnuXYalxhXbMHgr+y6c01S!lKeTU%<4At37kp3Oc1<`Zd*e{-`h4f#&`^UBc zl?_~uozMa;#FrK<8gKqM6O3}!8pvK`W0JjQXUWYS7+@3>r5WYq;VP%~e+?gI3BaiF zkxyZJg%bS~E+vnadE?|oxdVrbla?nL`&Bj6$>Vm4as@a^wN?k}$HU-#}J9`J4uMa&#H z?>NbiZ{xkJf#du-(2Bz+E~6agO5#gN(F>D`VZT4Csr>zo|7rm#<;SRjpjn!>G#{vu zLzwIRLd0xB#M5X-#*tlyITjRLV-eBBGlI&4;Tj`C{twA2j24CjMvb$DlN?yMIX*M~ zF?s{g$940ZVzb&59CP^a*r$nAzE)x*Mdp96fqy0}2R2Bu>j10!o`LIK2^^oe11||Z zMzm#@=(I$vhG*?KS7=Yn{C_Z$|3(@7anbD9BK6<=^FICz_NY#+aosL6^b1mbc}9E) zv)s-sAs6WHay?Gq-keT zQDk=yiC&#|f&45#F7T-q<`=~2){8a3JH6FSZ4~?kj1n$}# z*4&OHV3#P*0%KXb}l|SUW^xXU!YY$)3vje)!Yh9 zM1Q#SkafC>*-vR9B3?G(6khbwI~npjq{l_h5H?d~72A}SX9aMh@!$W8UzPKP{9=gq zA@Y4gv$cn?!=cKVeW#PK$|cLTp%UdDj|gm*Y0HP*ofet3hx_;ueWwTo}U0vYiLSCbkCBPxvAar=P z-#!RA2#uWteCIh&g6Zz%_a-1j1aFikTyu1qlf+9+#R#~pu*R$Nl&5aLcjG^IcOhxt z)RMeJyqjwE=5Q)1PJ&0i$bDw>q&w~!WuIFYx9+?dO!R2ZQXx1MU1)Tlm5>Qj%&EAz zh&xUQ2dx-_lgCij$&+LzugY`3grp@^|nyli;rsXUe)>;nB z^rH^sJ z;SE&cev%LaN7BLJL_XmJ3f>+odANpqw|o0-`^m!yiMHP72N*X96tD4sm%!>^Q;4zn zc=S@Pq36q_i{QpqfyWI9EwASFNd^@HHa1qX`!m9xc~Bu`v6Amp6USW+69q+w{p5N< zYh|s;&)Jdv(USd{pRTtYD7M<2$LB|&)0*=I(BF%^OY9@agJSKu>CVW= z2|5= z(=5ccHkeL*Uv=SizD8<^^NAS|td3+{)X5cnnNZq1!d>^8d`-Na+ub7|%H`JMLh?OM z|MvRwo&^7X>pQg;u{faS`eNNU6B>%FDr&UbRJRr8dC`&(pOA38e5ePj0Pn7+6sP!? zO9a4fFo|AnOoR_Sk>zQv<|i8wkH;5d_lpMdj4nM+RG~UJy_)-b7mw>>)6^x+1iQu< zI@E&wt6%vC>J_an7di7~>hN&yTU()E=D9ZN72g%+IqIW+2$gFpOB|M2mQ)MH%`wu` z<+@y)J|WEKM&y8k5rUbWoPn{>357H9hP*!4?DeirVS>+fMyuF((t^}vqshrL+xh!W z^^p(vHE#Rae6GmlTI~@bAt6D1Urep04^CEihe<%&YPx@(b?*i>H~%Ugc7BosDI&zm zFLI=);|-C?=leAc`r55`fkA}7)zhQIZrE_(8s5m`^KU1nw)IySMo&t47H5XJWk?8-T1M0xbakhRaxtiS!Gji(b>e5 zmyZb_;md>iCk7ya0w=|ju!@x%g*Bm)3-j9m4zK=&@|YA*=EDF+Abdw_<+l@w9oT*^BktUf)Nw{p!LUgjlbv4Ifjcq!g0u>z1Mx z;fFx6Wkaa?kH{=OJTL@Q-x7XE`z3dX0`HZ#ItY7Ax`5+GcBs*nN4eLLJznH?`Tb_~ zcK7{o@}sb4Vo3AFC1^i;mMIHF^)%qc9uXLhFKK>zhZbRrREIpEQaRmkfk=Il@M~d# z^SN6HW%{j4?zHdw3}LLi&0$`Igku>M1sP#GM)d{$X+=@`l=O z(GMRH;s=ezlDj&yL;kz{yCM#t&5+P~qbZA_1qvCA!KjC4Ozadwmk@W~wAEl8pZd>F zhgkUb>lAD(WlJr-%-`G;=k(jp!Xp+ZjMuo%NGj5IjFak)j0xqN^qs1|h-ewLMKpN% zf(!CrBi-yprH=X|yz{I!o)<(9*g(o+QTE?_tD8b2Ry`ivhPtr!of08tQY<-KH{*$Oj%x^M+n7!oO!gk z0m&sv7OJ{36BK5GATZ1|*ptARYAMcPVa zo%4-^jH0X&{Z^dmn@5A0QTGTebB`;ruMGm>Oul(69gXIz)ZNYEQ0?o*Gpd6A7%#vN z{~8fI>{3?*+(#vJ8Hq3D^Ai`^9!9c#+`RV-N|j~pe#2Q3G2Eq~3kyelpAl4~FX=T` zRqzUfuC<(k0{M4-GXGA33za;C-e4R^!iwK!dIU_IO;8t;qaVl7#GfBEanmas0}DGA z-6(rP0s_$rSq<1U>Xq7LMxU_VV6l0bFPPt4Y&Jg^xP!Gu&;Z~0QkJZVf!&OULpY@h z1(TEG`@sn!GP^AN%*?b1%mYLlWkN%h|4rpahWcq+A8b%~sw3&sBg_Sjm~i#rK|91H z8=hMNYid{b&nt|^b(vPz^8(i;aLls0y1FSn@5`5J6M|x)^m2ryUUuc?zL%t;JfiM9 z#Ri@3{pL#+`=G6!w(eBaR%cXBjGr`FY~1!3#<`a@WgD)IAlGNxR_Ky<#pTz;T*VJ6 zQk+MYmdcV5Y2e0E*X;>nL0zu(@b)T*CAz!@C^j7U3(kx`J1_m$F9lgy+2iuYO@oIX ztLkq?Jb)*6S=ozeE_k zA3h;)SV7s`$UXF}iVO3(PeQ$kNEdL5kVNu2x}lg&$5X9sxCOj8i>qN>CfsAJDeXfK z8lV}xuFSI!UCi&(C|o}4`{&6wk@yURAzkM6!*#rGSM z*-oK11YHB6OPk+8DGWL?t~VQB)8iI9HnKbhEzf>yvqp;krjw&6@XYM}fbtf$Zky_Ny$Q?Y zX^hyXe3|i82EXne+hgAKYKn~4;4;1UhQV-g(EGB6##S|fkUgLS;2r&wzch2)vrG5+ zY1fLXeI#fLJqZ&h5{d(pQP){focD`SZB0#P-c}mIgDS<+sRb-8GJF6BF#;a|vUjJo z?ax^nhSDsyHjDZ~`46f{jQRQc_{5PuV(>*3iYlqx=*NjBs&76C$UD`E56J;J-8*{? zVfdL_mIc=V&h{kILTS$w`>u;HWlUk0R%}E%4O$Q>5DrO~a2K)rQuJj=gzih@%Xxjb`ZUp}mCON9e z4TD>F)A6n8yJ!s$QAN+Cee;1IMO1n}=mi-ZKE-%TF!KHKQ>u8Y_p6|bMivDE`&7S= z1;x^##eHeO9}y;ipT|5LYg-D#4C;3kf~PJfehRm~txpwFuPoRW zS#uQ}_%2{9f<3kMyQZe5538~f$Hv*-3Tr&4kbm08a*|jO3N5)5^Z6R5leQS+EneD0 z-2{6bIIK8jDs=Nn;4PAdE~^}B2x3w6$mbX#HtZsy(@Rsm$hP$k+ujruHyp-OeT(PP zJhlTjdfvD$5%_=@o=!#G@O|F*Zhp~Cg!K)Tpvm3)B6srIJh5HG#~#1fo<<^j2M_bR zQ}s3R8Vcb6tJ?W>9|dH{(V{{FJCl+2WG0`Y9S6hqFbJcq6CGTZ!qOqje`cVA`%Vj&v`A#yvF2}o z(RyXjB=liaH)GJy)#HshvuJz1*x$r)kzIAQ?KxvxoQf{G&t*RJ@)jBzZ0wiecNB28 zrJrYqUfQPQPJS$@>RSLR61??%O`CU+vB&boAtU{|gcHf@VU0kpUCeYtQjk|8qn&0j zZn>)pAlsg?e`cY?Zxd(f3)LW)uAq>^t+t)}o;y82bheW=-g)?)J6v6uQ8|PxW#~{4 zh-2iY&HJur@TFg>*N{|UPrMbVh2RkTTEi`X`%-M6BaROli6dd>!GUZ5k6p&iAfo-h+2JLF$halD935W~eGazjBPV zNwqpdU$*d9JO6M0dB_s|o{RWL7R&NLc#_bo-h1CdyMhmt zL~UF}5`D@|UxCgSZQqAp-<(LR+@QMu$J|^0Rh>rr+X5TuPU(=6Mi8XC8w8|N8a7CG zcXxM6cXxMpgLHSpcRMrA%sF#j&(F^vpzKfI_qx{gUgv_aXf+QHZ)*e2yL}$N*xsa2 z(XFd?8f34!-_N;YxW4ZsvmV-!0&_aSoeKX#tt|buDXXVz+I$g$y8Xe{)Yrpexty=K zYrC(cE6VL@$~{IeYNYE^4%#6+d|-s^JHTuw1dmH#wF414Ini~?z*TH3$wSo<7|#A_ zh9jhe(R1wzbNBo24(~lbB-2f#*{^P}yDYyYM4+gGq_qt}?TsRrRokPn05>lt;dz>n znQz48*r(Bj`$$7Xu^@%)1#>|u6GrhN`I80moK98Qa8XLECd`gY6r~BdHN_=ypcDj_3Jk`uXa>GpU7q$iu6aM_zgtO z4Df~)$6M*b-XflgTA}OT^)=5|6=>|rjuy<$fnkNLyDsS@WH8785VE#@ljYqK4H}NY zq~=7&c6#@{EQ7v(2+jVh0CyzeqlNb89zyaQ;bFH1k+vrxG%_??Nmo%o^_>U4_X=4pvjBr5 z^Uo=#y{DY}cMV%)D{1g>o>frZ=*U|lYbtme0~=kws!_&1Ls#e7c-Le!Ue4^lvj8T6 zoK3gqTsQ7v6^F;zG8Rzt2CRX%9^Wd%nz$I1_6xR%a3x8CgM)m=+gXsT(pzr{J`Z4b zZ-p)K5IbL{Ii5IDXw~zgMCYuS8QfS=ynXx7le-iV2y|RNFHh@|!k&HV3C4u6^0}UJ z&8nPWyk`I_D=aaX+vKmv+K6R3iO#49M=on?2P{-fCJYmovOPi4wa#PNH`K(g&@3F+ zRIBs~LPduGD~qdrVCPfO^QSu>km|&X)e~CMfL1j6SVAh%{vv)ov6%Ubh}skWWb$tH zQ9Gkut7GWw&L>_Gh$_5{hzOa!rgYF^EmBdWu~1XEr{_bea@{(dY7iN z99CHb=*4E}v;?S|g`azBRs<=hQsR28QPh)c)d&bTKj>Xu{i|3=jJ*D;R`P1_d1Qy| z#F3{HjU+>b_>(`fZ-JdUI^t^kPdI-Ok7E9HO=vh?RvGDGP;t}p=E3Fk(OsYeZKvia zDZm=I?!RF9rQMmY!bDR+A@~Tfs@4LW$Esbhzz$&I+nPLR=ePkjw=N5grC+~jvDPYe zQ_lT7TveNNtQ^YWtV;%SBTyIn9;UR83A&x`%+hOCMuEJPHDow@6uve=W3wwDjZK29 z>(a=b@6(t3F$&dJBEolrnB#(O*W%~jvZDBcnwF)rslHQLv|G#A#2X~(iP``kY5H`i zp=e^u6S`<0cf&!ql3Tjt31s8psLZ4?p|j!xOjNi)a285E{nm3FQiUlSV75in-z2e? zA4^At^BgJ*nj|0^xaM$4OTIk|9;^oCNJIp5zkhwHiTDz+t5ADABTiP^o0c{5loVQX zbm2&|T;!=vw85+x@df$A=RIGz&fDwx^Jv7c%m&HSS0ipMEJr2laa_)87)&1acB*Ah z^F;NV&*_E^?xgRNp0=CAu}(^w?9Adhf!UlLt}G`pL(Z`A-C|#{ff)Pm0Et{yKfI96ze+b*%##PZuzh(KEPxy_ zaDkR@gyCFq^wVa~8wgHM%JrlH;kOckbVb~b>Uykyt1gkUx^Qb(9B9`AT2W!?(}-Hs zA_2On21wN7l-<>5jAZ}e5?(0UH-WE72rw8Xo!|4>Zu+a_+03_gN{C+`5$UMTUfP}( z(tXG>h-BQa{MU5sm(r)i9?!6mhu0ocE#dB6wrl3PY0oY(J9t2)^Esn{c%oDOV>+WMRoP=JLaj|*Bwi#>ZAM~%|c}@=aoQ#cI40Q z^G?g_G)jxu2ndK1E^b(dg4?MLW+MlKx%ENYHVmdH&p=m9t`-aGdsLBu_?bT3pYu$M z9D-J6j56<9#Nmoob9_ZK^X_!ol6M6UdriN~FOV%C!vm3>O$is^WC^d4$5zLmadEQpHM^`08$YDUz?w|hNF zU{;^NL~aJ|TdWkqiu^z`z8gb!mKF$9cIV%oVEetiS$E=nbBQa;lb)*3pXL;q6XvL_ z>f~3ze0f7K?8>0kK?uO2Hd~SD2me6kd;g{ag;BW`?DLz#rqfoLDNDD*IRjt3pO;kJ z`jjdek9TM?ldkD6=!b3!IB+CqJ8G6gnq)EGdEzfiv|jxrbxjtIQ3Wx;;ZUUcpL@$( zLtpmYL3hVo#fJ#M*DY4JREh}*NzJiIlT}l6nvUm$13RHk(s~Q~C2bUH zI=W2=cW{kx9hh!Df=EH~QZ@w9w3cg+ltvPRkce7i8%zY|`0*m+NW{Vv5#T_!5*uv?8=>K;|kQ_P%Ix(N2hYq zp!8K~UQ*kthj3LdVd&9@f}~Inh8Y=a(P>=*G$A8ZEAxqzUf!hac=J=FICD5?Q{{35 zV$)hjFlpk!4}Ggt=Kn^2g6+WIdD@In>$fn&X>l0nJ!(f&DuVgT;5d~~rTy#5x-^Et z+wrY;kd9@^Afhf&URsn;ZcL=D`4ers`T~p=Ew<21Ij7QV3F=m@-ro&)rv2vLhu{H$ z@u$4Sd<{GtA_;W+%Sf4E2QtG1s$GoP3O3zFKShuz_PdNISDv6u<}Y7BZ43KBZxChW z|Co&Z7u_Qw0heTI|5(1I5CCmy3S#syLRTWvymEzVK$D+SW1Lu1#YmSW!<<{wdnyD)L-$`@@wwi2-S5chyz6cGZ}Ti`O3e} zq@eGcXt<@(o^MQu=hV15^dCnPD7W+|GURLpV@XKXDN;I2JWpTYC2RcrTwvi4BxgAO zj~}JW?=b*b;44J$g z!{5imdj!oe@umj3^-qM4KWiKdp+AK@s(oS1GCfAO;fw%f797)PyjOh?^-1`jugzZ% z#y1l141y;^M&!p(JjB;>_8ezk!%c77nuYqA#w>8}PWBQ?;sUV38x)~IAqd$C-2a?~ zgg4;B)IM&|Fg2@?Cb7D5@;)=*>|pBo-!c2PXD#%J>Z(tkFcs7WcnI>o&sCfM{@H(@ z2Zn59=p8~Q=!p;~7aiQgO6x|itdrV^BxV75E6Yxup;^Fx{-i7*i_c@gH(hI*)Lm> zMU8ApBRvySh(rA6C^MkD13Z$y0xprza{+fC%*?im*kh6yAtH4~DxNDh>s`XU${< zrVgPm-Y~0%vnZ+ZUh2d6rnI;Obg2jBM`NFtT~O@5ty`Hr@sIxwrT&NnmWRjB;hXQX zL-;^2o_MSBOpB;<4Y+6anJzQ(1%Y{usZeNJsm&JhG@EWxc6Igw%4@5gU{Snrz_ z(NED9mCDNZ67?XUmW=G>`>(2qEBo2(^#qk2{O_)nk1(FRTiDWFp`-?2ANOJWjEWvJ z*?pKq>4^yR(Yj;H^z^c4xn*gjC1shQf{btB;O)pMr1=tUPecpcA}{pSh*OOVTVZfjGAe*m6HSFM{N z?Zs3Qw9ZyczqG_TAEUf!fX76`8rV6`vt;Zawr9o)!GEeY_)QaTA7k$ zcyeDR+%tPOAZ)7hgAh}XawtqL_MYM5_e!RouLcWsOb*Bw>)8`0o1!SA51-FW4(gLW zSuTo7VAAIaK|(4+JiMHtc(uNL=}#o~tLlTByOD{JeDC((U4;1g`={!D z-yO-$=@u+p3#^8q8W>;(1>JdZr{32A9C38PR1`2oRhg>AROBc8|#xfS; zC;ftOl-amU_hGdrm|cy2s&#HH1J~jKz2!-sK6flpDmacVJ1hxlugLqWuA@0YdpjO| z-I^UWeMI*qz`=)sQRW-}%=EJ4E2uVEkl*M0YQ)FKjV4HLjuzp3+A2YQ?S9>IIu#Lo zmRYdUWH-6UQnc?IubBrLuq|HR7K2vM`*COxV~8rPt$=%t(%o|l(w|zHz3}NL6*1)D zTxr}P$7w6;=t^S&5QbJ$krzY~eBV#r!(qYd>>V8B36e|XlJ(d;{WLZ?Er;efTdgn; zhKC`HjxYd`n6nA%#z7u(y`9VQ>4K!3x3;9dP1a7Sb%I3n#lz^`U=Epc?>?sAgT<48r3mH|_ z4<#BkL0Dpy1%BtvvU;wB;_^Y*oi z`g7t%n33o2$F9B4_YLLSVAsfo4{U+SMU-GQ3`ytd-@bHkYUc}l8e_hl?ZQG*4@5mN z77maLx=8xCEJpSY%lNAc?UzcHa_uhR#`8<0sqMILs`cLxR$gbcau>`-Xtu?^;c0*_n*o9~}!vvOvo*kTOgp# zGBt=aXx>W%2Nl$mq)LBDiToA=bR{xivBm0(+%_7F=2s{D15p=!fWiEj**3XA^&WpM zRj9e0`2E$U5cxtEpjmh!*7emB$tVF!>f0qOF$hry(-CD&)P_O!5)1UgOWupKows zBTsGwMjKL=_qoeBhAZFiZCS`g+Nj)tzQkhg4knHct}3hA$VvSxcUr>Z9+R4o>+=A* zJ(>4|$@fCC5{mlFtrH!{=ZL`Zn-aA|v+c@YJpxxy;eMPx%_-<0F@TTzvfXvEEI-X{ zb^vK^Wrf3D9#buduF7;B*ZD@4RxN~AAB$d7kmqHehrKb}^@3V3O<7sGboysBmc-tw#^uGq*);;IA)zi;NqCfSw?-IBbzVLkDUMX>MbGYT zz-*hOl+*~SHxp=fahBtewXr^d8Uvr39AZx14T)L{Rk2}wQ95+xqp#gdbEjEbOfnS6k7ab}(Xo2pU znzb?3cMMF(hqE`IYB81J+=yQH_l<2Xs%zLPoVH;d^UB)=%Evve{LkyYKPs90v}*Lw z=-IFcvkG%~A_qw1uxAFZg-ve$!=cZ9csN{(#9aMr5}d9;dlsG&K^iN}?z&@sV)h5H zP=rzA+)%L!#|WbYpU+_sH$Ly65bl%B$S1G?+5N$sqH#z7-Dnq+oaUdB;vu+s#5>d( zG-5~O4nwYO{ON7SXjg$y1R1X{hHKl*gs#VGZG5eKCd9&`w`e62JV%wz&&|!f znq4~WCzwpOJB@?6Y_-2d>pu`c;^uuetNG>C_n5%gYa)xQIAupp$DpUx=PM`-k%{12 zWF(AtF@3wi>*QBm8>dua7n#6lZ*8rrJ*3Q{r8c3fu~m6b;(O>RLACe|V3CWw4hp&? zWaPO@Y6zvodW1d)EkH=?SQ1?L=Zd(-tt6O1pHq>8wkA5X(vl*>&MzPI;GKys-swdU zAMpsjjw^GfC)*uI^gwY2Ut*ce!!GY6o&?qfd*~9{)VGXB8)xquGAq-Nv_CsXtP~C1 zJ4Z!DiBl(-;dI;rAx=lSSEsk~rO|Fk%P7^z>0a9L^^9TY-z1(=(>(U};B}+wKk-YY z=0M=O@K6_xGKNv{J4F)aOw(%Gde!TNn^Q0@4P@>)HPQNSUIdNhx{-;S& zj--7V0<>n@!SX9xpY@6L5Dwh7QSACT#3|~p&i)gPHcsT2d8J{CH$_TP<7s*#_)|n= zyHdjh<6wwoCUGs^CynB4_otUzV;_nihaetNR`KQhxNa@)E9ZoZ;VB36r~aN)bF>Ck%ytJ@w0O-6+!+7TWHj;Xj4S!D)vZ7w#H zpZUwn4X@TWviaLV%=h9&Q*No^etEz-PQfNx`D9=Y4?k8A%%;THu$9WCMFd8S`x)s|u*f$NEkFa20eKi@t zu79BEJJ74;5Z`eb>z~3$XyaY@Le!I7i!kl`|I_{3?e_*63hvt@^kNb+5twu^xmHDZz<@Q2;u1l8)QQCboueo*Ak_(2Ucv3WXZ0rNZ;ODdc69j zJ>=GJ)|NjWBe~xRI1wdN{K&RaOx&Yjlh!G1$4IlOEhjVQZerbGf^ojs&+kFNpv#{K zctlsLKj?h$gQ0;o!a(x{aW_#$2?_Cp=2WbBCHn@71*h+qp-nvp0bB(LW?#&HJ|{#1 zW|c_GiFBh72J~)uevww;(z`d2y_x)xIdye1>JE%@evo(A(IzTs%U%O)%fvcX5c}N# zLO~WB&quv)!sPheN^~dL{Zod!kSGklm}WIHb0!>>c3nnk$ zsac?4^p_B6h%h?C>_H}sHrJy8p#MZ&BU8By#XjUXvAUb9w;|~3l@DRAcNt4dCDnrk zh1uWE(+5v^SB(2GhH>+iJLmu?q_>dsw9kK_wOQSTwPWDuEm&=AuSt}>0OoRUe=op5 z`&2IQPT9Lx9b&4eP`!3#wqgLY6JCBQIk~c*$7Z1FdmGN=t3!_r-Pep640w2$LFm91 zXKuR|DJjeB3dvf1_^ZifGKoDj))i4w^*v~1c?D$&=hX%3(uM*aofoC&p-%aqu$CZ=AD?dv`%~*udesAq40WVaG)Vn^hfAr^E%6= zLCn!mexR1q^_T1e^>6|Xa%WNdV+P~mWH)uBr)m_?h0?GM(8ik)cFI3&ysvU+Q` zw3DNVK|>VhvoT_qb`reGL?n$yR zZ_+rcN(w@UO*F$qRh#zPCE)(VruhM)ZK@frKYc^l5DP5w`Y=pAnI)}fB^R+KO zjvr_Qdn4v{kk;fVGjFiVK&ou#Sf@R+P*tt>_^Gz@O2P5o%HCR1ryiwK<7Vo1wM%Ib z+^&r_OY60!YyzTL<`YgwcS~cf5+XzAA?q@@Wb#5#ZFRLEuw%bUvogfCLK&Sd^86?T z#1UCD{lC;iXrSwK^msA?bA=ipBpZn$FS=sUop~zgk-GsWKZtUS@`#%d-05#2FKw@oB9An zKu7-d?jsUA(QpaIVRi}BR9!#<&#^#I1_Rha`m~I*6VrZe2jgN&jL7tgFzMJ)F3ReU z&rzTF;i6oxe3`<+3A5-ddIlmTA2e&%zOX39c1U$-j$WJ2P13TiQqon|_Yrfq-PCBs zD$9Eas&ZY$+j-JbJLFTSj2JZKO3F(j(Fw~oPRqI^1{KSsd zQFelWCcTApUTOx+5>{zhBAj~L4yCo6!s>K#aBL*i8%(`5re1wD1_qJytbT#Loz>u| zl{o*!>gYy#Q&9mJrL8J}fILon-0X>PMR(@=mE*1|-^0rv+e7z?v*wfN$P6h?aQ24K znugxqd9@cEUyxN;bP7M67GH~p`_whj%GV!u!PgZUWf#o{@X`3Cu=V&`ojdc}uarwY z1S$CH==gX5x1Ottl$bPA5M7a_*9~&~Z&Sd#EHpv&5P(WnS*oNkt(BAE+ENAiBF5nt z3iWKOv+-}5F<$F2`Y3cmMLcm?Zhtpu2Z1~=P)|VvgW)y=2klG8xt;uS?}(bfQyqaC z%MY#=WQLr6r#&}alg;fxh)$=oX60xKpUcZ)v7drY7sIjl%<*-$4?7pstUDElJH^~M zLnjpVdk?~>l!Xaf!LC>B;qmf)>pw$Jh*0N6{90@`0JC9cwVMsM-Z(32mJNv(+N{fj zPtV~#-AQ+*uD(M3eH7q&WlJ_7cWBHa(vPioQInJOvyoq4oZ50Q>B(IoqCd#bO9LJY zI0ET?ed%M?*GW}CpAG&%Whct!t{O7M`+#IKT&V`uJ~6Fs}OSQmWDX`THgEdD{R zbm?mk%gF>n-UW20^g?%K2i@xDPZJ|XPa0fg_huMZ*KD~1m1wXtAZ+)eL9>_w1xeBi-Z;M# zh#c!9c&@BJCg8TdA|*>VQmCIZA~J7;Ioq03msUyX()1DshWBOUP_-a{xDnaLqW=X+ zY@ln{zqyfQZRo}z0>&wK1FNFb`(plm86ou?cGBETi?SC3)wb_s7*09I;6v zQ~RRIu=s~RW5bOpXP)TBNkzg@r5nG1u!-k6bV_MKjJ;i$%mnuDzx4}tG$S)iPfbF+ zBDZd2<%;vRFf@lv_FN2L%9@IGs(L2C!ch`AMc#V0s+guGu%{9))xEatFLRnFlYpY7 zJRo8ee_y3vh2sQd@J|g4%!;I(_rKUgx6c~|_;UE6Y2Or!7oFG~z;vfhuRG4BaX`Tgv8iQ&kFV_oaJ?wq zX%V7ZH|HY(#PQ#C{lC>Qmk?-%HNZW3>&7PG2__YoZ8qap=SvMFGgpL*YAT5REF1+E zp&26}FEKjzUTfn1Yv=LTVrB{s%-^K`&VPwo0cP*YNt3a**ReSYNl8`@rUrIX0jNQS zBaq)fA_k&8^;awf|Lk0TFNS!hp&1(9yq2>;yD#c@OR-)dXBAAOv<79;q$`;oE0@-% z*H0lBssL03Q}zE(NfHiRl2LoTa#%l(aTqom`7E4G9*(j+VFT6d{z{Ec;}pyQ)ns$T z1K4O@Cdqb-^S5L8&-EZu0wfr*O_RX?^4_Qmrql^Ro+XOyQwf5u4#h+vB2!}F~LQZ>~0~~VRKL%AivDa+}Nln z@gKs2?pY4);dGYX!+XSP%?1)JbDep+vRFT&hJZxN*?)ll_3S0k4;wNF9`peF@XLy7m{d>^4eg_o7wVcwXTx4lv!*;{ji20~hMX4Px7nU08FW2>A6c;XoPBos z^o1@_;7V-*PoaUU~Y~jpV8k>X{naF+EGj5TqJjB(gE_!(?a2JS0Ne zbSb~RE8dp-VJws1ychgM>fi6FKdy3u@h_QESoad`=xxm%lzsQ7&<|j##z-?On#pX9 z%IHr&r%H@`5%^fB!YmkwSzzVV7(nM&eBiD^37p_nV+jB#}hY6-j=my_QTl zlM{&?Pq(Za?yQD=IMO($p%GR&Yk5?S;{#OwmHBDG&`>FqO8`Y2t(tgGPY*K_r=%?w zJ*aN(Hgq^g@CbX%1N8VYng%y|Q?X3{p+Dcp2Y`c(9bxNrl4fymYB)I>m6|FXuxi|~!lEJ$BKNaUWoO-)+aEl$5AqY~DR|x8Z#mNQY*qOThzD(NWq}uQXc}NL|l)pa3<9~9t0(!7>qGX~{7hknf8o~J@6uyP5 z#!#8S{F4bm6SgJEd#p!ewFL{4L(5#&HM;!X6XHgL!{eCv2=TJkJDh9tRY@vGgO~+} zPi6}sUqfvYgs&(+e@+zRBhe@*&Ff2d2oj#J#1YGQoX^(E@_KS1M%}wDP371FiGNoY zMP*{M;N&jvI1|(9DznTInGJwIQE23TqoLd(n+HiUv~EGwHanJ7Hd}U&t746sDB=4B zjmBm6Qd|%knFKkMKIr7+Wb(#hjQBi`NsEaA{2u#Z=4$!|gRGm`f3XqbC(+wMp7_A| z#+f3Y_7LF9w&V>T<`2`u2-g&cSge!|iM1lPvZsz~(4~o=E!7kgkenCx6UpJRy+*J( zd4iPdNodqL#1z-1;d_9Mr};)x#*l|eb@}VUt)zhlM5{KL#L{q1|FZvRz#EC^!$C@r zwCnS8C8dI@x}f2YQF(>$a3cots@?A=XpIzln;d3D6u*SL`P`A>sBw3TVa_g|zrpYI<|3#e8e1$syfE_hX1$>L+DnLgf0l zwHID4JM{h8+mhmZ*dQ@zfKZgAI5qodRz0FkmL>%Xi4bN1Dk6F@_*!z&w42uXviG`L z-|81esV~+cS0!ZVrL^p7WRX-E`bhxkv`tU@K{wb8r23VY)BIujVQYOHl?_8Fy9h~Z zaN2jCIgMSL)+;SP)7U>kj|#i#B0-Qx7`h)HX8@y*cz%ZB&d||(=takCubx#U39U*Pqqh<0ssij-nz8+De47*PH*H%l7L% z>+%!Y!>n>0JP9=jM6;gz(+6t`k)wClp=fULEGhGdp=U9=V-+Hd(83ij?v7of4jvPeyV*7)c4oT60wded%w8qAM-U`Y!1eBC%3 zUiG`FE!A!-s>GW1YmQK~n#}>fTFPxMxNf%TVLTm9e6fr_qfsHt2yka!E#g`R9qMYX zy7+t0X;zDh(k|nuCH&;Ne7YRyno`VKWf|5EGT5NPA&8<>99X|tUehwp(z-?;Y;$4*h9n#xLBgjKS(l5I$nOizfVKE2!sUx!}J>uIDYsg`dO+l zhrX{^y6<^#BK|}46SY%{qmfiMGh&dF3_Em`(}|2&*QREJW=``}TN~IzX}<;gi3{@n z)NtvU>mf^jd4GfRfIZn$LW0zw$=@S`jAb z0(co!j)I5L80tFJhkzJl8JNN!U#2)^FFjHb7dWSu)sWKfGo;wJb^qX^IO*6T-d2Ir zj46T=1>wT}fl;VY`%m}ZukPpx@+-5puc+-;WLRcun5d*Bi*g7@Up}o|b+d8~M7J9oT{g3pre<2+X^ zu~tpSbBK;~*#5ip)BUaLWi~R+8i4A{gD;(0pDp10q0E%&YLU}QEJKb&kmL4Xf4hVcfCoXVaw zJBDa9hp-|`Aphz2sh_Qs6tnkX=R@xcxkbp=In2OJ(#>wfVKxqr(SWiIRBN&%mxJXD zb6)BXC(8dRw@cFgiEhaW5=^n>z)d3R5lef6(B?5d0-nf$*}FOO`pgXJw;J-8(PI;U z#kg4^D8B{NMRgJP+vv7Tkg^vj{Lr@-DMFCRbHBxdk#&pZevYLW=imz)m2A{?ujR+m zs4L&rJX{%8}IPqL^cCpR$Ls7h>Kl!)!+QL0fTjA#Oy3C!P5qUu7{-9g>K_1URd zohCMbp6&z55w#D|sn*NPEv080(N{CD9(@=5xr%}e5A6Y;=GzyjO_Go^npsageS4+) zTcs~wCgMhtyTP3sv-l@#%@^>j2=NisRokPju28zs{2@BjLttu9>UZh%O;IMbjhuL08qLWw{t(<^7iDd{EMn|{xa#ny#Fxi?)D7r z)m1e3iibPB7cK(@azL5-a-QpN;T(&-ypws2!sF73reVZ(xe9@0e(U4Z^)PL>FMJgE zdfyn;)cJTf!r+I`Q`_Ac+GplY^{9{WYG-fz?@a8EIeG|`X{LF3l`^p;d0P$u?^cIq zji6}?Lyz)8cD=i@gk4Bc9!V4b4Pi?We&ql-xDlmz?=~h-^Tv4K0Kctc z^??x+5HZ!WY?7z_0br|pz2t%)k|Lr7X-Nw~Ew+NnP+R*~8v>G?A_J@p#lHz(8Ux+demw-T86Bu5i+6}Xr8~oii5EjOe&Xsnu}x> zym7xll+vlSGI~!2Qi_d9PO=6aLS#4K2~^xAwJ2rl?@>u##%?O{&x8N2O0~$y&W;KE zQ5+$J5{Ciut%9ROa}$` zX)p8Wun1_43^e71$lgogT(rJ!=oo*5h7CAzBPV}LpS{~E{9&xbu=6iRL;>od#cjl7 zLF;SJ7Bo1Ck84E%1jPiu>ix9Z_X~3&gV=iJt}K7ohRDeZOEgr3)hz5Ch+xF|%dh=E zVfEnY>f2X11@yfinI^lwW_O_<3wo>T7UNt50#uws+aI{;Qe~7sDQV|)tl(+_+A*9H z_>L-+l@NgkCy|i&Ieg_n9Y8+A)|)@L%NkZad=~=F);x&2oF@NoaV>!akl=jgK5;~4 z6l-_~YayiqvxlzpY=sV?t55L06dBF1MueaSLlEaR5EE*6K|qO4ge;F&U7D#T^u3%v zeIPp&s*&6Y>SDvbuvHH}K2A2foxkgDs(lKRf8S<*AI8Z%rU=iYAq6|pI=2rcQAew1 z6Ya7(>!l~{rQ_!PWKo$oEP3-h*4J9IcO9MkabvDGADic@d^HT$zKevzS*o;!DbdiA z9#L>DyiCh^XJhERi>$9|7RkJs!H4r>g(A}xo! zS+d){!2((yki}=;h{5QNg|(mY<41E)G{GzTJA>L0Ddx$ma#yOcD9H5~3;)I}Fk_ye zq>yd{?6MTFKO8Zt#Uh@G=`!};n{p2l1~33Gn+k)y&|l{L^Ju+GHq#|C!|dm!Ed00} zZR;SEI1tLJDJC54IdG^{JnCW3(42GF#F-qZz=qTBEAZ(07rAWk3*@+e`I@DB!=5pZWfTz@eGVGsb+w%#dm zf1EP=LGZ6C`ZxkvJEjj#nGRozLt0TBJ(j&yvo{9#tpc7pi`*f_;GQlVSpda>7sZ?2 z*QlPj$CB7p+HO2&I-NlO83O0@b7^@}qNC5v8J^ESfQJ4;=xs8<3frbN9M>}HX0UQ> z^ll_w1Ue7wJL~79;vM^sx zcTgMw*1!j-6y8hL5i-0`)Ob7*GT(DW4OaHjsPhuSd=e-?2joO^T&K~|?p$>aW1(u@ zw-Oam6eQ}2VEVRBs6KTDldV#}q;xh%dFUhW+cgUIZ-f*KEcG}?8OR?-g{KPz9?%LE zPQX*u(cs#_^>2YJ%e$w+e1`UkEF4pKZp(81a zH|I~+D|%&g&R_)tCSrCi^q3za55mdYnp_>+`lM5k`Rk%(xcxkQHm22%jD6N^Ke4Si zP@f+Aw*LsUuTD}3XaVJt99smVpFA)Kyfx<>c?>@1)LH4`N_)4c4R|CtZ+ZJQt)M-~ zaTEPCcc%xZxfk-eJFv5yfuFZ_NG3)1_IA5EdMXpC6Q($%-5l@Ewu%BAq$Z&}AL%oh z1WtW?Tn%HHC{-zA-@pv@_Wxw5Lf-)7uY``@o*`6b!BPCzCG&g}03%qMAVF%>(__Be z%+l@t9M|eLiU(j|WI@8vAw6N}pTItq^Et*nWs0cm-rS zMmX|&^lp2NJW7O*#e(Wg-x^~VyYP{%9wZZ8R@Wtb~!*F_&I{ey+bL~wmJ^MmnM zV|WYEm%?Ft(7ADN`-52hQIcK1t zUgCe4e%C}%t76z*^~s4Sog_-TL^!|;AODc7q9uIid5-~swkVG}T}>(-zd7{;|1eub z{#I@v3k63kG&`&U;ABe~szhFnM%T1^!m*jH^+JnhtrrhMBTN`)1Ihm3c%h=mhljYj z^vk0<>bMl@?eP5pEg3E((>^^%BJgzU*(78e2S^q&XRh@(na!dGx@Dvs;rPuCsoXgVzDsg*z5aYTaWn;J1j&Bm;SyF(4 zkyS>|LXm;ea7vrB-$P39eOGWh}k6uV5QxRD1FIjOs!))zH2lp}J)vD}zu7CPmHmfN z5YqlZYnUV4Rixy=A11h?OqG%=T#{Tf@<>d3I+njmAkWq_bb%M4zLZZ){|h(&U3O+9 zZm$L(?yPVm?pE(Izmf;XLCcy_UWgDC5G;vX%wOIVPGd)cYHi%lgGas%NNClVj{A?@ z&~78_aZlc#e};hd3S|8XQ-B-#4(q0jHe(;;=^TUzeCioAF5#_zR}hndIsWeUXHCaH z-_<{WPrHpE5&A1|3IU`joEHkMknf(0BdYL)l1+c`8d)i=49T2QkNl~X3Zydmm-+bj zmq7(Z+Zyae#|rtSdLSU#eb1R!57HLrz`xQ(bp+n!R9br~RY#|4yj`*7|0>**0}^!G z+YJ&2X!H^=gsIjEt(;)#G7|Rxq%;*9b8Sc3zbMUvdC~tTr15eTrF_YxtFQ24AGtn{ zz@Hp2{~Od0;Xj|#ORP%^{U6tU{@<7ehocxo|NHZZQ#yW5nG~dJ1TiZ98HYLbLt^oN zP`^K`>4E=)(i}vbfTac^&CYz&o3#^yeCungD#tGiA@3-=uGwJp=7UrAe{LZ(UJpm# zaU|l*69{=)@gUUypLsYYiW^z=U$16{&8t7Kq(^O{W({VG|F7Ez6q=t(-2M~%sZ;m8 zo95;YVI$NPyWPUUQ|0^>Dz|Wt-P%tSyKy1%1!R9YJ)X?dskA?Uoho>)ZI@N(gshU&b22uiVNuq*vT`?O zAB^7RI$pk}Y>F#?Hb6i^m@I^Y+8Zc+T?(A~o8bKoL)Cep>m38D@MTF#(8)69hN@~; zfcQ~V&y@xF7_27lt;P<&vjBPma;FcNL2$?%tu89-q1hbx@j7%gzji{0ozp4On8thCE=FApl4hDR>eqSkCt^!a6nAVrlv?j zlYZ(?jw0>cdNZ$V^y&$r2+F%a!!p%2Hip5##FRG?hh?d&9zWlr=w~(AKr{V1C+;rC z$4&PB{djx3M?Is>t&M{FD=)9@*Th6-4ns^ba&mcPdnAyx0+j=`rf*;{nX*vTbJ|#v z?d|0J2(BnQ`n2pA(|@gLVxHa6(cxiJ_AiD~_M4$NRvb9b5SqFc?#W0aZY8(7?r$=_ zn@_*Hb4X&fN8I>qb9*&z&^t`4xv%NaG<0F&oCCrtOq8UgD;jtRNa3=eFhVe^9deKt zvwsNkAp&8YQ(35!gW%$2?*kQdY)geYd#_Ck8}sazKPH-K5GuQqFIkNI{T7zQ1(*%R z3vrg<$|Cp&SPP(G?qIptFEXp(qT+(jPN8S*kf*&s!QK9FW-?neKYg;@a=XgMJ>4C`w23B!MsGMz?bT$o%`u&_qdxhhd|+mRfGn6 z1)gj9%e{S2@p5}eD+iRwAotSgt5|DBWxmiHcnVjr2?{|^{gv(TqofFJ6T9+Fc>j_` zL*hxxt_870qGbPUnVJL{NDq0i@r@Y&mCgw2f)xVA1#yWt)($Mynq@V7|E2({(^eDF z$Abt)-qn?jiFuVMIm6ewZ!Ed_xo82CQ9Z#%N_L+#8u60mA;u6)=NK9!bG}Nd2FKB$ zOfIgC#hJ$a{HebPs&kHPuvalv739=Mu(Ox4y`QFjRVUX4*$??fMeX=m&F}fXqpTNs|XJ@uFIQEUZhoXB3dj#`|UgVcOgs$uEP_t50 zoRvkUsH*-x7>Q{34wN>Q8z-Tpwqi@%Y%nGKm2D0UmWFnJEZX6c(!;92cU6Y-vXS3t4T=p=Z`Yr1CsfG| zLCc+JdTjD5!aDf#czn#Xx3w>ws7Y-^;c@Y_*><(+pX$_6z5J_xeBaG2Ea$XXE!XV# zB`!SS_VAe2EY63UVYv`^Q&|t9Dlt$kmASdP7>^zmP!nq)H>E^fQ@GyW;TKjxwRQ=r z@Qp#of-!!}2t!~mdiw#1-Qs-m_6I0Dv5C${XQpX#L996yrIs`erdR**`DsbRF)bigkb{U$QZ(&|$6j;H%6 z))4#hD^9H) zHi{%*PNeT3Dv0fiEjY`Xt`)*0n}1`=`L?Bl~SG3mp`@NInvl% zVw6iY$gP6Jynx;bg@ui(R9+PrU8i#cg9-^>?|=qPu0VstkUUuD*EMUepXr`=szg2& ze5)?~rxvF}s9`$Pu|@mZ?!Y{z&Pg2OX}q$-34chHA2m5wRO;S|@!x<7miX$Q*4Ekt zmdjNED#0j)frDSg=z@PGKDhG~8CQjPb`vknt#ulHsf3t5M+vFvUK znY62Co642|_nQ42;VSQ|w?wHONse?N%sbcseT^>_5XmMXm3em9z{=$jG2z>fTUp)PF#xhiS7X+yW|7IF~KLTF5_IP5fp{*dRKA zY)Ov&eldh7(&5bw6Nk%^TCO)b>MPh_kWMGqHsUtYoO9G3M9W1?&zuq94mt8%qe7?% zwdva4-la1qs^|hzh0>v zc)i?ePmHpzOSSfg1U8JFO%^*CQgN6|85spi;qtHjh)YPANUvXye)ws%B3*B{NN9g4 zWm7qCIo$_;GuGzhGH%e0Gz&@45czf%^iKGtwy`n53JB7XAYCUL%>GvI4Ma_NoSzvx z&7l!Fkr5<_MD+*pJZGNtx?k?9^ia4_o+|`UUNDYxpU*&tt+%~CyAbFUqe8N(L}@ql zclm~KJJGg3r8G2lp)UY5rQ3-$`R#O)@pe31x~0_kofBx)NuCjPZI2D+gAhovvJ4D* zFm+x9RFmTB=~V-BFSGy+ad#IyBA*wgp+M6A@<f z^;1tC0IAcuH;DUn%|z(=xmm`xJb{G`*O+lwLPCBlg#&`24*ad(dX#!0zQ*6B5n`iMO!LQlWtDLL9MCuXm|##3-XY8~ad|Wfexe-MX~MtmSigs8x$L5U zy11P(S&@GlBY5RGFXxhWyFbg5W_ME1GvjETyj?zV$62}x8hpKC7)J5sjwJkPVl6D3 zH{F!eKh&EY9K4lH-1Wv=kIP{f$HVrJCDfUBf1TkbRuXgz4}bI9o?}#s&lYKt<9l;J z{I8=ERaYPC6t)W!;Ngchx_w$jNzaDXuA_(8{B#UT3gSvxi_;Y57%Lv2rUT3tuaEmq zPXM9n*{nC2QL#7HNH8LU ztCyxsk}NXh3(0K-0}Fj)pU^s&@i(}qjwC?6QT&0T%DjXbMXUvshGcjLN&qxRu(!Iv zF6$v7uA~bubvhB1v{hKaBs36!rv+f%{|&%9c~m_4zEJCJK^K#x2UQYwQ^2W?H0YPp z1>>f70yUz#>AQ6PeS=19#AS8TY9c*NrJOI%#xn>5?2q40h3e^_ z!w4oUo$zv%6x8>D30taQT6I8=lt>3IwCbb;b;K=q&Y{xL5h8$=-$Eym22+UtUVVU$ z`%)~wsaN*Pciz4H27x$l7&ylF?!Ty4{7v>|s9(1i>_ZW;Vc0}6zYf5p^>1sURu9Ui z2;&JjzVx8(pnB1^ytKR|Pw`n_iJ+QVU16BvxvmREHg{ctNRE=RR>f0?mbYL20#Jq% z%Y0#;8UIxCGAfAF3vJadeQL)kJ>%zC1jdm5HDR#~k0u%@Z`*hO0K|mCjaVn3H=rZY z`7;520*hZ9&>L_^h(j7EP5Pzr2Sc_#;%}cXSEcq}JE??t?EI}_F;_=WS6@zzwWg#f z?#aVe=4ucV?8?*6`tX}#G4b*@b7LAmgF+Rd>cLVmsd0pCZqOrw*rrhH)RiLZj*{H5 zcO%gmziV*dFY zXf%^p(}Y{ZFRRx_vNoEo|Agvfpp!&82ashbd+(9-`OMkiGlaHy8uW{wXpNvA{<1-b zj<W{qoRp{`oR&y ze`E(^Ov2UyJ$qNWUC)=1&6qYuuo=L8z_~`B*hV(V8yM=iq zyqj3KLeUqSst(m*SD8x@(=JIrC@Tr+OWhOoRF{PR6J0r?pxfrfHH{5P_9kr{fj!** z!T1vHlyC$g+5}0k$H=2!L&wDelS)BLO$`)fT-(eO?5^xwkZwN`C03m&!t00rkCR5A zjA+_4nhvkn%L12qS9ML5)H8P_RW`WOH^RfnwP(vw%jsIYr1M&80k>$^+YNH~R9AQM zAlI~`SuZ@76rboVR(fdJww_8Y;Tia(lvI z_aQk3FE^jJbfd>Ra+%)J{{R>B{_DyF2Svi(yXjmqMevZRGLB3|s})*7_ykJwrL>c+ zkgfB(o$eFdbkaHp$W`OpCQnJ+n`QY8)A6~xvUvX3d&|@+jb>B8gOt5y${9d2yw2MH zhh+mP^&mWSw?sYAv%yXf?7VfK+f3CoJ|O{vRTiaq&GW&JAj(uBpMxlbJ@?`9$LFSC z1YC@9pIY;=lf^S#bXWwUlUGEpjlfG9CD<*s*W}QI&(y5BrMqapkWb0e`Hhc`NFSUF z`iP|g8+jAs=~NR${bGJuQM0p3R+nGk1et=e!mAxjh)V{^AO%e;Q>^ksX&Uq%>EqDs zdFcs|PCKJyq2zB^QESMo1M3zwDzB)d8>kc7;-12GMY(mK=|oYK&t2^I*f6v<#vBs3 za1p^-lnL)zMA+WQ2(ICYDyp`=Y16OiszS!qbs>Af?vFcxR`Jfs z)_}D$!)`*;|Hx`UieWT<*sSMin+FG(LZG?3HpWauWJGk)NG-P(L?ZT2^Yy?aRxo%(9lS5~NDQ(3EoV&0E{j|6WMwCw!e zK`ZCHZ@D-h!`5xwuYzQug${wfv*d|bhzChO8_81`Lm@);M$;%GUzYlJan@fF$~GEh zRe+*#eD@8Ht7KX3Eh-kQt)~T4PpooH#fFKIya3@H>JVN;{&co3E?plC3`^bHE@E0q zC8&-jFv!X25{v;<(q}asB00Se8n7vU4@k~ZIuQLz{7e3~Brq42(J;u=%EtY=Osrrta6^K@*OJ$UDl&s;Ru23>Ij6p;@ zI*UcA6ULhzX#MW|y#-F+br)D*M%NvE$Z`N_`3^In82DYXXq~#X2KzN7CEJ(KkK+Hd zGc9QJRnI(2%VgO#pI1W#v-y0!d+B}gClY)laGleWWhA$Ono`-#X<|4p%_lxK+bT>3 z4@oj^1Bp`!RfZ=?J88E$w6taBykeCl5yipHvQMTeAWK@M2|0Jx!2f?M93E*!x{`2?X%)ScsRMu- z61oyJ}yynxK4z^U?Ry6hv zQcr;q5~Xbx52O{6bHG+}-aTa~6;?(lgX!V|s&WEO}sB3s{mbK!BB{9NwxJMM>9PqVj6+wN_iw8=0C~ z53bO2#0V2yRqeO`ywuO|_r$${CrpTvDU(^S<^4t{Edat1?B<&$gbRwVRvFp817=~R zJ>J6k*fgo6O2kuYy+Q`3*;-mvddoY+LS2z%luBj(g16|l8#Lv5TZ^M4+-au+(SxL} zZdjuuAOfsTv@aEcK_^S+9T-wQP$+~Te_vz*9W^1S1QP@Rmw{rrH#kXTD?j2P87nfb z>le29Gk4KB`ReZ+*ZE#Jn<$DKCY)|#L*}Jk@;))sVgB*@)o&lk@Svmk9>FW3P=J5_ zYB=C;oM=Z+;NEv%h4_5fsYo&ywKyvzRPO~QNG~#;Q);lSZX07{;UJsM2OBNVe{TBw z5Ab1h6w`K)pBb}wQ$J3!!N`Fz71U2chYK0HlXZ%VCQTO|pj`-Ye@^~7CW-j(cmC`4 zn;>s9IP1NuWP#Vsmro0RC!j#5h$1x>JhKXff*tytbFQG*uA3Swf_b0!Q-e1*rPpn8;ppIWBKsgS^xNxNYqx4|Wd?{~G zyB{GA__)l!)|2zJ(m!`-gOX&(9W}%gz;2#G*nOfay+UP4k7oIZ!yjp`2YF6ASl0I{Hd}=4lm{xFB^D$ zyBwsmYw8Zb!AyPoa&Bz9S#X6MDRO(THK7Cl|MbVd`$mR047?a@J}b-tBFw~_KAs#q zA&ayPNpHTjiHs(Pet{dRPm)|9LV=*3{@-KrPrvy18AJ^6^%~~OlW+vU>j7*FccRl- z&yVG&Dks#?J;&^!Un<{d*sP3?huI_-#R}1%;q`Yr`*|P1kn*NN!DEWu#-M$b_fF_^ zBsBKP&rz^uA5I3&D)UcjL*NtBI)jUB!7a zY;8Aj8b`$VeYHgK;n_=u*D@lzoyOO%^WFac-Ict(LtzhK;~|gix~-6nVgf)NkW%~Q zQ=qbv(J}(AvM7bBc|Ga}Zo>ss<-3cW&JuERFo%PYXYE{$tr}f7yl=Ex$coK=?vS%n z&}woMS*9t;udh<{bBKtI`wlYP<=jj5Yr+bpKg)UyrE)r(el!l#AVg*b4cbe z47G5*#k5_>6-PC^1Lj6CJXUVB+cm>Yzj^7)=OL$UxrsShd^TdBSj*tGE*qJu`V^1=LLuU?NCmPm4J20^Z?mwnAjBhg2MrG@?n!Xw77nkOIXRyr#`RE0 z6%mK}>9tzb9#X1ra%yPDl9?6im@Mlv>kXQXW?|TEF61)oZCm5pHT&&bPmMhP9?j0f z&_Ye1ccpTA&h3b$$qbH51ns-<`T?w|-kgPZYD8q*=RN8zvbat#P-VIl%1_5dv}uV7 z^;}<6Xo4NAJ-_h)+`fJ1xsIbEVC46Fc$KcQxI3M?F{>U)uTiVxdJe=jEsyo6f1_ld z;}q<(el48Q7{p`)?p+cf&%H7%ESE^qq8nT;-kx{r)L926b3}Jn9KpV{h#SJC#-ze0K$5WF@m5+{(cO%s2hXI!3!JD5A z&Qeu5GVa}}1gfth6^QD-k5XV4rUo%1*joy}?$c_({X~AQ9tuMhNiVrky)_1IPgy-z z*u1zti?ZpmiZ0F<@Ij)o1@|wrmLv{7RpmDQQnTR^;dslJ2p!)#g$dyaM6irvrc`ON zUs;{ale+bV!(FO+OJF_|M(D|hey1YgM;>dI8RlENROM?hjr5NJaHDoX>mnb*`8JGw;4?C2DK zliK{H-FBTrmDRk3>n@%*KAaL`;N0_Q`|8#DtyLAL+pTrtC$EVnvUmXyo%xU-uzBSmQivZXs?xoEq^p=am(AU@uK$j+s-D)HCo~R zYPa*B3<|_xqHV`Iy^&ti>t7uVR%<%p>p~E{D>ZvxgOpdV0 zhv2_3e`$r0fak+fof8(9-3-)Por)yV4#{SCiND>zW+;bklRw_S-`(#P=BLL5T<;Bv z;(G$xo@UF3YS$Y}$(Q9tP2{#bQf$1A;56`8E0Rg$`pWCF zhs~b51*yyf_(*>;w^Db?nR@^72zy2Ab*lodOkMG^^?vnyb#ULdA9%AVH$IMDI<#`0S!?hKLN`s3OCPEnh#L=z2s_r)kMH`cWA7@IjxptQh;u20aD!7N=( zjZW(ziLGU2GQ*|z#8X4v^sQ}%#SRX8h(Cq70;AHo93_C$$XQIzxkih#c;9i9q&-sE zGXn09(h5jKyab@9ML~dLLEW>N(Hlmsjy6QdOCh_pQWU!Y*JR-P4AKD z{W!;41IbbRw)?IR&|&oA>9ITzq@aPva+kued$55;*&fC- z90VLUi^8z`$8?w9&V>68tBr3s*f$>dU-SWwb$HDh7yx=(nR=`1`OjJo9Z!*Okd!aq zPV-}LUCR}QX#%r8O^5Qxp5*TsXRfWy^#}%|~Q>+(Dg>hhx7~*1q>_pQdTk8? z>8)O&1N-szOi;&QRJ-w)lYI(B*{3JZ$IoL^H$8}+WJwv1>CNXTaJR2#_XN{}bF-i3 z3c3}Hix-^v_g~T?y6JLngLt1RaY$T>5k`A#wg|XxKa&#EqhTq}&(Az1?Tr;WGF4j@dWPZ1yh5sK&7oi>LPrDft-ZttTyQcCEd^!Zp{?@EGLt4e0KKjGDRH?#uplK z-&m2aTi26(2vWr!sn>>W?T!&o`1&*6Lzc$KXa9@-i;=yA&mKLbQ%1FrrUEJwz$hXN z&kF!_a$>G0TX#9@3(X=|P}<>#(+#GKK2ce7quc zGhF%j%H*eZF@6tC@!g zXy=yA8JC=^eHnQ@d5s0c0KYgu0^-1risakNuil_?5VXy!h4aR>ig#T zyUg4n*kPTewB32@dAzLq7U~tG*9GL8V{lpA^Ird(oU+$0W8rKkMmZ6=isVt&P5nxgrhz$!sb_qU9fK32!c=rD>8GDQzRKf~T;&43@w{ra__ z77BSg?tX=wT)1Uiu9n~SSZY1IO2J2;Z74{(XUv}6`w)vJ>x?EQb;qpsLo(MXk!bEq0 z0smpUEvf*r`9KipaO-G_fXlWc&r~pradtGD8i0X8&Q&0Y0&p)4rEQRRZuW=Zkt6M)r3w$wAeVyKt+fHX=?c4hNXEKh`(KEpC59ry*?6v5 z=d?Ur{bIv^HR`J=EaTmRfNt0ZI)^|kr$^x9>G909-tejY_(A6+caBU_K%^W^L(W%y zVY# zfwENr8qr8RM^(bI4bNMB0!MZCjw%ozGa3!1U2;xbpG74C|LkRS~5qgxPg{_(*JQqKG>Jwu3jqhhN9tZx-m_6&Qjun08oiZ0-v- zizSrE`VV9pZb2F9P((W2yhMSz=%>M3B`q~C7(p$x?M;gzaKWfhtC)&$XzEI}02gJe zKZt$3tIbOB%o7}c`~Ij04HmJAOt>UklODpg++@&X{fKUGBYO+sA!<1#)z7b3 zwpdr;=rNrb3GO%AfMS;q$MJ3X@(?vv8deEu<9dyBI$9;~En_8_ z1Dny>rY(|{veo8QjD*SfCJAaPYU$;=M!IU>mtAcS%~DhL$wT5CL;%&O?ivX`i|)4` zI#mFT*UNN%T1T(VZ*brLtz}rsSgDq4li3eap9@KLB7@kc!CUaL!lw36&Y=3c+8R!6 z?_-x)WyM6tqV2M~DxmQ@xF8a}7rgz>g*_Tl86fqe@(Og59VLDlEc-~U-+%{vLWxci zX^C)BM(CqQ-2}4d8pN1C^Gm>$zHU1ElLBj!K4hmiOD!&Y_Vu|DXS7dNrfHd+i>WEH zGMPu=E#^>031WA|WF%e9Ebi0C2zU%z)TAsZ`$HR2Wn&B2-o&A0bma}OeMn1MX)psKGMzPsL3GE-dpDt%gv z6mQ;AfWG#$8qq%**xwIj4TZ@WfNahSV|ljoiO8*uF4OyJ99 zmHwV0Qc2zLFu!#U5MUkm`T5W@2Rm}GaB?Er{I;_nvCNG^I<=!$?Ge8J({U;bBeQ!i zSB2VWQt-Q)4BjPW&X`5Cqt`Wk@=YJAskjDK4|GWema=jCb)#fZnci0bTw?Y5ZI2um zm60EN(zIi(M)1)3+vs{!5buqo?P9S?xqMdRNU7EH2H3g1UghJ3E~_=dCc)bVq(gK< zp^VttZ5d};z-{oDV1VFag+=F8D5N44n}5YU?}ay|b)gz8QJi{2P0y{;5ZGkMHm}#F zySt}s@mKf+e6E$BbV9Gr75$+lNyd;w-gWv!pQvu+4~%6o4(JSWJCRn+UUzliihFE? z?Ab7}KEg$Z+tI~g@Pg^{CMr$W4loJQsF;7 zy}72d32`Q6JlTE8r=t&{H*W5~9eJgj7Bn#wD(cbe9AwGA#N;HQ9i;=YbAdRIqD-*X zXda2P_R~>jYDgNk+j>O6S)gMZ0@;6O#AK2#w_gk_*d9o-T^O{~bf%iXePxB5j0mXv zmhPr4={{vw5K|nNd3mq}LK#mmntzY&}N|@gnoK6aGMCglXOsehY28FHauk z>#YO)uqGV!{Mad^T-%=q96>lQ6KEnD5n#!R3gyF$ZcD=La*LJUHsLkVozNtd=h>u- z)?R~Q!YK-sZ`tz{f+pXtF+DV*$64>s6FIHgr4bIh`1a`bQM)yp6-Sb0;q@+>*S$3G zfiBUHBl-BYcsP`MdLTsh&$B64o|+v{-Po5n!T>I)$BX+N#EclXbC*c>JH`yVLEZ39 z>nN*G47x5~p@+vrQP0!ut8i=2VPKayxW`9@Qk{XMgb%FN1Np+X{mJa2refvmVbvon z(>CtHcx93!Jo}|w8LhpD;glx!mWY;YNZS&9IMWgfTZHhq( zkY|7KZo1L|w+^e+#d!N+O>8q|2lwA32it}BbM>5$`E&&3RR&5b+|RRim}8Le(>iaYRQ5>agBb#y z=K(AQvwcG4yMi8gX?T?Zb6t>27}qCevjU-;=_G;W`QbXMrQO8RYo&4H0+4 z43eT{==Mq9S!*WCgX}c4kLC`qWovns`C~ga7kdvQ24rCix*6DbPxxo**g_}D-9E)A zWL~DbTAXHSSUkdEwA^?N20%Lv2Sl6insqB47GaZL*|lom6c+nw%|JF!H+Aj0v~L+a z)}oE89^{in!Dd%22r}+@Vb}qf$jEu3>i`}BL#~`%^zYXq9HbNWVVif&2hNp=LSyw3 zQD{ZHlc_w-ziG$Ncxdw+RorK^B9Ols$I=)1uF%@{xEI|No82|Kl^5138cIfU^^!3? zTQb&T=c*$kuD^GcE!$9hHU)lt-mFWG_y>q53f>g{ohO8Bn-xB_)f3&O88)LxL7Mq6 zmJN1_Bm|FvQVG^OStnR+ax#(4qOL`v64KUhHXSYgzaf6C3-IqJE2p0J_8qKo@L&GK zh0e>+YyqIQD5mJ;K)1TQ4<5_1vXAiI-b^5&u}8ut+RVw4U_ML7{uE}y{WgWIfz<6p z5OxTb%an|_krCixIv1!Npb`%M#=7NDKv2`Ij`JZ>NH!@Nz9Qo$g`9lJb~GA=>n@qa z*x&ip8Z37Sb7|_)J!SXLp@Khj%e-J_)Ei$iTIRgq_YXU@vA{zNyl(=?W_{8bGWV%l zHPrsa`2LK32;P3WsQ(S`(}O4glhDYRB%M`ILmUwDG`1bO=qdE(L4U&xi^Wf7>$V{k z(F__moa9IJh5Ylvzsr4bhawk_EIBd27x*zb3(ixGk6(9!)B5qo8r#cV* zY1fZ|F@I>szZZ1ALGaXtU9B%YA`? z6xO<>Q8)(`u7#GFw~aeNE-jRTbQ^x#-njj8R$}fnVM)fS2?_1iDkXKvlp4+3XBqh(R#l5;@+D6ZuPS;(xvt z$aYHd^+MbP%@aOhheXSH*`Tky3E+7jPE%#HCBKoXOl@ylAs0@7IO|xT`~hYD_2!NZ zqD&O9k2KaK*L`&7{X~3TikD7&K6(|+xx0+U5WW8EE9Q`}^$5rWPH|t>^8bsi`s>93 zB2?6hLAi&B2%V?2Ga%{sr0f`JOLNXGwbu`4NKP<4+s=07Un@jK|3ut>I@l{|rU2Mf z*I_v5lOJf!I{;D#>Mi}ft3DIYHSnS{E*7NY9QfAWYv zo%|oB1m9agLp-BD&k?*Kk?}$H7g8-q1j{g5=9AbJP4_oAoJkAKFfuI+PT*t-*OKCS zIsZs~7*@KnHb{G!t#ukMY5G>SW$rj@cc?uUm0n&FQx~+C&l2nNA6*N`ccuclzX3#A zTH-X6V?-|t4S$oUjBSH!TTigjLTjkaqQui$ z<)^6`TAkuv#tNEu?K9d)+~(^BpZbRFj%Kh5+pL3`cs#_4%1wwl{_Ep@CVjXG${;og ztWF_U$bY?*cJDd=A?UZ|V2yFrONzi#6Olp}9>b9Z<0UH_35UbeLZ!TahE3bw-XQPl zWZLiACf%gP^BK~KWf(Q_`*4Qz0&RZ%S^OEnNi<_5e$*MN zPFsn3y|U4GDmgRrtlinmBirke`ka`wW4MNyWq9{SGM#P<*OfUzBBe@M!2-3Gr&b@s zcaaDzB86X43CN!Vhili&?QV}|8{&X$6MNn2YF>y%9KGSy7i@03T%fi}Qbk2Y1B0u! zwwA$VN(h_LHO@h+@n}pXiIDPr0yw=CScsT=Hc+aG&?hQsF<2`Dv8n1S(qr?9?PNO7 zmTey=(!uS*va-1c+Xz)n%~B03>v2lrzX&d@GTMsXTg>$OV@rJiWJF$bbN;vs;&hVN zOhI&&!6YcV?NuB^>(^!GVr4N=QPKYwmxXl#e$fIxUpk(bBN|EL$_7m05!ck@n)s`< zR60Y`dt=M>$K0|%8h??h3jKir#bkUQmsq?wSp@~J*w{qMV3Un*+ zprH40_ao%S?Ym`gV(TiH>RMph`0haXSl0Vfr$K_pp8mY*Y6S#l)_p?)mXdEk7=>aq zNS%U&uk$_um<8vprE!WbmtQ3E)7NU_QHN7Dug&b`G6f$v>a2N^MMd zS*=&Bg^Zhd*wX7iF<}>Wz3q_G4pePbI|rR&bJXz9m1vMN8cR8`?q6flG#vcZaxVw6 zi8Qqm%`h!gLFPtxBiMEok2cCvIG@@r2KCrI_B6yTPh@+o=Pz&a!=?T#Hmsk0svr*X zB;_>~KF>~&MvKsU=&2)gwu-yL`IE?)rEEyy6Kjba#OYwdc`->YoZjE(*l%HBPpy$9 z($)@V_t9UCzxTgWzuzp}?|`-6HeVg&L=I`zuM+)C_vl1-J5{R<)hu<L==ND=9 z%$9XL_qqV7{V4Mns|2JW_qNX4&$Sv)=in*W5QoP!o1bPhT4C zXRcV(wAPo}IgK)xr2xS9#>`v#1;kx)I4uJxM6Llzr%rW%JoR~S?5e1pZH23}u_&((*Hyjw_afDiNxabmQ6tjUbJ58Y2kIb~HrW*( zKIWJK4s|b&N=roSa|Akzn@%hEyhHsp)A+!ouyKlz*|=#Z$DDO&{%!v~s@#2-lK3?3 zDmsYg1FA(?lDV3Gx!Tjr6Z=tkY~w{oy;v{0e}0Ip(CYyyuOmi>I0sy9(g!?-TrHmW z=JwCBZx^Cf=J!^{crEW9_eIPWG1XWC9XxkEu^g$D%kXKhE5^LXQ@RbbRN9aT_;Cd9 zJEn~V)>bg=aPa{Fvs6}ML9d%;9(X+NfOa6FM%uyPbh2Qpf(O3Y8?8rv3X0nMWST3j{;r8^i4e;tr zp5XYR>v1tpR&7tgC(YG8^utp#2g4wmCYj0B_hjw)7itFc(;d9tZ6l`ERMzMlpn5Cp z5`j9((b3VaA+HzuahauL*nID{j=sF&N7^O_!v|1YL8Zwy#-}PG2duXilTCNy!k`Ks zkGmfcU)X-LG`~+7SaQA$Lj&k1AqmemFS5a25nd6MA99vt>4~$->XgTqO)=A1Mp^(s z^R_L2=%+1%1h`qAl|`4{R&Vz2E81YH{eCb1j!waT@)byq938@I@dSc8=w!4{sR!k| zI93igxM{sS($VT%Y~X)&?3&4&sIXhDJ+qx!R-#nGY%&}lTfJJ0d<*5eO*QT*4f9OQid~iE!ljM1L3)%thz@>s92rY9E^3q6U-%s|-(wtUZ^4%&t&s?l4 z;fDj;vvh+|iz*yQRwFdsV)&DvljiGLKPms{L6UN2!J3z;itOU})lDoO8+*6;kapc#Wd#&_`Gd;;>CqiUaBmrVLqhcIZ7 zl}iC6Dh_X#7#^mKLP7qt%9W-F^RH%UKSLS|)$7lrh@N5$F9lvhzO=4%;(0)K2c3q2 zT$|Caf9j4Sq9W?MC$RW_spE&q*L>e{SOylXwG%`Juc@4dbe5FkdeQ^UD&;xV6H?`H zDZp-jS5jQPaOrW!0v6J z4@d(;D^jzQvoTwM8jdp(=)=mJ+r$Ix@C|IZR+E)PA@>*tfo1-$meSG5Or7+U$bgx1mPTWVYJ;ZE} z`enJSiPb~4XU}u*t`*}9iGjm5!rMBDuy3(@m3M}V&t@}#zMe_^2|-YDw38UZ_5(js z6A)ZR-1N!nMlz{j3&8j$8M}f(Nt=xE+&h&h?^OvLX%$Zb4>Bs7(8WfrFrTV_i*;B9 zg=f#W-Da1TfiCl*hLQ$A+CA+9C5CqK7Hk>X8jY?`fx5!L>Yn&!@5RXUjND?KQOiob{0j-C?9mtQfenoSU!6Jyc7p(~~*f@%=Dbu7szm z=z)6(_+}B9V15AMrrZysMW}iYsoiVOA5715#A_v4K0Po_3|DYJKI=~f`*%k|3 z!N#$l;tJGP3F%^R(K5jvUJO}A(?(xdVb~@~gkm3KH*J@438jswD!e=oxeiSUzA9;U z=e9K~b8waoB)-2$UhTfWXm8Pj8=s_o^-h?Id)hR{J zOFzW3Lu)HSvm)$Plxo*Ho(?^~fByDi zZ&+7&-VXOFLJ8SBAe=642De1G0QLHLWHTSIA&@0c<^?<*;Falk)`dSu6Z}H} zh{wN5Yn#31{Uu$Y#%eP~*vHm`nzPi$45dq?aqWyj*QT&8=CpKcwUnZ)mcxpGX&wpuo0eYYSceRIekE+I$*=ZtRB8^sZz}m%mepc7mJjIrY>6yL%O;oGL}Hz zYz=9#a~L>Nm>H<}jR1J0i;iYuY*oF~<*6xp@dCl;C0c1h8K1fmW^#XOc_?8@s}d48 zAMhu$I2NHg58Vqg;k6jri2OdXp!zI`9;(Rx`1lT~qp0&LDK;GoD{Ti09rQ5m} z1cK&(n5w!Y)F4O`stw;Dj~D7WcFSo8F%xt3GbeGPCpw|2H=R{+djL143;ma>rlz6% zd3~S`AfnnqjGU?(K=%=-NGY=Xzls!Eq|+XoS#}O4?AK0D2Jqd@e2Ecs%JC`xWxYem z!ghe?3~C)BuP|uwT?@NCE-aKr30qiJFB?A82{0RiFi@5f$vJ7ncJ)C9~H!YQZnkUtc(QWrkoN@SmEUSMI76~C)L!)V893)d!qm^86 z49pFhkVR3Wd_*jQ^us36ae)v6WvSk)eOc7rTD5xVweiDiM^TOT`_J}TO;R3$*ERhs zAUT5iQbrc|glEN`UA*zukvCWwT>WqXht)s(q+Fh$6wLDS@hdL=Rs9i{Je;b7OeB04 zl-C|z%YD3di&3*xSXMYmY9g%?i;tfpl<#3jr=XR|R+GAN^K}@ytJC3&NQ3;9_2hTm ze<9#5bU&)ZF>@<7l-`(%s}c2Prdc8sBL)*29alibkj-*$3uKEqe73xa=kF@fhs1v z-qI!vURO!Ch0sUveQ~NrQ@f3f_AkN71;hL=dzsD(B~b`MZd||?fVKeZmWHRvgcqiE zgOI6M1}?m>OVC`U+^(@gqRL)PdF~Q?cH+d_wI-tjZM};P7x?TtGpT_T+9WJ9oVqXN!X|xa9#x}PN=E2^JYgnZco3auO zoX7c-8+O^*UkXj-S>H>f64dkd1%B3~mn2e>iVkeidWf5q?**n{6hVg#^0DDZ?(LyV zB%&h>%ZeHY!3ft*t?@yI8I)5TOHfI~S&)3imQB*0C4_SE7Q>|cdWjCIs{E<3SWQ%0s7q7;WsVMKoIWd(cF6z-N0~=FK zT0C>)TSnFWYDVn$G{mNj%W}!pT}g&2mqe!b=DaDETGhTE0CG)Bl{|iGyS!jorkuQ8 zG3?UApFnw=;f?fSJJN`gTQTmQ@6otM!s%A87RpzEwix*O#j3!qv65ne2U$5FIcYz* zUEptkAFXKHbs12wQgZf zGy5-ePDdekh{Np1q+6#TpFBYAD=HjcKc9RN14JlDZgd&9B_vkAS^T7WXjNf8Q94X{ zFPSOPq88K=&v;HwjOGNci1a>qPtQ$v&*ip+-2W_)U+9pa3($OUC-bCJ8C!iduNAxz zdQesLeiE{s3iWLq#ZTXILmS?Mb9!W^_WuZ7A`wU9NoP($oE!jpfs35>wtXy zKR^~18aN@_48MK{&M61t4dk6;9#&JQR0maN5oa1BQ3@A{|9*%xl#(Qx8j{e#H z=`*PaIp1@qZRS_zW6~uElanlyACarUD~7G4cRC6X9fDJ^_+uH3yO;q(~s~QZ8m*V!>F>5(rXX0V@cA{hBH*K;k#JFL!{*x-$jJq|K&Z zBS#NM01iHKbtq#up;i6k=`VmkOoLWz54vjlpV{*d-kgUl7XlK$DXlqxa)hW24pFdA zcfk7>x(^xmJUZ@-PN9^+eu(Xel)?}B`uJZ})v+w|w?IcmzkoK2nT&7!|7-8Nqnd2C zcL70Ax>ThEEFjX0(g{UCMVf_P1w?u$^nidMy(lOxARtKZy(m?BCv>EQUPDbnk{iD7 z=<)lVv+lZU-Mj8zviM{2wwZa~nLT?y^X$Ek%Uz|(zc=d3a^X6a$P})cy!FFap;<8GKmas&v+KquTt3n3c#&&Sf^a%*IE?<_|jg%@_Nzsz7iuuIjq=(M-6OB+~YL#D}bAJ~YCkWZ3 z4+pqo))>TJpcj(WNY}o~1b-|nH?1V1!TAN5rONiHhT zYjolhg^DaqH|JVp`y&Sbjv(qXWTan;nmosU1ZyPCIQnkF}iV# zG`WV6eKSuYe|2Yj@V>Nkqx1got@VwKuq9GYr3gkov(5-j6XvDQ{x;t?{wGx z!fh=qDp{ack?rS>B*lqK1-gjEpba@c=B2TG?baX9u(_HyEcy#RmStOcTl-`aKnwKP zaX{RL3tv%nQ}w1tQT1%^FO(vk@X&Lzi;`2mmFP+1O`;n(cc+4!HU<04rEX|~AHStw zpIeNxK+4Es5$iRM=dJxcO=%RKE*&JgB@2ebUdXJJvwd|~BjwBuL+HkGJA+YNIH&se z@3BjYLWFCLCk>sKns#>VIa|*CE++Pu;L+GaUT9m=1G3MRpotPwA-k#3irlhRHtT%4 zJ8c%Ks+Be)hyw=mo~rF@<4gDXrwD=f$%6W`$F(Nw?34jRuOdI-9EEsr6x6pZQ+0YD zML|(2V6EWW%u$GA^356VfV3S}T>)^Kt)Fi=O?Bu%H&88jqT50x)nk49ES$atq14c!BWMUzpW5*?|WFbN@a0$OklS+R*qk`93JfUHQKM*1+`aHFN8+S| zU5vx>D1v~O;^9K4@jKq{nsp~_G5OAg`AT(B5%K-(-WDBjee+pqCRp2BnleZ9o!Zp^ z)N<*rpK;S^2)5p5IS`QW#q!N}1`4%!aFUM_1zBJ9T5x4k+4^(DWch%&9H=L04E_VX z>ypq1kwHiJz-+)Q9KI@NILiDxob^;o`m*J8zswo&1ZN6{F>6s1o~&Rl@Ubq^Xw}^c zXXxH|-;??(NIvLKXw#A2EJZIAgZ&4|Jkl0l7RUB``$*fb1UGEW&gTJuPV1p{@P}0qz z_i6B7+Zr{+3*b)Rf1t$w?`53Ko&af^Ji@^Edh>*rY`~c|JrG5U8lS#8)~MllT}ah2 z=vo^BWqrlizGJn^i^W?)@q6yXXJf~?!H^Ewhy{A;?y#6^t{#5z%M}N4uV=VC_pjR5 zORFV&cnF=JP$dqh)vz7Ci^IX|Yk9gjNR}8JDI}RgFp3l+WJP+alG_^_3Rx`@PBxyX za?04aeiv%naItpVjcNm*dqr%susFGtMV&irP*hilWq_0QO8P8d<*wQGA^F*W|A5!8 zN?X74j&(wAUESU!W(JZhbaZ5t*@b|`OK}@dAT`s{kk)XDL&?KtNqa+nKF%)-Tnu2PECV33DIbM|<#)1z<&`3txj^$rMPia|8=W%n64%^Y8>O6nKUSWeyF+lY zg0`m$VImUv=W$`gG_P9r?un3H{Y1GJ>Acm}^_okN?69oLW;v8Pjy9d~O1Q|Bo^z%;KaUax#DULHSguu9%D~+AT0{Y}&h9=O zcEqc?1H0V9R0%N+y*qI>G* z+JjZYNviw@iL~iB-w`sL1hW#;c@oVET(~cwMD4oy>g|ipC87Pmp9`Uh1~tihqBO}A zQW0XJwRDM8#cNEd1}WxDd9Q{?LJ$inb5AyU0tkb|XTY>v^$$k(-{cFKKgo9mI3#Kt z&EAyOGl`~XZJ!6R`abh|Cbaa8eCOEEX#`~d`;VPXYdhV{pfUB*n z^RKL(>2oV8=r2+`KAAS?H9cM0Gi|jvVA7x2)6BBdUbswr&e1nr2z*Za>7J_r!~ZjHCYFAIah9M~ybG@#ogN>fh5uPijp<*?LPAS#+%E zcMjKu_q(elLAy>0Nv3_0F5?6FytL8IKR;B3^kWB%^MS74A-7JM>7*!ec(Hf{)f!DP z8p_j-!|ELhV#ul(g(#e&5oPw&oW7G6Ht#I{*n_t)YrRnO{u1|`v&a`0`bgu|p@l(p z|2l)__`c5U%rE{I*%gA=$SzX#<_nF~q?*!Xmj~sX`JN`P4}7KpW$g7LQX_@0e86l6 zj(qvfAdVGk>D1K$S9My~XT5w}e;84-^=5kRxG1uYt@kdc1m(I$69kA7hRVE8ch1gJ zlXw&P-a=dS!AJ}Psn*D_a~rR`h|j42B6S|AT8lCSb&EEwYk#YJjEg43ewBc3k$@t6 z;Dc~}A1*l%bPSnDlNuUwlmn!&Fr~gOC6yUfTW-1O^Ta_>m76%f>O~rhA755Xt3iF~ z9Rj1kTilE*mu#rDyn_eZ4U}pGGnH%YPUBgF;ZP`aUER6Y*=383(L~x#9M=c~!q|X? z(*HuT|HjfP+9xNnFkyl(8gTU-+22^|sM7HH(okq`t77Nj~Appu{GbWfIpgiIVP?fvadFVm|zt0Q8UI{ zsB1*^OBxL6c0PTCe*DXwRKTAkx4`j{0;IiR?J%6G_Ui-Z(|yYexqY^efFy)k>OJpi zAoOw5D?FJpy`alS#IHpX>qO=?E3L;PcHY^zN%teI7R$9;*%dT7+c>KV?~<2Q`{nV} zO4MexOi$tRQ70S6BP5tX>?@^2MF_`o8vAo*q zo(H)-e@5=wuPFM6~)Uqwo z@l4>Dp;s|}gl>mtrj_`fqMsCKNUDdZ2L?0L2&XDic3udGOuUc=CE>K!kSUm@($n3~ zEk2J;^dNc6J?o(hY(*u(?IG_wsx+!^jCGgU#a-S2{K6}A(l0GCyYLB0T_vUNOA~(@ zGUlZsFf3aUJ)U@tTmnpcgTREky%A5rwsoQUfqqyIjt(zeBbp9;9b8+mRO&$BAGroR6IhB0EgeZ zRh-sw3)L3sW7>IdSj-1|maW2>#qVL%5aH{TT?J_)LMLZd>sy|DG86~Pw&gO*?TJR? zijTCu*UumEA?m@WZ*GkstP^2x0aCYi>rWctOb|>KjU{R{sayxpyTB&Xj6!$90#Lj0 z=VYeNAKzm$;WG#PZ4tDFRVp`IVpKy#rS3N$cW8A|18H6ZNuKp;x4#BbeYK((2P79) z`>e+`+#HZXHPAf9nU1({)zI0psY<3+@V{XFCDBKMqb^I^)pb&gN|RZn!xN6@5v7jo zG+DFXt>RqYA3ZcKz&wG6Y%2iTV^;qwtiLw;FIe9+LkYn3GosY&b4FjClMRQal87vT zZ^3Udr1Wh`@-MfxkPv@s%(klMG5pjZ!n)2b_N~WY8rGxMh9k9Sg$%1Up$u@}sC!r7%iDQ^$!U0OcW#TjiwlEO5?K@0xl<%ti;_7gi_5 zjXVyO_n7-Cs}U~|gDTkD$;sL(py!c?_pBQw^c)`#FbQFM!7)>FGmna4j$@USuE zrCN)(Eha=8>qmyN|7-ikF|s1eVxa!XwvClY=)M z7Q^E2xfgRaO-Dqq8bXswG5}^%oH=R?JniLPT83&v-V@Us#0=f2&qg|DrfAwBhB|@l zs!Q|_cf~4u^GvfdHfngp4mgDhxGf{)yo;c8-Vc-~t$>kEjc!eJ2tkHt;FU64?FTWd z5NuiaK(7vNzarvqPWoHVs8jCcvc(Y(25XfJiMj5-kb1B1(nl)b8!cgDx;zwamMjC2 zuK+#2Af#M@fLG=gB2TGWE_`ZRrySvUUg58Gdb5GNofDAcf_iy0nwF7@?^0IM9@Bma zYKl)z>9qgOy%sd|91#v#r6-iQdlvX4J0aL2>NNb6nlPJf#$N#BK@wHFy zs*+W9gLCN24Cx98M%;7WA>7pMOMMkN=%Erv z`#B-{KklqCA`ov}Xn$#Mtm6KuB8=eBi@kXkaiErWk3-$(latq{cq^ZhkF9qp3D*9> zvBuG?f4rgc*W_tt%cXoprzd+*OP1G|HSw4xDHxXB^gc9dyfK3DnR0)}FKyy)%$rP# zz{pfK@#ft($wYYpIPS%6!TXs{BHuoesWf?pl0sQcQonK#b~+vY10u(Pp0c%sDI|g} z+B)Cw7QJR#WQ_3BkyCX|Qlv$hdUzNljE5i)83QFd#65dg>D#I-zD;LYje4jUUA*w*^U@bvU%lt^y~o=W6n z4%=3N(aq!3hhr9wu$t(-c4eyQL;EAcHdM!{)Pmm!(K|yMyYa;PatKYh&yqdW0I~~$ zR_8C-^J-P3**QRlJKR`VKy!L6d~C41py3FHmR3k@NFj-2TnH9&<-ShZ&oddn{ zjaS9iyQ0z?Z_=lFS!h33L_M&iUel;E*4JhtWbfsaQ|=N!G85f!2!*fnMD7-O)P&N& z5O*hpY8|=7HPbsXZffU^MTad)X?j%yf;*q<)tM*d@{RAR>-AnSxUPlI1h>R49fjye zWyaLJj60!MZAGcOGA*D**T~jnYf<^gQ*@3Lyr~C2cpjKebxiJu3k|^limFDUi1qLW zcAc={JI2S6Ash?H$X&Qo5aj{%Ufh~r%55AMadT#j+P7G%P`7@LC5QV1ATZ6pQJ19J zT?6sritjYUD^ZI^SGmM7XyQPdj~=mjWsK%T77+S9cePf{_HCi?_4hq5@axFoH4mzH z!w=B&!%80uduwyKqWRjbuSG7L-wz2q@%)-!*c*Wi?n0m+8>mx5|3 zu4cczI_*dJ0Sd;WeN6CqsE8AWUydcJnZRD`JX&ow_Yv)|oO$aaw-W9^Lo=!M$T z#(Qmu3t1}~BgWJH@?#JI?R|UsM4DIxaUw$zOCOMCJqHW8rFL0;Tf=FpL~6d);3Xld z#U~`9yT?uA=3q6qo|+M7R2j@|E|)LssY8kk-yB_1FXQ%Rhapt~5Tq|?v^;?6+~jY9 z{LsP=L0#SjQirU4-u$qK>DdKrlN|d-%HXdaFLc#Ae)0`cW{6j;_YFEgh0FubFmH=F zK@ZnL3_0y3(|4K|81Sy;;bN;IHi4K9*N^~daH^#75Au=vtfcuIC*4>0M;aGVISlTY zHV*Vc=|?=pN2i4&^?R@84X?j12nHd`3+gO-HT>QTAqPlErNU{ig2=D zSIZe-~cHBfh9(}mzI&M_~{a`nWOy5*nvu)C^ zHL`SjVgF^kqf%L?!NXajh;gbBrpV+Ty!1(-2n0cX=N(8Gq z%`foy;U~@?)`5}+J8U8Mqpob)tq%=&U5pOjUsC2KXPN80D` zO}t$StVjUPtaLlyZC6~***6Zu1sRUlwwR;XJgJb^>foPQS?mI^e6MqqxpCaQ#~QaO z?3rjVM^Owk0<%#azu!v|0`sMv0Or6RSlEgByya9%%|0k{M*7AsH>U$`@c8OZN<*vG zY}^BnLG!QXub43S;SDr84bGnVLfW!}pkY~fZX))J(R&$#6f~w?q~o}ljRTpp6w;Jx zsA$8AA$w-n;!<=?eh0hhs%vl!h4qltI<@xg893e4XBcUVBW`H>Gn8DC?`oz>#3itn zv_iZVIL0-u0ZRx~b+jYQXu#4g#u0N_aPk^cb)YDBfxn$ub`gAd>Au>dI+=GWW~QI{ z;TqS~jUWf5wZ<1!Kd)fYZ^ZC3AHj$CucaI=zpX~U!~BSvbjw;>n?)%R*0kO30m*RR zxYV_Jc&4SEQMIyK+Gt>CB{L4mTzne~p2xr(3bw#a#fLC6 zsNU2)X!Ed71IGE82RZ<67wB8eKL_?{{(}p5xrpCXz6e3Nhu=Jp3k}&K@bZqn>8Il}yr}q3ffTizMZ* z2H?wvyls#HS8{g}XAmE&G6dJCGDkK(AT-M-Cy6KWi&PMiUld6g4}Zk~yQ1q6)A%9r z2z8)_k(+d6*x3fLL=j0qkT{c{H(nI#PH5F2O7gb5q>jp=ouk@Gkm&mBCz@d;et#`7 ziC|+#(1%m47otpaVCQ#n)!Nh18M^*r-QS{x12XoUlbKp<<{JYIXfqOWy+B`*lif-M7%rGe7&-d-2 znV7HjdmP@U4oPrkVN)#-K9x)ttOo@VbIwV;b$eZL;bfi4Oq8C^t7|SHq4WVRB*HXD zyk1Dhfs>AQ#$nf)E=nuPE`D7Xd}tkT-VB?@!K1hazp~>(=4p8 zNWCO|o$<<(cPr)2w#mKiC|sMe=fK;nkxjI98pc5j34B)v*l`@}f|ItsA3@DytZ~%?C z=x2&gJBkOQ<|Atpy19a(fOBC8GY}W$pgb=%VC;JJ=z3OTD_Hcm@0zAbL!rPIGGXwVURbx=&kMtukC-9LG ze>F7&QSF{s;q(E3HXT0#^jfq@z_G;L>3(CIL}+%Y@6ro~kHORQ`x*1_>2Y%4V4crN z#jrC0RK9?WQ>QhJ>6Qt)mXzs1g!Zt*jGYRmskjuY$nd11bhAso5V()*X|9Th-WO7< zQuJA?-t4bxz<8i*n@m`^ru4vNCYuTk7UHa z6G*D!=CjSNQ%3$JbA1MghtY{sRF(xuzn+5^7k~3XgF;o8ni(a`a{)iRq?IkwP5x$S zjbpeYch7L9L&p(YfJAt(19dAzn^C9vSRt>n76%E~ytb*Z&u(7vVIm*hpXBQHHLNE0jnjcl5np2x+Jo^l{ zeV2Sl7}$k`W;$NOy)%y0>Gpzcy zcV9votXX2su-xMg&b|?7{sw#bX2Clhf{y`6PxL+#ceub|A2Wbb_n0R^k1IdgRF=7m zqXb4yezWXVqth=ifIdbV;YgvladSxOsQ3s4I)`hEcYN#3C;Z*n9 z-onC4csMB$IW)IJEW|yBZE)QJBfzB78x%o$4?gBFPMVl zk_OHvd5!ZwPUJG;Umgt0u=w_CtMXTY9d{1|> z0vM;`U<{nG{`-IW3&Pv7+;0+y{s&JEXK@GN&L~{_&DZQ_f1X9)5xr_3EHn8BC+xgS z0Wr8U4+4J$0Q=RefH3NGyh27j`hNyVK5seIm2^CEtAD%7{N#q+)DR?~O{Y`ndHp|) zcz;59+!=QA|FG_H{7fZ(f{GWfbOrwN8$9s;^Kt%soIf+?&&>HF*8Z$Hf7YBo3c+s* z!B_13T&kb$^Hfi~Ia#`=g16Jt{mH_vf9en$IA{>jq}hJIQ(31CUTe<8Mih*tI-uES z8{?v?oLFDjIHtW|UL<>+&PEU>L_2|!_UPdq|AKF=pef_c1%is_%+>F@^oJm4eJ>Ia zl2Eb9`r`#y6A-+*s~3FfuV46oA5N#Gl2sx1{dvt_M{_yY{IxQlf57|KHU8&?NBH;^ zql28d=lnYQ=YfCT;on2+&#?Jz0{xjOf27U7#pE9W`+qDPwoyB#cz85q5AVx8`!j_8 z452@2$sbMZ?^*EwX-PF~EtCSyIklqcrfJRv&`(YGD`^6Eu`1r29J9>bKU(d%CGF4>pSPW6E zuYOag*aTh@5e^F0+xQCn;{klQ2e@uiX0rcY*9ST(+0Lz^qi_FsKpi0t*scUdy!pK@ zGc8r;*RDCygiB+tby6 zl8%O2myuDfrn}(_bxB;#RvKK*jDhDz{^mPSz$j@8Qph=Qu&+zB4mPm|~gcE#n4rJct(_c)(!n zA*bg){C54lHr9Ef6C`$#3N#>X2J1fTuiwo^0laFiws3LstB4wStLmj08+)T8Ma()P zIQ&_APbsoGaNaI)FN{?`N{RF8meovZi_SV4B4+_R>!_CW^1g2oeY-m}VlloVSKM)I z!`pdsKieXZN3>WmEs8yMuiHC2Nb;u%ls-5dNglbAqn?}qsYg6V+E5q|H154P5RZ~x z`QSQviy_A>`eVE$%xfeG=TW+(F7@&HXu|d=EGH}cAmT+z98l9LTJ*i-g7G!}V-!BbMcAznVHbtb4~NTth{U{ZP!e2@Mu-Sr-+sLIW8^d5QGtqUytF440f9u{Fs zwW&%Q?V=+mujSItY87b~;xVxTaYhf}cW$3o@hC zR@2ZUTW~iobfl<$=h(d8&Tjdb4u}7^#`E}}LdoPA3C{%{w37(8CxzUW}O)I~Y$V+;0db~kyGW+~8ivaGW9Ug<%` z3IJ>%Ar2`+x>5)TqC9U_VjFic%(?xlPB%Wma#Ch`_d3YmU;pX@U(bnDY_fc+*SM0o zVzxa|T0VB?rE+HD=`i>D7_=!FpJJd)seB0kjHW!A8c&kfHT%Rl>y5n-say~(TmOlqskC5ZkN#E^Z-LBDIsVm@`gSpXB>w%-8rWy1g94ZZLg_x(_GJb=uffF3 z*~^oixt9^1EnIzRck?e%T~uY6k@LA!Fzt|MaIS59oB^Gl6W>Y9n&Z8Ho!+y}Jm zpdSI0x2(`DkFq}o2UVy|r|hQBj9-*6dT~!k&38L!Aa@_J3K12~5M-LNvNOACi{A1H zbFBCfuw79zsYSRK&@nc7?AUNQJw##F`^ryEh1ogaG*e2AIRh}xQ*NrCCUv@?BKVP(!uo2wr3Uu5R?-svWu9k z=F~2HBv^hG`ntpFv*{IKt5kZqhLMY98@40!HoBS3M}fThwJ#KfQHb?w;gCQ(<09ao zpmL;`s`xROaHyYwO!y$eL!W8Q-c=2$Kq!G4dua6E$aG0&z)x3+{pT<6# zhVL6z#j6=etU39VpmKehGmbkc-4v#&fA)z=ERJxI5}Wz5-`2kBN{)R8izCA-w5G`E z?9}&?&F);IxJTdPL?dU-dF61XexLWn8R46f(Lj5nFyJ!SZYY7j94(z>=h3iiR2^1 zv%T0?P7+Q*lQw1A5qFzRPR9&EpKH1XWtYNEWX=~PvwIgLu7p|t0&gQE0z8V$g9=Z9 zdO_W8;s#P_4m=g2A>XrA=tZLeZ`~YLbq8M|hC4ZYq+(hvq&W8(s?iO)ReosX;bHl} z-RUjVYVyiO@%@6{8kem~ZNpg1=~snzbq97Q5hM~gLjxk|2c`NVrjS{7cCb;SG=`|NfbSXiNDS6|I@UPWv=tq;o6)s|=t9t$-1AAg|pV z9Q~A7U$m&?x>`U4aA&FRJ4SD&%>t*N^Z^dgg~heDp1rG^#O2o>MWU;}pbyR~@mNImFH{6XB?gR>uS!oGAS2e-olNeGm7^5B2yW-?2 zbDm%O2f2JDe85geKR@?{3M*-ZXZ(s1fz(UU8zyg5^_Rvu%n%t=jNO7$_b8%Gt^R2& z)Hrd$uoZrUi@I+SPTA;t_UJMlk5HnE5=zr=7HvNNuAt|U-v4Udr873~EzA3$sBm^9(v#vbECeAA7uBDE|ZlIJCS2u7Tbx00l7w30-u zo=jEQ>2GXrOZ#N*_y4jg&INv9Y z5kYV#rzf8F`$$!v%1{3mZhwJXa)gU7U&P+1udh#j+rKXE(f?6e@@QvWHD62mJqEBj XK6Y!fkg*XD_xDgy?S7HG$?N|GLNRpQ literal 20753 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 literal 29095 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 diff --git a/docs/images/gateway/plugin-settings-marketplace.png b/docs/images/gateway/plugin-settings-marketplace.png index 58ccf8155992e3b9b6a997d2e73ded562ccd322f..7973fa99811a3e763451cbe59bdc487e28a842dc 100644 GIT binary patch literal 115955 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 literal 58881 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 GdGa4E%$X$s From b67ece1cbcb7cbdc7776c05ba8ccc3a11f17684b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 7 Aug 2023 09:45:15 -0700 Subject: [PATCH 028/156] fix: image formatting for gateway docs (#8947) --- docs/ides/gateway.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/ides/gateway.md b/docs/ides/gateway.md index 3f2afbd9b55c3..89c793dd3e3f0 100644 --- a/docs/ides/gateway.md +++ b/docs/ides/gateway.md @@ -18,11 +18,10 @@ manually setting up an SSH connection. 1. Click the "Coder" icon under Install More Providers at the bottom of the Gateway home screen 1. Click "Connect to Coder" at the top of the Gateway home screen to launch the - plugin ![Gateway Connect to -Coder](../images/gateway/plugin-connect-to-coder.png) -1. Enter your Coder deployment's Access Url and click "Connect" then paste the - Session Token and click "OK" ![Gateway Session -Token](../images/gateway/plugin-session-token.png) + plugin + ![Gateway Connect to Coder](../images/gateway/plugin-connect-to-coder.png) +1. Enter your Coder deployment's Access Url and click "Connect" then paste the Session Token and click "OK" + ![Gateway Session Token](../images/gateway/plugin-session-token.png) 1. Click the "+" icon to open a browser and go to the templates page in your Coder deployment to create a workspace 1. If a workspace already exists but is stopped, click the green arrow to start @@ -30,9 +29,8 @@ Token](../images/gateway/plugin-session-token.png) 1. Once the workspace status says Running, click "Select IDE and Project" ![Gateway IDE List](../images/gateway/plugin-select-ide.png) 1. Select the JetBrains IDE for your project and the project directory then - click "Start IDE and connect" ![Gateway Select -IDE](../images/gateway/plugin-ide-list.png) ![Gateway IDE -Opened](../images/gateway/gateway-intellij-opened.png) + click "Start IDE and connect" ![Gateway Select IDE](../images/gateway/plugin-ide-list.png) + ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) > Note the JetBrains IDE is remotely installed into > `~/.cache/JetBrains/RemoteDev/dist` @@ -42,8 +40,7 @@ Opened](../images/gateway/gateway-intellij-opened.png) 1. Click the gear icon at the bottom left of the Gateway home screen and then "Settings" 1. In the Marketplace tab within Plugins, type Coder and if a newer plugin - release is available, click "Update" and "OK" ![Gateway Settings and -Marketplace](../images/gateway/plugin-settings-marketplace.png) + release is available, click "Update" and "OK" ![Gateway Settings and Marketplace](../images/gateway/plugin-settings-marketplace.png) ### Configuring the Gateway plugin to use internal certificates @@ -116,17 +113,17 @@ Connection](../images/gateway/gateway-add-ssh-configuration.png) 1. Click "Test Connection" to validate these settings. 1. Click "OK" ![Gateway SSH Configuration](../images/gateway/gateway-create-ssh-configuration.png) -1. Select the connection you just added ![Gateway -Welcome](../images/gateway/gateway-welcome.png) -1. Click "Check Connection and Continue" ![Gateway -Continue](../images/gateway/gateway-continue.png) +1. Select the connection you just added + ![Gateway Welcome](../images/gateway/gateway-welcome.png) +1. Click "Check Connection and Continue" + ![Gateway Continue](../images/gateway/gateway-continue.png) 1. Select the JetBrains IDE for your project and the project directory. SSH into your server to create a directory or check out code if you haven't already. ![Gateway Choose IDE](../images/gateway/gateway-choose-ide.png) > Note the JetBrains IDE is remotely installed into `~/. cache/JetBrains/RemoteDev/dist` -1. Click "Download and Start IDE" to connect. ![Gateway IDE -Opened](../images/gateway/gateway-intellij-opened.png) +1. Click "Download and Start IDE" to connect. + ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) ## Using an existing JetBrains installation in the workspace From 3b16e7112dbdb7010c8836990de8d46bae68a28c Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 7 Aug 2023 10:34:41 -0700 Subject: [PATCH 029/156] fix: improve formatting in Gateway docs (#8949) * fix: image formatting for gateway docs * chore: fix some more spots * more * fmt * space things out more --- docs/ides/gateway.md | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/docs/ides/gateway.md b/docs/ides/gateway.md index 89c793dd3e3f0..b0550c378b668 100644 --- a/docs/ides/gateway.md +++ b/docs/ides/gateway.md @@ -19,17 +19,26 @@ manually setting up an SSH connection. Gateway home screen 1. Click "Connect to Coder" at the top of the Gateway home screen to launch the plugin + ![Gateway Connect to Coder](../images/gateway/plugin-connect-to-coder.png) + 1. Enter your Coder deployment's Access Url and click "Connect" then paste the Session Token and click "OK" + ![Gateway Session Token](../images/gateway/plugin-session-token.png) + 1. Click the "+" icon to open a browser and go to the templates page in your Coder deployment to create a workspace + 1. If a workspace already exists but is stopped, click the green arrow to start the workspace + 1. Once the workspace status says Running, click "Select IDE and Project" + ![Gateway IDE List](../images/gateway/plugin-select-ide.png) + 1. Select the JetBrains IDE for your project and the project directory then click "Start IDE and connect" ![Gateway Select IDE](../images/gateway/plugin-ide-list.png) + ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) > Note the JetBrains IDE is remotely installed into @@ -39,8 +48,11 @@ manually setting up an SSH connection. 1. Click the gear icon at the bottom left of the Gateway home screen and then "Settings" + 1. In the Marketplace tab within Plugins, type Coder and if a newer plugin - release is available, click "Update" and "OK" ![Gateway Settings and Marketplace](../images/gateway/plugin-settings-marketplace.png) + release is available, click "Update" and "OK" + + ![Gateway Settings and Marketplace](../images/gateway/plugin-settings-marketplace.png) ### Configuring the Gateway plugin to use internal certificates @@ -97,32 +109,56 @@ keytool -import -alias coder -file cacert.pem -keystore /Applications/JetBrains\ > these steps. 1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html) + 1. [Configure the `coder` CLI](../ides.md#ssh-configuration) + 1. Open Gateway, make sure "SSH" is selected under "Remote Development" -1. Click "New Connection" ![Gateway Home](../images/gateway/gateway-home.png) + +1. Click "New Connection" + + ![Gateway Home](../images/gateway/gateway-home.png) + 1. In the resulting dialog, click the gear icon to the right of "Connection:" + ![Gateway New Connection](../images/gateway/gateway-new-connection.png) -1. Hit the "+" button to add a new SSH connection ![Gateway Add -Connection](../images/gateway/gateway-add-ssh-configuration.png) + +1. Hit the "+" button to add a new SSH connection + + ![Gateway Add Connection](../images/gateway/gateway-add-ssh-configuration.png) 1. For the Host, enter `coder.` + 1. For the Port, enter `22` (this is ignored by Coder) + 1. For the Username, enter your workspace username + 1. For the Authentication Type, select "OpenSSH config and authentication agent" + 1. Make sure the checkbox for "Parse config file ~/.ssh/config" is checked. + 1. Click "Test Connection" to validate these settings. -1. Click "OK" ![Gateway SSH -Configuration](../images/gateway/gateway-create-ssh-configuration.png) + +1. Click "OK" + + ![Gateway SSH Configuration](../images/gateway/gateway-create-ssh-configuration.png) + 1. Select the connection you just added - ![Gateway Welcome](../images/gateway/gateway-welcome.png) + + ![Gateway Welcome](../images/gaGteway/gateway-welcome.png) + 1. Click "Check Connection and Continue" + ![Gateway Continue](../images/gateway/gateway-continue.png) -1. Select the JetBrains IDE for your project and the project directory. SSH into - your server to create a directory or check out code if you haven't already. + +1. Select the JetBrains IDE for your project and the project directory. SSH into your server to create a directory or check out code if you haven't already. + ![Gateway Choose IDE](../images/gateway/gateway-choose-ide.png) + > Note the JetBrains IDE is remotely installed into `~/. cache/JetBrains/RemoteDev/dist` + 1. Click "Download and Start IDE" to connect. + ![Gateway IDE Opened](../images/gateway/gateway-intellij-opened.png) ## Using an existing JetBrains installation in the workspace From b2dc8897ff20f7610a2dff7491451d36b43346d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:16:03 -0500 Subject: [PATCH 030/156] chore: bump github.com/go-playground/validator/v10 from 10.14.0 to 10.15.0 (#8941) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fa411a3263e27..5715e42cd08a5 100644 --- a/go.mod +++ b/go.mod @@ -108,7 +108,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.0 github.com/go-logr/logr v1.2.4 github.com/go-ping/ping v1.1.0 - github.com/go-playground/validator/v10 v10.14.0 + github.com/go-playground/validator/v10 v10.15.0 github.com/gofrs/flock v0.8.1 github.com/gohugoio/hugo v0.116.0 github.com/golang-jwt/jwt v3.2.2+incompatible diff --git a/go.sum b/go.sum index 7e721762b664b..3ef2c00c75a85 100644 --- a/go.sum +++ b/go.sum @@ -334,8 +334,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.15.0 h1:nDU5XeOKtB3GEa+uB7GNYwhVKsgjAR7VgKoNB6ryXfw= +github.com/go-playground/validator/v10 v10.15.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= From 827de08007bf42855ca13468295b820d087066b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:30:14 -0500 Subject: [PATCH 031/156] chore: bump the golang-x group with 3 updates (#8940) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 5715e42cd08a5..2bb28a73b9d28 100644 --- a/go.mod +++ b/go.mod @@ -169,14 +169,14 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.2.1 go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 golang.org/x/mod v0.12.0 - golang.org/x/net v0.12.0 - golang.org/x/oauth2 v0.10.0 + golang.org/x/net v0.14.0 + golang.org/x/oauth2 v0.11.0 golang.org/x/sync v0.3.0 - golang.org/x/sys v0.10.0 - golang.org/x/term v0.10.0 + golang.org/x/sys v0.11.0 + golang.org/x/term v0.11.0 golang.org/x/tools v0.11.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b @@ -356,7 +356,7 @@ require ( go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect - golang.org/x/text v0.11.0 + golang.org/x/text v0.12.0 golang.org/x/time v0.3.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde // indirect diff --git a/go.sum b/go.sum index 3ef2c00c75a85..5e85b47943f66 100644 --- a/go.sum +++ b/go.sum @@ -947,8 +947,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1035,8 +1035,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1047,8 +1047,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1146,14 +1146,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1165,8 +1165,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 758c36822221c4254a51366148270c6bb1fe3a6b Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 7 Aug 2023 21:17:39 -0500 Subject: [PATCH 032/156] chore: fix `TestTailnet/ForcesWebSockets` flake (#8953) --- coderd/batchstats/batcher.go | 1 - tailnet/conn_test.go | 2 +- tailnet/tailnettest/tailnettest.go | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/batchstats/batcher.go b/coderd/batchstats/batcher.go index fc177fd143d6a..e7c734862e8bc 100644 --- a/coderd/batchstats/batcher.go +++ b/coderd/batchstats/batcher.go @@ -13,7 +13,6 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/codersdk/agentsdk" diff --git a/tailnet/conn_test.go b/tailnet/conn_test.go index 99a88fabb2263..db526bfd3da02 100644 --- a/tailnet/conn_test.go +++ b/tailnet/conn_test.go @@ -99,7 +99,7 @@ func TestTailnet(t *testing.T) { t.Run("ForcesWebSockets", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) + ctx := testutil.Context(t, testutil.WaitMedium) w1IP := tailnet.IP() derpMap := tailnettest.RunDERPOnlyWebSockets(t) diff --git a/tailnet/tailnettest/tailnettest.go b/tailnet/tailnettest/tailnettest.go index 655568a341ccb..16fc92591cbd3 100644 --- a/tailnet/tailnettest/tailnettest.go +++ b/tailnet/tailnettest/tailnettest.go @@ -77,7 +77,8 @@ func RunDERPOnlyWebSockets(t *testing.T) *tailcfg.DERPMap { handler, closeFunc = tailnet.WithWebsocketSupport(d, handler) server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/derp" { - handler.ServeHTTP(w, r) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello")) return } if r.Header.Get("Upgrade") != "websocket" { From 7e3ff5b66e589c37b25f22b5610849679d8d938e Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 7 Aug 2023 21:55:31 -0500 Subject: [PATCH 033/156] chore: fix `TestBatchStats` flake (#8952) --- coderd/batchstats/batcher.go | 21 +++++++++++---------- coderd/batchstats/batcher_internal_test.go | 16 ++++++++-------- coderd/workspaceagents.go | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/coderd/batchstats/batcher.go b/coderd/batchstats/batcher.go index e7c734862e8bc..7185d251e19b6 100644 --- a/coderd/batchstats/batcher.go +++ b/coderd/batchstats/batcher.go @@ -125,6 +125,7 @@ func New(ctx context.Context, opts ...Option) (*Batcher, func(), error) { // Add adds a stat to the batcher for the given workspace and agent. func (b *Batcher) Add( + now time.Time, agentID uuid.UUID, templateID uuid.UUID, userID uuid.UUID, @@ -134,7 +135,7 @@ func (b *Batcher) Add( b.mu.Lock() defer b.mu.Unlock() - now := database.Now() + now = database.Time(now) b.buf.ID = append(b.buf.ID, uuid.New()) b.buf.CreatedAt = append(b.buf.CreatedAt, now) @@ -198,15 +199,6 @@ func (b *Batcher) flush(ctx context.Context, forced bool, reason string) { defer func() { b.flushForced.Store(false) b.mu.Unlock() - // Notify that a flush has completed. This only happens in tests. - if b.flushed != nil { - select { - case <-ctx.Done(): - close(b.flushed) - default: - b.flushed <- count - } - } if count > 0 { elapsed := time.Since(start) b.log.Debug(ctx, "flush complete", @@ -216,6 +208,15 @@ func (b *Batcher) flush(ctx context.Context, forced bool, reason string) { slog.F("reason", reason), ) } + // Notify that a flush has completed. This only happens in tests. + if b.flushed != nil { + select { + case <-ctx.Done(): + close(b.flushed) + default: + b.flushed <- count + } + } }() if len(b.buf.ID) == 0 { diff --git a/coderd/batchstats/batcher_internal_test.go b/coderd/batchstats/batcher_internal_test.go index a6e28f1a9f389..8288442400a3e 100644 --- a/coderd/batchstats/batcher_internal_test.go +++ b/coderd/batchstats/batcher_internal_test.go @@ -46,7 +46,7 @@ func TestBatchStats(t *testing.T) { // Given: no data points are added for workspace // When: it becomes time to report stats - t1 := time.Now() + t1 := database.Now() // Signal a tick and wait for a flush to complete. tick <- t1 f := <-flushed @@ -59,9 +59,9 @@ func TestBatchStats(t *testing.T) { require.Empty(t, stats, "should have no stats for workspace") // Given: a single data point is added for workspace - t2 := time.Now() + t2 := t1.Add(time.Second) t.Logf("inserting 1 stat") - require.NoError(t, b.Add(deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) + require.NoError(t, b.Add(t2.Add(time.Millisecond), deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) // When: it becomes time to report stats // Signal a tick and wait for a flush to complete. @@ -77,7 +77,7 @@ func TestBatchStats(t *testing.T) { // Given: a lot of data points are added for both workspaces // (equal to batch size) - t3 := time.Now() + t3 := t2.Add(time.Second) done := make(chan struct{}) go func() { @@ -85,9 +85,9 @@ func TestBatchStats(t *testing.T) { t.Logf("inserting %d stats", defaultBufferSize) for i := 0; i < defaultBufferSize; i++ { if i%2 == 0 { - require.NoError(t, b.Add(deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) + require.NoError(t, b.Add(t3.Add(time.Millisecond), deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t))) } else { - require.NoError(t, b.Add(deps2.Agent.ID, deps2.User.ID, deps2.Template.ID, deps2.Workspace.ID, randAgentSDKStats(t))) + require.NoError(t, b.Add(t3.Add(time.Millisecond), deps2.Agent.ID, deps2.User.ID, deps2.Template.ID, deps2.Workspace.ID, randAgentSDKStats(t))) } } }() @@ -105,7 +105,7 @@ func TestBatchStats(t *testing.T) { require.Len(t, stats, 2, "should have stats for both workspaces") // Ensures that a subsequent flush pushes all the remaining data - t4 := time.Now() + t4 := t3.Add(time.Second) tick <- t4 f2 := <-flushed t.Logf("flush 4 completed") @@ -113,7 +113,7 @@ func TestBatchStats(t *testing.T) { require.Equal(t, expectedCount, f2, "did not flush expected remaining rows") // Ensure that a subsequent flush does not push stale data. - t5 := time.Now() + t5 := t4.Add(time.Second) tick <- t5 f = <-flushed require.Zero(t, f, "expected zero stats to have been flushed") diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 0f5607db73436..545f3e7c6ed84 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1414,7 +1414,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques var errGroup errgroup.Group errGroup.Go(func() error { - if err := api.statsBatcher.Add(workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, req); err != nil { + if err := api.statsBatcher.Add(time.Now(), workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, req); err != nil { api.Logger.Error(ctx, "failed to add stats to batcher", slog.Error(err)) return xerrors.Errorf("can't insert workspace agent stat: %w", err) } From 694729b4f7c4023f01b6accd6ce8f7fc8427d90b Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 7 Aug 2023 22:23:00 -0500 Subject: [PATCH 034/156] chore: disable goleak in windows cli tests (#8955) --- cli/root_internal_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/root_internal_test.go b/cli/root_internal_test.go index e8c463e95cc90..2d99ab8247518 100644 --- a/cli/root_internal_test.go +++ b/cli/root_internal_test.go @@ -1,6 +1,8 @@ package cli import ( + "os" + "runtime" "testing" "github.com/stretchr/testify/require" @@ -67,6 +69,11 @@ func Test_formatExamples(t *testing.T) { } func TestMain(m *testing.M) { + if runtime.GOOS == "windows" { + // Don't run goleak on windows tests, they're super flaky right now. + // See: https://github.com/coder/coder/issues/8954 + os.Exit(m.Run()) + } goleak.VerifyTestMain(m, // The lumberjack library is used by by agent and seems to leave // goroutines after Close(), fails TestGitSSH tests. From 73b136e3f0203a593c1c089ad7592423c5ad4fe0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Aug 2023 21:29:35 -0700 Subject: [PATCH 035/156] fix: add exp backoff to validate fresh git auth tokens (#8956) A customer using GitHub in Australia reported that validating immediately after refreshing the token would intermittently fail with a 401. Waiting a few milliseconds with the exact same token on the exact same request would resolve the issue. It seems likely that the write is not propagating to the read replica in time. --- coderd/gitauth/config.go | 18 +++++++++++++++++- coderd/gitauth/config_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/coderd/gitauth/config.go b/coderd/gitauth/config.go index 29d4804dcd538..1387acee7ebf1 100644 --- a/coderd/gitauth/config.go +++ b/coderd/gitauth/config.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "regexp" + "time" "golang.org/x/oauth2" "golang.org/x/xerrors" @@ -17,6 +18,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" + "github.com/coder/retry" ) type OAuth2Config interface { @@ -75,12 +77,26 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, gitAuthLin // we aren't trying to surface an error, we're just trying to obtain a valid token. return gitAuthLink, false, nil } - + r := retry.New(50*time.Millisecond, 200*time.Millisecond) + // See the comment below why the retry and cancel is required. + retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second) + defer retryCtxCancel() +validate: valid, _, err := c.ValidateToken(ctx, token.AccessToken) if err != nil { return gitAuthLink, false, xerrors.Errorf("validate git auth token: %w", err) } if !valid { + // A customer using GitHub in Australia reported that validating immediately + // after refreshing the token would intermittently fail with a 401. Waiting + // a few milliseconds with the exact same token on the exact same request + // would resolve the issue. It seems likely that the write is not propagating + // to the read replica in time. + // + // We do an exponential backoff here to give the write time to propagate. + if c.Type == codersdk.GitProviderGitHub && r.Wait(retryCtx) { + goto validate + } // The token is no longer valid! return gitAuthLink, false, nil } diff --git a/coderd/gitauth/config_test.go b/coderd/gitauth/config_test.go index 31d6392341426..f58531fdf773f 100644 --- a/coderd/gitauth/config_test.go +++ b/coderd/gitauth/config_test.go @@ -73,6 +73,39 @@ func TestRefreshToken(t *testing.T) { require.NoError(t, err) require.False(t, refreshed) }) + t.Run("ValidateRetryGitHub", func(t *testing.T) { + t.Parallel() + hit := false + // We need to ensure that the exponential backoff kicks in properly. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !hit { + hit = true + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Not permitted")) + return + } + w.WriteHeader(http.StatusOK) + })) + config := &gitauth.Config{ + ID: "test", + OAuth2Config: &testutil.OAuth2Config{ + Token: &oauth2.Token{ + AccessToken: "updated", + }, + }, + ValidateURL: srv.URL, + Type: codersdk.GitProviderGitHub, + } + db := dbfake.New() + link := dbgen.GitAuthLink(t, db, database.GitAuthLink{ + ProviderID: config.ID, + OAuthAccessToken: "initial", + }) + _, refreshed, err := config.RefreshToken(context.Background(), db, link) + require.NoError(t, err) + require.True(t, refreshed) + require.True(t, hit) + }) t.Run("ValidateNoUpdate", func(t *testing.T) { t.Parallel() validated := make(chan struct{}) From bac3a588b3ecc82bca5e51dd7baf16d0cf43c5fe Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Aug 2023 22:36:46 -0700 Subject: [PATCH 036/156] chore: add e2e test for backwards client ssh compatibility (#8958) * chore: add e2e test for backwards client ssh compatibility This was discussed as part of our regression review for outdated agents, so here is the reverse with an extremely old client. * fmt --- site/e2e/helpers.ts | 18 +++++------ site/e2e/tests/outdatedCLI.spec.ts | 52 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 site/e2e/tests/outdatedCLI.spec.ts diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index dfbd5f99896a2..b86a7c756f3a8 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -65,20 +65,18 @@ export const createTemplate = async ( export const sshIntoWorkspace = async ( page: Page, workspace: string, + binaryPath = "go", + binaryArgs = ["run", coderMainPath()], ): Promise => { const sessionToken = await findSessionToken(page) return new Promise((resolve, reject) => { - const cp = spawn( - "go", - ["run", coderMainPath(), "ssh", "--stdio", workspace], - { - env: { - ...process.env, - CODER_SESSION_TOKEN: sessionToken, - CODER_URL: "http://localhost:3000", - }, + const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], { + env: { + ...process.env, + CODER_SESSION_TOKEN: sessionToken, + CODER_URL: "http://localhost:3000", }, - ) + }) cp.on("error", (err) => reject(err)) const proxyStream = new Duplex({ read: (size) => { diff --git a/site/e2e/tests/outdatedCLI.spec.ts b/site/e2e/tests/outdatedCLI.spec.ts new file mode 100644 index 0000000000000..ab143bad27c34 --- /dev/null +++ b/site/e2e/tests/outdatedCLI.spec.ts @@ -0,0 +1,52 @@ +import { test } from "@playwright/test" +import { randomUUID } from "crypto" +import { + createTemplate, + createWorkspace, + downloadCoderVersion, + sshIntoWorkspace, + startAgent, +} from "../helpers" + +const clientVersion = "v0.14.0" + +test("ssh with client " + clientVersion, async ({ page }) => { + const token = randomUUID() + const template = await createTemplate(page, { + apply: [ + { + complete: { + resources: [ + { + agents: [ + { + token, + }, + ], + }, + ], + }, + }, + ], + }) + const workspace = await createWorkspace(page, template) + await startAgent(page, token) + const binaryPath = await downloadCoderVersion(clientVersion) + + const client = await sshIntoWorkspace(page, workspace, binaryPath) + await new Promise((resolve, reject) => { + // We just exec a command to be certain the agent is running! + client.exec("exit 0", (err, stream) => { + if (err) { + return reject(err) + } + stream.on("exit", (code) => { + if (code !== 0) { + return reject(new Error(`Command exited with code ${code}`)) + } + client.end() + resolve() + }) + }) + }) +}) From 31b7de6a3ea9112d2f1fad5e7f364908c39f19e3 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Tue, 8 Aug 2023 09:20:36 +0300 Subject: [PATCH 037/156] chore: upgrade go to 1.20.7 (#8923) * chore: upgrade go to 1.20.7 * remove unused env --- .github/actions/setup-go/action.yaml | 2 +- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yaml | 4 ---- .github/workflows/security.yaml | 3 --- dogfood/Dockerfile | 2 +- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml index 4d696ef298b12..968f1f1ab8d27 100644 --- a/.github/actions/setup-go/action.yaml +++ b/.github/actions/setup-go/action.yaml @@ -4,7 +4,7 @@ description: | inputs: version: description: "The Go version to use." - default: "1.20.6" + default: "1.20.7" runs: using: "composite" steps: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bc1af416e3901..67ecdec43d0f5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -224,7 +224,7 @@ jobs: with: # This doesn't need caching. It's super fast anyways! cache: false - go-version: 1.20.6 + go-version: 1.20.7 - name: Install shfmt run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d78fbe7c5a5b9..ab27aa12d117c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,10 +28,6 @@ env: # https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/ CODER_RELEASE: ${{ !inputs.dry_run }} CODER_DRY_RUN: ${{ inputs.dry_run }} - # For some reason, setup-go won't actually pick up a new patch version if - # it has an old one cached. We need to manually specify the versions so we - # can get the latest release. Never use "~1.xx" here! - CODER_GO_VERSION: "1.20.6" jobs: release: diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 48c8f16fd9bea..04c3b1562147b 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -21,9 +21,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-security cancel-in-progress: ${{ github.event_name == 'pull_request' }} -env: - CODER_GO_VERSION: "1.20.6" - jobs: codeql: runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile index c5f1481679147..c5145ecb98629 100644 --- a/dogfood/Dockerfile +++ b/dogfood/Dockerfile @@ -8,7 +8,7 @@ FROM ubuntu:jammy AS go RUN apt-get update && apt-get install --yes curl gcc # Install Go manually, so that we can control the version -ARG GO_VERSION=1.20.6 +ARG GO_VERSION=1.20.7 RUN mkdir --parents /usr/local/go # Boring Go is needed to build FIPS-compliant binaries. From b2a84462abb6caf2dc1982081c8c1f1bd10c5b61 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Aug 2023 05:32:41 -0700 Subject: [PATCH 038/156] chore: fix ruleguard xerrors rules (#8967) --- cli/stat.go | 2 +- scripts/rules.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/stat.go b/cli/stat.go index 3657a4f3c71c9..6311596ecea2a 100644 --- a/cli/stat.go +++ b/cli/stat.go @@ -240,7 +240,7 @@ func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd { ds, err := s.Disk(pfx, pathArg) if err != nil { if os.IsNotExist(err) { - // fmt.Errorf produces a more concise error. + //nolint:gocritic // fmt.Errorf produces a more concise error. return fmt.Errorf("not found: %q", pathArg) } return err diff --git a/scripts/rules.go b/scripts/rules.go index 20d0c43f7b883..1c26b80704b62 100644 --- a/scripts/rules.go +++ b/scripts/rules.go @@ -51,8 +51,12 @@ func xerrors(m dsl.Matcher) { m.Import("fmt") m.Import("golang.org/x/xerrors") - m.Match("fmt.Errorf($*args)"). - Suggest("xerrors.New($args)"). + m.Match("fmt.Errorf($arg)"). + Suggest("xerrors.New($arg)"). + Report("Use xerrors to provide additional stacktrace information!") + + m.Match("fmt.Errorf($arg1, $*args)"). + Suggest("xerrors.Errorf($arg1, $args)"). Report("Use xerrors to provide additional stacktrace information!") m.Match("errors.$_($msg)"). From c20c4faa7c2177b3764e786c324bdb9b2fab4c5d Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Tue, 8 Aug 2023 17:12:51 +0300 Subject: [PATCH 039/156] docs: format CONTRIBUTING.md (#8973) --- docs/CONTRIBUTING.md | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e794828520e81..611faf48400c9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,11 +4,11 @@ We recommend using the [Nix](https://nix.dev/) package manager as it makes any pain related to maintaining dependency versions [just disappear](https://twitter.com/mitchellh/status/1491102567296040961). Once nix [has been installed](https://nixos.org/download.html) the development environment can be _manually instantiated_ through the `nix-shell` command: -``` -$ cd ~/code/coder +```shell +cd ~/code/coder # https://nix.dev/tutorials/declarative-and-reproducible-developer-environments -$ nix-shell +nix-shell ... copying path '/nix/store/3ms6cs5210n8vfb5a7jkdvzrzdagqzbp-iana-etc-20210225' from 'https://cache.nixos.org'... @@ -19,18 +19,17 @@ copying path '/nix/store/v2gvj8whv241nj4lzha3flq8pnllcmvv-ignore-5.2.0.tgz' from If [direnv](https://direnv.net/) is installed and the [hooks are configured](https://direnv.net/docs/hook.html) then the development environment can be _automatically instantiated_ by creating the following `.envrc`, thus removing the need to run `nix-shell` by hand! -``` -$ cd ~/code/coder -$ echo "use nix" >.envrc -$ direnv allow +```shell +cd ~/code/coder +echo "use nix" >.envrc +direnv allow ``` -Now, whenever you enter the project folder, `direnv` will prepare the environment for you: +Now, whenever you enter the project folder, [`direnv`](https://direnv.net/docs/hook.html) will prepare the environment for you: -``` -$ cd ~/code/coder +```shell +cd ~/code/coder -# https://direnv.net/docs/hook.html direnv: loading ~/code/coder/.envrc direnv: using nix direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_STORE +NM +NODE_PATH +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +XDG_DATA_DIRS +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH @@ -53,15 +52,15 @@ Alternatively if you do not want to use nix then you'll need to install the need - [`pg_dump`](https://stackoverflow.com/a/49689589) - on macOS, run `brew install libpq zstd` - on Linux, install [`zstd`](https://github.com/horta/zstd.install) -- [`pkg-config`]() +- `pkg-config` - on macOS, run `brew install pkg-config` -- [`pixman`]() +- `pixman` - on macOS, run `brew install pixman` -- [`cairo`]() +- `cairo` - on macOS, run `brew install cairo` -- [`pango`]() +- `pango` - on macOS, run `brew install pango` -- [`pandoc`]() +- `pandoc` - on macOS, run `brew install pandocomatic` ### Development workflow @@ -108,13 +107,14 @@ Database migrations are managed with [`migrate`](https://github.com/golang-migra To add new migrations, use the following command: -``` -$ ./coderd/database/migrations/create_fixture.sh my name +```shell +./coderd/database/migrations/create_fixture.sh my name /home/coder/src/coder/coderd/database/migrations/000070_my_name.up.sql /home/coder/src/coder/coderd/database/migrations/000070_my_name.down.sql -Run "make gen" to generate models. ``` +Run "make gen" to generate models. + Then write queries into the generated `.up.sql` and `.down.sql` files and commit them into the repository. The down script should make a best-effort to retain as much data as possible. @@ -140,8 +140,8 @@ migration of multiple features or complex configurations. To add a new partial fixture, run the following command: -``` -$ ./coderd/database/migrations/create_fixture.sh my fixture +```shell +./coderd/database/migrations/create_fixture.sh my fixture /home/coder/src/coder/coderd/database/migrations/testdata/fixtures/000070_my_fixture.up.sql ``` @@ -153,9 +153,9 @@ To create a full dump, run a fully fledged Coder deployment and use it to generate data in the database. Then shut down the deployment and take a snapshot of the database. -``` -$ mkdir -p coderd/database/migrations/testdata/full_dumps/v0.12.2 && cd $_ -$ pg_dump "postgres://coder@localhost:..." -a --inserts >000069_dump_v0.12.2.up.sql +```shell +mkdir -p coderd/database/migrations/testdata/full_dumps/v0.12.2 && cd $_ +pg_dump "postgres://coder@localhost:..." -a --inserts >000069_dump_v0.12.2.up.sql ``` Make sure sensitive data in the dump is desensitized, for instance names, @@ -164,8 +164,8 @@ emails, OAuth tokens and other secrets. Then commit the dump to the project. To find out what the latest migration for a version of Coder is, use the following command: -``` -$ git ls-files v0.12.2 -- coderd/database/migrations/*.up.sql +```shell +git ls-files v0.12.2 -- coderd/database/migrations/*.up.sql ``` This helps in naming the dump (e.g. `000069` above). From 4d3230c9ad85b9fdff3d4e81ac9239df4a6023fd Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 8 Aug 2023 07:35:34 -0700 Subject: [PATCH 040/156] fix: default to executing e2e ssh without args (#8975) This was causing the test to fail consistently! --- site/e2e/helpers.ts | 56 +++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index b86a7c756f3a8..bdfc8b015352d 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -66,8 +66,11 @@ export const sshIntoWorkspace = async ( page: Page, workspace: string, binaryPath = "go", - binaryArgs = ["run", coderMainPath()], + binaryArgs: string[] = [], ): Promise => { + if (binaryPath === "go") { + binaryArgs = ["run", coderMainPath()] + } const sessionToken = await findSessionToken(page) return new Promise((resolve, reject) => { const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], { @@ -120,7 +123,7 @@ export const downloadCoderVersion = async ( } const binaryName = "coder-e2e-" + version - const tempDir = "/tmp" + const tempDir = "/tmp/coder-e2e-cache" // The install script adds `./bin` automatically to the path :shrug: const binaryPath = path.join(tempDir, "bin", binaryName) @@ -138,26 +141,35 @@ export const downloadCoderVersion = async ( // Runs our public install script using our options to // install the binary! await new Promise((resolve, reject) => { - const cp = spawn("sh", [ - "-c", + const cp = spawn( + "sh", [ - "curl", - "-L", - "https://coder.com/install.sh", - "|", - "sh", - "-s", - "--", - "--version", - version, - "--method", - "standalone", - "--prefix", - tempDir, - "--binary-name", - binaryName, - ].join(" "), - ]) + "-c", + [ + "curl", + "-L", + "https://coder.com/install.sh", + "|", + "sh", + "-s", + "--", + "--version", + version, + "--method", + "standalone", + "--prefix", + tempDir, + "--binary-name", + binaryName, + ].join(" "), + ], + { + env: { + ...process.env, + XDG_CACHE_HOME: "/tmp/coder-e2e-cache", + }, + }, + ) // eslint-disable-next-line no-console -- Needed for debugging cp.stderr.on("data", (data) => console.log(data.toString())) cp.on("close", (code) => { @@ -189,7 +201,7 @@ export const startAgentWithCommand = async ( buffer = Buffer.concat([buffer, data]) }) try { - await page.getByTestId("agent-status-ready").isVisible() + await page.getByTestId("agent-status-ready").waitFor({ state: "visible" }) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- The error is a string } catch (ex: any) { throw new Error(ex.toString() + "\n" + buffer.toString()) From 05054c6a0a73df1326b6be6f55a6c83dd55097cc Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Tue, 8 Aug 2023 17:57:57 +0300 Subject: [PATCH 041/156] ci: make `test-e2e` a required check (#8977) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 67ecdec43d0f5..8220492528b9a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -668,6 +668,7 @@ jobs: - test-go-pg - test-go-race - test-js + - test-e2e - offlinedocs # Allow this job to run even if the needed jobs fail, are skipped or # cancelled. From 1d4a72f43f805b1903b99c9cfaed8534531b8b91 Mon Sep 17 00:00:00 2001 From: Cem Date: Tue, 8 Aug 2023 18:02:52 +0300 Subject: [PATCH 042/156] perf(coderd/util/slice): refactor unique method for large lists (#8925) --- coderd/util/slice/slice.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 9909fe2b72c21..0bc5920ce3760 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -38,17 +38,19 @@ func Overlap[T comparable](a []T, b []T) bool { } // Unique returns a new slice with all duplicate elements removed. -// This is a slow function on large lists. -// TODO: Sort elements and implement a faster search algorithm if we -// really start to use this. func Unique[T comparable](a []T) []T { cpy := make([]T, 0, len(a)) + seen := make(map[T]struct{}, len(a)) + for _, v := range a { - v := v - if !Contains(cpy, v) { - cpy = append(cpy, v) + if _, ok := seen[v]; ok { + continue } + + seen[v] = struct{}{} + cpy = append(cpy, v) } + return cpy } From 5339a31532fa2f2a09becc51fe2e87c019c3b4d1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Aug 2023 10:05:12 -0500 Subject: [PATCH 043/156] fix: remove refresh oauth logic on OIDC login (#8950) * fix: do not do oauth refresh logic on oidc login --- coderd/coderd.go | 2 - coderd/coderdtest/coderdtest.go | 79 ++++++++++++++++++----- coderd/httpmw/apikey.go | 100 ++++++++++++++++------------- coderd/userauth.go | 7 +- coderd/userauth_test.go | 95 ++++++++++++++++++++++++++- enterprise/coderd/userauth_test.go | 2 + 6 files changed, 217 insertions(+), 68 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 58b6c902c7dbc..3407be5ac8de2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -693,7 +693,6 @@ func New(options *Options) *API { r.Route("/github", func(r chi.Router) { r.Use( httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil), - apiKeyMiddlewareOptional, ) r.Get("/callback", api.userOAuth2Github) }) @@ -701,7 +700,6 @@ func New(options *Options) *API { r.Route("/oidc/callback", func(r chi.Router) { r.Use( httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams), - apiKeyMiddlewareOptional, ) r.Get("/", api.userOIDC) }) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 71e3336ab2e87..5e2e55d5c032f 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1022,9 +1022,31 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif type OIDCConfig struct { key *rsa.PrivateKey issuer string + // These are optional + refreshToken string + oidcTokenExpires func() time.Time + tokenSource func() (*oauth2.Token, error) } -func NewOIDCConfig(t *testing.T, issuer string) *OIDCConfig { +func WithRefreshToken(token string) func(cfg *OIDCConfig) { + return func(cfg *OIDCConfig) { + cfg.refreshToken = token + } +} + +func WithTokenExpires(expFunc func() time.Time) func(cfg *OIDCConfig) { + return func(cfg *OIDCConfig) { + cfg.oidcTokenExpires = expFunc + } +} + +func WithTokenSource(src func() (*oauth2.Token, error)) func(cfg *OIDCConfig) { + return func(cfg *OIDCConfig) { + cfg.tokenSource = src + } +} + +func NewOIDCConfig(t *testing.T, issuer string, opts ...func(cfg *OIDCConfig)) *OIDCConfig { t.Helper() block, _ := pem.Decode([]byte(testRSAPrivateKey)) @@ -1035,33 +1057,58 @@ func NewOIDCConfig(t *testing.T, issuer string) *OIDCConfig { issuer = "https://coder.com" } - return &OIDCConfig{ + cfg := &OIDCConfig{ key: pkey, issuer: issuer, } + for _, opt := range opts { + opt(cfg) + } + return cfg } func (*OIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { return "/?state=" + url.QueryEscape(state) } -func (*OIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { - return nil +type tokenSource struct { + src func() (*oauth2.Token, error) +} + +func (s tokenSource) Token() (*oauth2.Token, error) { + return s.src() +} + +func (cfg *OIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { + if cfg.tokenSource == nil { + return nil + } + return tokenSource{ + src: cfg.tokenSource, + } } -func (*OIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { +func (cfg *OIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { token, err := base64.StdEncoding.DecodeString(code) if err != nil { return nil, xerrors.Errorf("decode code: %w", err) } + + var exp time.Time + if cfg.oidcTokenExpires != nil { + exp = cfg.oidcTokenExpires() + } + return (&oauth2.Token{ - AccessToken: "token", + AccessToken: "token", + RefreshToken: cfg.refreshToken, + Expiry: exp, }).WithExtra(map[string]interface{}{ "id_token": string(token), }), nil } -func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { +func (cfg *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { t.Helper() if _, ok := claims["exp"]; !ok { @@ -1069,20 +1116,20 @@ func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { } if _, ok := claims["iss"]; !ok { - claims["iss"] = o.issuer + claims["iss"] = cfg.issuer } if _, ok := claims["sub"]; !ok { claims["sub"] = "testme" } - signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(o.key) + signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(cfg.key) require.NoError(t, err) return base64.StdEncoding.EncodeToString([]byte(signed)) } -func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { +func (cfg *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { // By default, the provider can be empty. // This means it won't support any endpoints! provider := &oidc.Provider{} @@ -1099,10 +1146,10 @@ func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts } provider = cfg.NewProvider(context.Background()) } - cfg := &coderd.OIDCConfig{ - OAuth2Config: o, - Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{ - PublicKeys: []crypto.PublicKey{o.key.Public()}, + newCFG := &coderd.OIDCConfig{ + OAuth2Config: cfg, + Verifier: oidc.NewVerifier(cfg.issuer, &oidc.StaticKeySet{ + PublicKeys: []crypto.PublicKey{cfg.key.Public()}, }, &oidc.Config{ SkipClientIDCheck: true, }), @@ -1113,9 +1160,9 @@ func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts GroupField: "groups", } for _, opt := range opts { - opt(cfg) + opt(newCFG) } - return cfg + return newCFG } // NewAzureInstanceIdentity returns a metadata client and ID token validator for faking diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 5f0ec0dc263c7..f8f809761787c 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -142,6 +142,56 @@ func ExtractAPIKeyMW(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { } } +func APIKeyFromRequest(ctx context.Context, db database.Store, sessionTokenFunc func(r *http.Request) string, r *http.Request) (*database.APIKey, codersdk.Response, bool) { + tokenFunc := APITokenFromRequest + if sessionTokenFunc != nil { + tokenFunc = sessionTokenFunc + } + + token := tokenFunc(r) + if token == "" { + return nil, codersdk.Response{ + Message: SignedOutErrorMessage, + Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie), + }, false + } + + keyID, keySecret, err := SplitAPIToken(token) + if err != nil { + return nil, codersdk.Response{ + Message: SignedOutErrorMessage, + Detail: "Invalid API key format: " + err.Error(), + }, false + } + + //nolint:gocritic // System needs to fetch API key to check if it's valid. + key, err := db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, codersdk.Response{ + Message: SignedOutErrorMessage, + Detail: "API key is invalid.", + }, false + } + + return nil, codersdk.Response{ + Message: internalErrorMessage, + Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()), + }, false + } + + // Checking to see if the secret is valid. + hashedSecret := sha256.Sum256([]byte(keySecret)) + if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 { + return nil, codersdk.Response{ + Message: SignedOutErrorMessage, + Detail: "API key secret is invalid.", + }, false + } + + return &key, codersdk.Response{}, true +} + // ExtractAPIKey requires authentication using a valid API key. It handles // extending an API key if it comes close to expiry, updating the last used time // in the database. @@ -179,49 +229,9 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon return nil, nil, false } - tokenFunc := APITokenFromRequest - if cfg.SessionTokenFunc != nil { - tokenFunc = cfg.SessionTokenFunc - } - token := tokenFunc(r) - if token == "" { - return optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: SignedOutErrorMessage, - Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie), - }) - } - - keyID, keySecret, err := SplitAPIToken(token) - if err != nil { - return optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: SignedOutErrorMessage, - Detail: "Invalid API key format: " + err.Error(), - }) - } - - //nolint:gocritic // System needs to fetch API key to check if it's valid. - key, err := cfg.DB.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: SignedOutErrorMessage, - Detail: "API key is invalid.", - }) - } - - return write(http.StatusInternalServerError, codersdk.Response{ - Message: internalErrorMessage, - Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()), - }) - } - - // Checking to see if the secret is valid. - hashedSecret := sha256.Sum256([]byte(keySecret)) - if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 { - return optionalWrite(http.StatusUnauthorized, codersdk.Response{ - Message: SignedOutErrorMessage, - Detail: "API key secret is invalid.", - }) + key, resp, ok := APIKeyFromRequest(ctx, cfg.DB, cfg.SessionTokenFunc, r) + if !ok { + return optionalWrite(http.StatusUnauthorized, resp) } var ( @@ -232,7 +242,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon ) if key.LoginType == database.LoginTypeGithub || key.LoginType == database.LoginTypeOIDC { //nolint:gocritic // System needs to fetch UserLink to check if it's valid. - link, err = cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{ + link, err := cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{ UserID: key.UserID, LoginType: key.LoginType, }) @@ -427,7 +437,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon }.WithCachedASTValue(), } - return &key, &authz, true + return key, &authz, true } // APITokenFromRequest returns the api token from the request. diff --git a/coderd/userauth.go b/coderd/userauth.go index 9b6ba7992bad5..f1e110c08bfdc 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -1427,7 +1427,8 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C } var key database.APIKey - if oldKey, ok := httpmw.APIKeyOptional(r); ok && isConvertLoginType { + oldKey, _, ok := httpmw.APIKeyFromRequest(ctx, api.Database, nil, r) + if ok && oldKey != nil && isConvertLoginType { // If this is a convert login type, and it succeeds, then delete the old // session. Force the user to log back in. err := api.Database.DeleteAPIKeyByID(r.Context(), oldKey.ID) @@ -1447,7 +1448,9 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C Secure: api.SecureAuthCookie, HttpOnly: true, }) - key = oldKey + // This is intentional setting the key to the deleted old key, + // as the user needs to be forced to log back in. + key = *oldKey } else { //nolint:gocritic cookie, newKey, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), apikey.CreateParams{ diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 6f49222ff8764..efa7673890863 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -9,6 +9,7 @@ import ( "net/http/cookiejar" "strings" "testing" + "time" "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt" @@ -24,12 +25,97 @@ import ( "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/database/dbgen" "github.com/coder/coder/coderd/database/dbtestutil" "github.com/coder/coder/codersdk" "github.com/coder/coder/testutil" ) +// This test specifically tests logging in with OIDC when an expired +// OIDC session token exists. +// The token refreshing should not happen since we are reauthenticating. +func TestOIDCOauthLoginWithExisting(t *testing.T) { + t.Parallel() + + conf := coderdtest.NewOIDCConfig(t, "", + // Provide a refresh token so we use the refresh token flow + coderdtest.WithRefreshToken("refresh_token"), + // We need to set the expire in the future for the first api calls. + coderdtest.WithTokenExpires(func() time.Time { + return time.Now().Add(time.Hour).UTC() + }), + // No refresh should actually happen in this test. + coderdtest.WithTokenSource(func() (*oauth2.Token, error) { + return nil, xerrors.New("token should not require refresh") + }), + ) + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + auditor := audit.NewMock() + const username = "alice" + claims := jwt.MapClaims{ + "email": "alice@coder.com", + "email_verified": true, + "preferred_username": username, + } + config := conf.OIDCConfig(t, claims) + + config.AllowSignups = true + config.IgnoreUserInfo = true + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Auditor: auditor, + OIDCConfig: config, + Logger: &logger, + }) + + // Signup alice + resp := oidcCallback(t, client, conf.EncodeClaims(t, claims)) + // Set the client to use this OIDC context + authCookie := authCookieValue(resp.Cookies()) + client.SetSessionToken(authCookie) + _ = resp.Body.Close() + + ctx := testutil.Context(t, testutil.WaitLong) + // Verify the user and oauth link + user, err := client.User(ctx, "me") + require.NoError(t, err) + require.Equal(t, username, user.Username) + + // nolint:gocritic + link, err := api.Database.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{ + UserID: user.ID, + LoginType: database.LoginType(user.LoginType), + }) + require.NoError(t, err, "failed to get user link") + + // Expire the link + // nolint:gocritic + _, err = api.Database.UpdateUserLink(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLinkParams{ + OAuthAccessToken: link.OAuthAccessToken, + OAuthRefreshToken: link.OAuthRefreshToken, + OAuthExpiry: time.Now().Add(time.Hour * -1).UTC(), + UserID: link.UserID, + LoginType: link.LoginType, + }) + require.NoError(t, err, "failed to update user link") + + // Log in again with OIDC + loginAgain := oidcCallbackWithState(t, client, conf.EncodeClaims(t, claims), "seconds_login", func(req *http.Request) { + req.AddCookie(&http.Cookie{ + Name: codersdk.SessionTokenCookie, + Value: authCookie, + Path: "/", + }) + }) + require.Equal(t, http.StatusTemporaryRedirect, loginAgain.StatusCode) + _ = loginAgain.Body.Close() + + // Try to use new login + client.SetSessionToken(authCookieValue(resp.Cookies())) + _, err = client.User(ctx, "me") + require.NoError(t, err, "use new session") +} + func TestUserLogin(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -819,7 +905,7 @@ func TestUserOIDC(t *testing.T) { }) require.NoError(t, err) - resp := oidcCallbackWithState(t, user, code, convertResponse.StateString) + resp := oidcCallbackWithState(t, user, code, convertResponse.StateString, nil) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) }) @@ -1045,10 +1131,10 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { } func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response { - return oidcCallbackWithState(t, client, code, "somestate") + return oidcCallbackWithState(t, client, code, "somestate", nil) } -func oidcCallbackWithState(t *testing.T, client *codersdk.Client, code, state string) *http.Response { +func oidcCallbackWithState(t *testing.T, client *codersdk.Client, code, state string, modify func(r *http.Request)) *http.Response { t.Helper() client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -1062,6 +1148,9 @@ func oidcCallbackWithState(t *testing.T, client *codersdk.Client, code, state st Name: codersdk.OAuth2StateCookie, Value: state, }) + if modify != nil { + modify(req) + } res, err := client.HTTPClient.Do(req) require.NoError(t, err) defer res.Body.Close() diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index 428cf91a6fef2..2cb110abe987b 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -99,6 +99,7 @@ func TestUserOIDC(t *testing.T) { "roles": []string{"random", oidcRoleName, rbac.RoleOwner()}, })) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + _ = resp.Body.Close() user, err := client.User(ctx, "alice") require.NoError(t, err) @@ -112,6 +113,7 @@ func TestUserOIDC(t *testing.T) { "roles": []string{"random"}, })) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + _ = resp.Body.Close() user, err = client.User(ctx, "alice") require.NoError(t, err) From 4a987e991796449c2b321db76dc9bdeb91f12281 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 8 Aug 2023 13:09:31 -0300 Subject: [PATCH 044/156] feat(site): add parameters usage to insights (#8886) --- coderd/apidoc/docs.go | 6 +- coderd/apidoc/swagger.json | 6 +- codersdk/deployment.go | 4 + docs/api/schemas.md | 1 + site/src/api/typesGenerated.ts | 2 + .../TemplateInsightsPage.stories.tsx | 371 +++++++++++++++++- .../TemplateInsightsPage.tsx | 233 +++++++++++ 7 files changed, 618 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 61a6989ba27fd..9faf9de1611cc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8024,7 +8024,8 @@ const docTemplate = `{ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "deployment_health_page" + "deployment_health_page", + "template_parameters_insights" ], "x-enum-varnames": [ "ExperimentMoons", @@ -8032,7 +8033,8 @@ const docTemplate = `{ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentDeploymentHealthPage" + "ExperimentDeploymentHealthPage", + "ExperimentTemplateParametersInsights" ] }, "codersdk.Feature": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 982ed816264c1..635457901db36 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7185,7 +7185,8 @@ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "deployment_health_page" + "deployment_health_page", + "template_parameters_insights" ], "x-enum-varnames": [ "ExperimentMoons", @@ -7193,7 +7194,8 @@ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentDeploymentHealthPage" + "ExperimentDeploymentHealthPage", + "ExperimentTemplateParametersInsights" ] }, "codersdk.Feature": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e714d9c1c34b5..fdc5b92af28de 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1881,6 +1881,9 @@ const ( // Deployment health page ExperimentDeploymentHealthPage Experiment = "deployment_health_page" + // Template parameters insights + ExperimentTemplateParametersInsights Experiment = "template_parameters_insights" + // Add new experiments here! // ExperimentExample Experiment = "example" ) @@ -1891,6 +1894,7 @@ const ( // not be included here and will be essentially hidden. var ExperimentsAll = Experiments{ ExperimentDeploymentHealthPage, + ExperimentTemplateParametersInsights, } // Experiments is a list of experiments that are enabled for the deployment. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 2f09bfb6728ae..4ab6eceeef96d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2679,6 +2679,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `single_tailnet` | | `template_restart_requirement` | | `deployment_health_page` | +| `template_parameters_insights` | ## codersdk.Feature diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7b9b79d69b3db..c01f4d94385ba 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1586,6 +1586,7 @@ export type Experiment = | "moons" | "single_tailnet" | "tailnet_pg_coordinator" + | "template_parameters_insights" | "template_restart_requirement" | "workspace_actions" export const Experiments: Experiment[] = [ @@ -1593,6 +1594,7 @@ export const Experiments: Experiment[] = [ "moons", "single_tailnet", "tailnet_pg_coordinator", + "template_parameters_insights", "template_restart_requirement", "workspace_actions", ] diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 49678eef08375..4fb65c0b2b5f1 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -83,7 +83,376 @@ export const Loaded: Story = { seconds: 1020900, }, ], - parameters_usage: [], + parameters_usage: [ + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Compute instances", + type: "number", + description: "Let's set the expected number of instances.", + values: [ + { + value: "3", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Docker Image", + type: "string", + description: "Docker image for the development container", + values: [ + { + value: "ghcr.io/harrison-ai/coder-dev:base", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "Optional random string", + type: "string", + description: "This string is optional", + values: [ + { + value: "ksjdlkajs;djálskd'l ;a k;aosdk ;oaids ;li", + count: 1, + }, + { + value: "some other any string here", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Region", + type: "string", + description: "These are options.", + options: [ + { + name: "US Central", + description: "Select for central!", + value: "us-central1-a", + icon: "/icon/goland.svg", + }, + { + name: "US East", + description: "Select for east!", + value: "us-east1-a", + icon: "/icon/folder.svg", + }, + { + name: "US West", + description: "Select for west!", + value: "us-west2-a", + icon: "", + }, + ], + values: [ + { + value: "us-central1-a", + count: 1, + }, + { + value: "us-west2-a", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Security groups", + type: "list(string)", + description: "Select appropriate security groups.", + values: [ + { + value: + '["Web Server Security Group","Database Security Group","Backend Security Group"]', + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "buggy-1", + type: "string", + description: "This string is buggy", + values: [ + { + value: "", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force rebuild", + name: "force-rebuild", + type: "bool", + description: "Rebuild the project code", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Location", + name: "location", + type: "string", + description: "What location should your workspace live in?", + options: [ + { + name: "US (Virginia)", + description: "", + value: "eastus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Virginia) 2", + description: "", + value: "eastus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Texas)", + description: "", + value: "southcentralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Washington)", + description: "", + value: "westus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Arizona)", + description: "", + value: "westus3", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Iowa)", + description: "", + value: "centralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Canada (Toronto)", + description: "", + value: "canadacentral", + icon: "/emojis/1f1e8-1f1e6.png", + }, + { + name: "Brazil (Sao Paulo)", + description: "", + value: "brazilsouth", + icon: "/emojis/1f1e7-1f1f7.png", + }, + { + name: "East Asia (Hong Kong)", + description: "", + value: "eastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Southeast Asia (Singapore)", + description: "", + value: "southeastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Australia (New South Wales)", + description: "", + value: "australiaeast", + icon: "/emojis/1f1e6-1f1fa.png", + }, + { + name: "China (Hebei)", + description: "", + value: "chinanorth3", + icon: "/emojis/1f1e8-1f1f3.png", + }, + { + name: "India (Pune)", + description: "", + value: "centralindia", + icon: "/emojis/1f1ee-1f1f3.png", + }, + { + name: "Japan (Tokyo)", + description: "", + value: "japaneast", + icon: "/emojis/1f1ef-1f1f5.png", + }, + { + name: "Korea (Seoul)", + description: "", + value: "koreacentral", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Europe (Ireland)", + description: "", + value: "northeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "Europe (Netherlands)", + description: "", + value: "westeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "France (Paris)", + description: "", + value: "francecentral", + icon: "/emojis/1f1eb-1f1f7.png", + }, + { + name: "Germany (Frankfurt)", + description: "", + value: "germanywestcentral", + icon: "/emojis/1f1e9-1f1ea.png", + }, + { + name: "Norway (Oslo)", + description: "", + value: "norwayeast", + icon: "/emojis/1f1f3-1f1f4.png", + }, + { + name: "Sweden (Gävle)", + description: "", + value: "swedencentral", + icon: "/emojis/1f1f8-1f1ea.png", + }, + { + name: "Switzerland (Zurich)", + description: "", + value: "switzerlandnorth", + icon: "/emojis/1f1e8-1f1ed.png", + }, + { + name: "Qatar (Doha)", + description: "", + value: "qatarcentral", + icon: "/emojis/1f1f6-1f1e6.png", + }, + { + name: "UAE (Dubai)", + description: "", + value: "uaenorth", + icon: "/emojis/1f1e6-1f1ea.png", + }, + { + name: "South Africa (Johannesburg)", + description: "", + value: "southafricanorth", + icon: "/emojis/1f1ff-1f1e6.png", + }, + { + name: "UK (London)", + description: "", + value: "uksouth", + icon: "/emojis/1f1ec-1f1e7.png", + }, + ], + values: [ + { + value: "brazilsouth", + count: 1, + }, + { + value: "switzerlandnorth", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "mtojek_region", + type: "string", + description: "What region should your workspace live in?", + options: [ + { + name: "Los Angeles, CA", + description: "", + value: "Los Angeles, CA", + icon: "", + }, + { + name: "Moncks Corner, SC", + description: "", + value: "Moncks Corner, SC", + icon: "", + }, + { + name: "Eemshaven, NL", + description: "", + value: "Eemshaven, NL", + icon: "", + }, + ], + values: [ + { + value: "Los Angeles, CA", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "My Project ID", + name: "project_id", + type: "string", + description: "This is the Project ID.", + values: [ + { + value: "12345", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force devcontainer rebuild", + name: "rebuild_devcontainer", + type: "bool", + description: "", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Git Repo URL", + name: "repo_url", + type: "string", + description: + "See sample projects (https://github.com/microsoft/vscode-dev-containers#sample-projects)", + values: [ + { + value: "https://github.com/mtojek/coder", + count: 2, + }, + ], + }, + ], }, interval_reports: [ { diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index d6161124d3540..77bf66a0759e9 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -21,10 +21,17 @@ import { Loader } from "components/Loader/Loader" import { DAUsResponse, TemplateInsightsResponse, + TemplateParameterUsage, + TemplateParameterValue, UserLatencyInsightsResponse, } from "api/typesGenerated" import { ComponentProps } from "react" import { subDays, addHours, startOfHour } from "date-fns" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined" +import Link from "@mui/material/Link" +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" +import CancelOutlined from "@mui/icons-material/CancelOutlined" export default function TemplateInsightsPage() { const now = new Date() @@ -42,6 +49,10 @@ export default function TemplateInsightsPage() { queryKey: ["templates", template.id, "user-latency"], queryFn: () => getInsightsUserLatency(insightsFilter), }) + const dashboard = useDashboard() + const shouldDisplayParameters = + dashboard.experiments.includes("template_parameters_insights") || + process.env.NODE_ENV === "development" return ( <> @@ -51,6 +62,7 @@ export default function TemplateInsightsPage() { ) @@ -59,9 +71,11 @@ export default function TemplateInsightsPage() { export const TemplateInsightsPageView = ({ templateInsights, userLatency, + shouldDisplayParameters, }: { templateInsights: TemplateInsightsResponse | undefined userLatency: UserLatencyInsightsResponse | undefined + shouldDisplayParameters: boolean }) => { return ( + {shouldDisplayParameters && ( + + )} ) } @@ -261,6 +281,219 @@ const TemplateUsagePanel = ({ ) } +const TemplateParametersUsagePanel = ({ + data, + ...panelProps +}: PanelProps & { + data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined +}) => { + return ( + + + Parameters usage + Last 7 days + + + {!data && } + {data && data.length === 0 && } + {data && + data.length > 0 && + data.map((parameter, parameterIndex) => { + const label = + parameter.display_name !== "" + ? parameter.display_name + : parameter.name + return ( + `1px solid ${theme.palette.divider}`, + width: (theme) => `calc(100% + ${theme.spacing(6)})`, + "&:first-child": { + borderTop: 0, + }, + }} + > + + {label} + theme.palette.text.secondary, + maxWidth: 400, + margin: 0, + }} + > + {parameter.description} + + + + {parameter.values + .sort((a, b) => b.count - a.count) + .map((usage, usageIndex) => ( + + + {usage.count} + + ))} + + + ) + })} + + + ) +} + +const ParameterUsageLabel = ({ + usage, + parameter, +}: { + usage: TemplateParameterValue + parameter: TemplateParameterUsage +}) => { + if (usage.value.trim() === "") { + return ( + theme.palette.text.secondary, + }} + > + Not set + + ) + } + + if (parameter.options) { + const option = parameter.options.find((o) => o.value === usage.value)! + const icon = option.icon + const label = option.name + + return ( + + {icon && ( + + + + )} + {label} + + ) + } + + if (usage.value.startsWith("http")) { + return ( + theme.palette.text.primary, + }} + > + + {usage.value} + + ) + } + + if (parameter.type === "list(string)") { + const values = JSON.parse(usage.value) as string[] + return ( + + {values.map((v, i) => { + return ( + theme.spacing(0.25, 1.5), + borderRadius: 999, + background: (theme) => theme.palette.divider, + whiteSpace: "nowrap", + }} + > + {v} + + ) + })} + + ) + } + + if (parameter.type === "bool") { + return ( + + {usage.value === "false" ? ( + <> + theme.palette.error.light, + }} + /> + False + + ) : ( + <> + theme.palette.success.light, + }} + /> + True + + )} + + ) + } + + return {usage.value} +} + const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, border: `1px solid ${theme.palette.divider}`, From f4122fa9f5d4b02780a8056696f90003f13f79de Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Aug 2023 11:37:49 -0500 Subject: [PATCH 045/156] feat: add auto group create from OIDC (#8884) * add flag for auto create groups * fixup! add flag for auto create groups * sync missing groups Also added a regex filter to filter out groups that are not important --- cli/clibase/option_test.go | 34 +++ cli/clibase/values.go | 38 ++++ cli/server.go | 2 + cli/testdata/coder_server_--help.golden | 8 + cli/testdata/server-config.yaml.golden | 8 + coderd/apidoc/docs.go | 23 ++ coderd/apidoc/swagger.json | 17 ++ coderd/coderd.go | 14 +- coderd/database/dbauthz/dbauthz.go | 7 + coderd/database/dbfake/dbfake.go | 40 ++++ coderd/database/dbmetrics/dbmetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 ++ coderd/database/dump.sql | 10 +- .../migrations/000148_group_source.down.sql | 8 + .../migrations/000148_group_source.up.sql | 15 ++ coderd/database/models.go | 60 +++++ coderd/database/querier.go | 5 + coderd/database/queries.sql.go | 77 ++++++- coderd/database/queries/groups.sql | 22 ++ coderd/userauth.go | 56 +++-- codersdk/deployment.go | 22 ++ codersdk/groups.go | 22 +- docs/admin/audit-logs.md | 2 +- docs/api/enterprise.md | 119 +++++----- docs/api/general.md | 2 + docs/api/schemas.md | 98 +++++--- docs/cli/server.md | 22 ++ enterprise/audit/table.go | 1 + .../cli/testdata/coder_server_--help.golden | 8 + enterprise/coderd/groups.go | 1 + enterprise/coderd/userauth.go | 27 ++- enterprise/coderd/userauth_test.go | 214 +++++++++++++++++- site/src/api/typesGenerated.ts | 9 + site/src/testHelpers/entities.ts | 1 + site/src/utils/groups.ts | 1 + 35 files changed, 887 insertions(+), 128 deletions(-) create mode 100644 coderd/database/migrations/000148_group_source.down.sql create mode 100644 coderd/database/migrations/000148_group_source.up.sql diff --git a/cli/clibase/option_test.go b/cli/clibase/option_test.go index cacd8d3a10793..9affda90668e4 100644 --- a/cli/clibase/option_test.go +++ b/cli/clibase/option_test.go @@ -72,6 +72,40 @@ func TestOptionSet_ParseFlags(t *testing.T) { err := os.FlagSet().Parse([]string{"--some-unknown", "foo"}) require.Error(t, err) }) + + t.Run("RegexValid", func(t *testing.T) { + t.Parallel() + + var regexpString clibase.Regexp + + os := clibase.OptionSet{ + clibase.Option{ + Name: "RegexpString", + Value: ®expString, + Flag: "regexp-string", + }, + } + + err := os.FlagSet().Parse([]string{"--regexp-string", "$test^"}) + require.NoError(t, err) + }) + + t.Run("RegexInvalid", func(t *testing.T) { + t.Parallel() + + var regexpString clibase.Regexp + + os := clibase.OptionSet{ + clibase.Option{ + Name: "RegexpString", + Value: ®expString, + Flag: "regexp-string", + }, + } + + err := os.FlagSet().Parse([]string{"--regexp-string", "(("}) + require.Error(t, err) + }) } func TestOptionSet_ParseEnv(t *testing.T) { diff --git a/cli/clibase/values.go b/cli/clibase/values.go index 288a7c372b152..6ec67d2d1bc09 100644 --- a/cli/clibase/values.go +++ b/cli/clibase/values.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "reflect" + "regexp" "strconv" "strings" "time" @@ -461,6 +462,43 @@ func (e *Enum) String() string { return *e.Value } +type Regexp regexp.Regexp + +func (r *Regexp) MarshalYAML() (interface{}, error) { + return yaml.Node{ + Kind: yaml.ScalarNode, + Value: r.String(), + }, nil +} + +func (r *Regexp) UnmarshalYAML(n *yaml.Node) error { + return r.Set(n.Value) +} + +func (r *Regexp) Set(v string) error { + exp, err := regexp.Compile(v) + if err != nil { + return xerrors.Errorf("invalid regex expression: %w", err) + } + *r = Regexp(*exp) + return nil +} + +func (r Regexp) String() string { + return r.Value().String() +} + +func (r *Regexp) Value() *regexp.Regexp { + if r == nil { + return nil + } + return (*regexp.Regexp)(r) +} + +func (Regexp) Type() string { + return "regexp" +} + var _ pflag.Value = (*YAMLConfigPath)(nil) // YAMLConfigPath is a special value type that encodes a path to a YAML diff --git a/cli/server.go b/cli/server.go index 15cefb364ce3e..55a4db844723f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -597,6 +597,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. AuthURLParams: cfg.OIDC.AuthURLParams.Value, IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(), GroupField: cfg.OIDC.GroupField.String(), + GroupFilter: cfg.OIDC.GroupRegexFilter.Value(), + CreateMissingGroups: cfg.OIDC.GroupAutoCreate.Value(), GroupMapping: cfg.OIDC.GroupMapping.Value, UserRoleField: cfg.OIDC.UserRoleField.String(), UserRoleMapping: cfg.OIDC.UserRoleMapping.Value, diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 121ce98a98bd7..4e1274bb7ad20 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -298,6 +298,9 @@ can safely ignore these settings. GitHub. OIDC Options + --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false) + Automatically creates missing groups from a user's groups claim. + --oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true) Whether new users can sign up with OIDC. @@ -334,6 +337,11 @@ can safely ignore these settings. --oidc-issuer-url string, $CODER_OIDC_ISSUER_URL Issuer URL to use for Login with OIDC. + --oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*) + If provided any group name not matching the regex is ignored. This + allows for filtering out groups that are not needed. This filter is + applied after the group mapping. + --oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email) Scopes to grant when authenticating with OIDC. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 7eab5aba07ecc..f3149b31eec7f 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -271,6 +271,14 @@ oidc: # for when OIDC providers only return group IDs. # (default: {}, type: struct[map[string]string]) groupMapping: {} + # Automatically creates missing groups from a user's groups claim. + # (default: false, type: bool) + enableGroupAutoCreate: false + # If provided any group name not matching the regex is ignored. This allows for + # filtering out groups that are not needed. This filter is applied after the group + # mapping. + # (default: .*, type: regexp) + groupRegexFilter: .* # This field must be set if using the user roles sync feature. Set this to the # name of the claim used to store the user's role. The roles should be sent as an # array of strings. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9faf9de1611cc..f930bf74f48fc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6624,6 +6624,9 @@ const docTemplate = `{ } } }, + "clibase.Regexp": { + "type": "object" + }, "clibase.Struct-array_codersdk_GitAuthConfig": { "type": "object", "properties": { @@ -8274,9 +8277,23 @@ const docTemplate = `{ }, "quota_allowance": { "type": "integer" + }, + "source": { + "$ref": "#/definitions/codersdk.GroupSource" } } }, + "codersdk.GroupSource": { + "type": "string", + "enum": [ + "user", + "oidc" + ], + "x-enum-varnames": [ + "GroupSourceUser", + "GroupSourceOIDC" + ] + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -8583,9 +8600,15 @@ const docTemplate = `{ "email_field": { "type": "string" }, + "group_auto_create": { + "type": "boolean" + }, "group_mapping": { "type": "object" }, + "group_regex_filter": { + "$ref": "#/definitions/clibase.Regexp" + }, "groups_field": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 635457901db36..0d691b237b655 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5874,6 +5874,9 @@ } } }, + "clibase.Regexp": { + "type": "object" + }, "clibase.Struct-array_codersdk_GitAuthConfig": { "type": "object", "properties": { @@ -7430,9 +7433,17 @@ }, "quota_allowance": { "type": "integer" + }, + "source": { + "$ref": "#/definitions/codersdk.GroupSource" } } }, + "codersdk.GroupSource": { + "type": "string", + "enum": ["user", "oidc"], + "x-enum-varnames": ["GroupSourceUser", "GroupSourceOIDC"] + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -7703,9 +7714,15 @@ "email_field": { "type": "string" }, + "group_auto_create": { + "type": "boolean" + }, "group_mapping": { "type": "object" }, + "group_regex_filter": { + "$ref": "#/definitions/clibase.Regexp" + }, "groups_field": { "type": "string" }, diff --git a/coderd/coderd.go b/coderd/coderd.go index 3407be5ac8de2..ed17e0da48e76 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -127,8 +127,8 @@ type Options struct { BaseDERPMap *tailcfg.DERPMap DERPMapUpdateFrequency time.Duration SwaggerEndpoint bool - SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error - SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error + SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error + SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] // AppSecurityKey is the crypto key used to sign and encrypt tokens related to @@ -262,16 +262,16 @@ func New(options *Options) *API { options.TracerProvider = trace.NewNoopTracerProvider() } if options.SetUserGroups == nil { - options.SetUserGroups = func(ctx context.Context, _ database.Store, userID uuid.UUID, groups []string) error { - options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license", - slog.F("user_id", userID), slog.F("groups", groups), + options.SetUserGroups = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, groups []string, createMissingGroups bool) error { + logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license", + slog.F("user_id", userID), slog.F("groups", groups), slog.F("create_missing_groups", createMissingGroups), ) return nil } } if options.SetUserSiteRoles == nil { - options.SetUserSiteRoles = func(ctx context.Context, _ database.Store, userID uuid.UUID, roles []string) error { - options.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license", + options.SetUserSiteRoles = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, roles []string) error { + logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license", slog.F("user_id", userID), slog.F("roles", roles), ) return nil diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 488842dcaf351..21333e6f470ee 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1853,6 +1853,13 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP return q.db.InsertLicense(ctx, arg) } +func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { + if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.InsertMissingGroups(ctx, arg) +} + func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg) } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index c968a52f8d00d..c7ce32c7945b0 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -3641,6 +3641,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar OrganizationID: arg.OrganizationID, AvatarURL: arg.AvatarURL, QuotaAllowance: arg.QuotaAllowance, + Source: database.GroupSourceUser, } q.groups = append(q.groups, group) @@ -3693,6 +3694,45 @@ func (q *FakeQuerier) InsertLicense( return l, nil } +func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + groupNameMap := make(map[string]struct{}) + for _, g := range arg.GroupNames { + groupNameMap[g] = struct{}{} + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, g := range q.groups { + if g.OrganizationID != arg.OrganizationID { + continue + } + delete(groupNameMap, g.Name) + } + + newGroups := make([]database.Group, 0, len(groupNameMap)) + for k := range groupNameMap { + g := database.Group{ + ID: uuid.New(), + Name: k, + OrganizationID: arg.OrganizationID, + AvatarURL: "", + QuotaAllowance: 0, + DisplayName: "", + Source: arg.Source, + } + q.groups = append(q.groups, g) + newGroups = append(newGroups, g) + } + + return newGroups, nil +} + func (q *FakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { if err := validateDatabaseType(arg); err != nil { return database.Organization{}, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 85ee8c26a0d51..1a33f35962f7c 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1110,6 +1110,13 @@ func (m metricsStore) InsertLicense(ctx context.Context, arg database.InsertLice return license, err } +func (m metricsStore) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { + start := time.Now() + r0, r1 := m.s.InsertMissingGroups(ctx, arg) + m.queryLatencies.WithLabelValues("InsertMissingGroups").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { start := time.Now() organization, err := m.s.InsertOrganization(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index cb7278369884b..c3a8905e565ac 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2332,6 +2332,21 @@ func (mr *MockStoreMockRecorder) InsertLicense(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLicense", reflect.TypeOf((*MockStore)(nil).InsertLicense), arg0, arg1) } +// InsertMissingGroups mocks base method. +func (m *MockStore) InsertMissingGroups(arg0 context.Context, arg1 database.InsertMissingGroupsParams) ([]database.Group, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertMissingGroups", arg0, arg1) + ret0, _ := ret[0].([]database.Group) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertMissingGroups indicates an expected call of InsertMissingGroups. +func (mr *MockStoreMockRecorder) InsertMissingGroups(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMissingGroups", reflect.TypeOf((*MockStore)(nil).InsertMissingGroups), arg0, arg1) +} + // InsertOrganization mocks base method. func (m *MockStore) InsertOrganization(arg0 context.Context, arg1 database.InsertOrganizationParams) (database.Organization, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f121fccf8cebb..9dfb9ded10e64 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -31,6 +31,11 @@ CREATE TYPE build_reason AS ENUM ( 'autodelete' ); +CREATE TYPE group_source AS ENUM ( + 'user', + 'oidc' +); + CREATE TYPE log_level AS ENUM ( 'trace', 'debug', @@ -299,11 +304,14 @@ CREATE TABLE groups ( organization_id uuid NOT NULL, avatar_url text DEFAULT ''::text NOT NULL, quota_allowance integer DEFAULT 0 NOT NULL, - display_name text DEFAULT ''::text NOT NULL + display_name text DEFAULT ''::text NOT NULL, + source group_source DEFAULT 'user'::group_source NOT NULL ); COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.'; +COMMENT ON COLUMN groups.source IS 'Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.'; + CREATE TABLE licenses ( id integer NOT NULL, uploaded_at timestamp with time zone NOT NULL, diff --git a/coderd/database/migrations/000148_group_source.down.sql b/coderd/database/migrations/000148_group_source.down.sql new file mode 100644 index 0000000000000..504c227d186bb --- /dev/null +++ b/coderd/database/migrations/000148_group_source.down.sql @@ -0,0 +1,8 @@ +BEGIN; + +ALTER TABLE groups + DROP COLUMN source; + +DROP TYPE group_source; + +COMMIT; diff --git a/coderd/database/migrations/000148_group_source.up.sql b/coderd/database/migrations/000148_group_source.up.sql new file mode 100644 index 0000000000000..d06e89ca2b1d6 --- /dev/null +++ b/coderd/database/migrations/000148_group_source.up.sql @@ -0,0 +1,15 @@ +BEGIN; + +CREATE TYPE group_source AS ENUM ( + -- User created groups + 'user', + -- Groups created by the system through oidc sync + 'oidc' +); + +ALTER TABLE groups + ADD COLUMN source group_source NOT NULL DEFAULT 'user'; + +COMMENT ON COLUMN groups.source IS 'Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.'; + +COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index 4e34989b09ae9..d3b7700b56bfa 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -281,6 +281,64 @@ func AllBuildReasonValues() []BuildReason { } } +type GroupSource string + +const ( + GroupSourceUser GroupSource = "user" + GroupSourceOidc GroupSource = "oidc" +) + +func (e *GroupSource) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = GroupSource(s) + case string: + *e = GroupSource(s) + default: + return fmt.Errorf("unsupported scan type for GroupSource: %T", src) + } + return nil +} + +type NullGroupSource struct { + GroupSource GroupSource `json:"group_source"` + Valid bool `json:"valid"` // Valid is true if GroupSource is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullGroupSource) Scan(value interface{}) error { + if value == nil { + ns.GroupSource, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.GroupSource.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullGroupSource) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.GroupSource), nil +} + +func (e GroupSource) Valid() bool { + switch e { + case GroupSourceUser, + GroupSourceOidc: + return true + } + return false +} + +func AllGroupSourceValues() []GroupSource { + return []GroupSource{ + GroupSourceUser, + GroupSourceOidc, + } +} + type LogLevel string const ( @@ -1498,6 +1556,8 @@ type Group struct { QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` // Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string. DisplayName string `db:"display_name" json:"display_name"` + // Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync. + Source GroupSource `db:"source" json:"source"` } type GroupMember struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b308589ffc350..6c5483a12795a 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -206,6 +206,11 @@ type sqlcQuerier interface { InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) + // Inserts any group by name that does not exist. All new groups are given + // a random uuid, are inserted into the same organization. They have the default + // values for avatar, display name, and quota allowance (all zero values). + // If the name conflicts, do nothing. + InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8748e4d7ca9ed..95efcc4369baa 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1180,7 +1180,7 @@ func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error { const getGroupByID = `-- name: GetGroupByID :one SELECT - id, name, organization_id, avatar_url, quota_allowance, display_name + id, name, organization_id, avatar_url, quota_allowance, display_name, source FROM groups WHERE @@ -1199,13 +1199,14 @@ func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, err &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ) return i, err } const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one SELECT - id, name, organization_id, avatar_url, quota_allowance, display_name + id, name, organization_id, avatar_url, quota_allowance, display_name, source FROM groups WHERE @@ -1231,13 +1232,14 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ) return i, err } const getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many SELECT - id, name, organization_id, avatar_url, quota_allowance, display_name + id, name, organization_id, avatar_url, quota_allowance, display_name, source FROM groups WHERE @@ -1262,6 +1264,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ); err != nil { return nil, err } @@ -1283,7 +1286,7 @@ INSERT INTO groups ( organization_id ) VALUES - ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name + ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source ` // We use the organization_id as the id @@ -1299,6 +1302,7 @@ func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uui &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ) return i, err } @@ -1313,7 +1317,7 @@ INSERT INTO groups ( quota_allowance ) VALUES - ($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name + ($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source ` type InsertGroupParams struct { @@ -1342,10 +1346,70 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ) return i, err } +const insertMissingGroups = `-- name: InsertMissingGroups :many +INSERT INTO groups ( + id, + name, + organization_id, + source +) +SELECT + gen_random_uuid(), + group_name, + $1, + $2 +FROM + UNNEST($3 :: text[]) AS group_name +ON CONFLICT DO NOTHING +RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source +` + +type InsertMissingGroupsParams struct { + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Source GroupSource `db:"source" json:"source"` + GroupNames []string `db:"group_names" json:"group_names"` +} + +// Inserts any group by name that does not exist. All new groups are given +// a random uuid, are inserted into the same organization. They have the default +// values for avatar, display name, and quota allowance (all zero values). +// If the name conflicts, do nothing. +func (q *sqlQuerier) InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) { + rows, err := q.db.QueryContext(ctx, insertMissingGroups, arg.OrganizationID, arg.Source, pq.Array(arg.GroupNames)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Group + for rows.Next() { + var i Group + if err := rows.Scan( + &i.ID, + &i.Name, + &i.OrganizationID, + &i.AvatarURL, + &i.QuotaAllowance, + &i.DisplayName, + &i.Source, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateGroupByID = `-- name: UpdateGroupByID :one UPDATE groups @@ -1356,7 +1420,7 @@ SET quota_allowance = $4 WHERE id = $5 -RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name +RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source ` type UpdateGroupByIDParams struct { @@ -1383,6 +1447,7 @@ func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDPar &i.AvatarURL, &i.QuotaAllowance, &i.DisplayName, + &i.Source, ) return i, err } diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index e1ee6635a5fe0..da47116983c87 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -42,6 +42,28 @@ INSERT INTO groups ( VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; +-- name: InsertMissingGroups :many +-- Inserts any group by name that does not exist. All new groups are given +-- a random uuid, are inserted into the same organization. They have the default +-- values for avatar, display name, and quota allowance (all zero values). +INSERT INTO groups ( + id, + name, + organization_id, + source +) +SELECT + gen_random_uuid(), + group_name, + @organization_id, + @source +FROM + UNNEST(@group_names :: text[]) AS group_name +-- If the name conflicts, do nothing. +ON CONFLICT DO NOTHING +RETURNING *; + + -- We use the organization_id as the id -- for simplicity since all users is -- every member of the org. diff --git a/coderd/userauth.go b/coderd/userauth.go index f1e110c08bfdc..03ae7a6379672 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/mail" + "regexp" "sort" "strconv" "strings" @@ -688,6 +689,13 @@ type OIDCConfig struct { // groups. If the group field is the empty string, then no group updates // will ever come from the OIDC provider. GroupField string + // CreateMissingGroups controls whether groups returned by the OIDC provider + // are automatically created in Coder if they are missing. + CreateMissingGroups bool + // GroupFilter is a regular expression that filters the groups returned by + // the OIDC provider. Any group not matched by this regex will be ignored. + // If the group filter is nil, then no group filtering will occur. + GroupFilter *regexp.Regexp // GroupMapping controls how groups returned by the OIDC provider get mapped // to groups within Coder. // map[oidcGroupName]coderGroupName @@ -1029,19 +1037,21 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { } params := (&oauthLoginParams{ - User: user, - Link: link, - State: state, - LinkedID: oidcLinkedID(idToken), - LoginType: database.LoginTypeOIDC, - AllowSignups: api.OIDCConfig.AllowSignups, - Email: email, - Username: username, - AvatarURL: picture, - UsingGroups: usingGroups, - UsingRoles: api.OIDCConfig.RoleSyncEnabled(), - Roles: roles, - Groups: groups, + User: user, + Link: link, + State: state, + LinkedID: oidcLinkedID(idToken), + LoginType: database.LoginTypeOIDC, + AllowSignups: api.OIDCConfig.AllowSignups, + Email: email, + Username: username, + AvatarURL: picture, + UsingGroups: usingGroups, + UsingRoles: api.OIDCConfig.RoleSyncEnabled(), + Roles: roles, + Groups: groups, + CreateMissingGroups: api.OIDCConfig.CreateMissingGroups, + GroupFilter: api.OIDCConfig.GroupFilter, }).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) { return audit.InitRequest[database.User](rw, params) }) @@ -1125,8 +1135,10 @@ type oauthLoginParams struct { AvatarURL string // Is UsingGroups is true, then the user will be assigned // to the Groups provided. - UsingGroups bool - Groups []string + UsingGroups bool + CreateMissingGroups bool + Groups []string + GroupFilter *regexp.Regexp // Is UsingRoles is true, then the user will be assigned // the roles provided. UsingRoles bool @@ -1342,8 +1354,18 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C // Ensure groups are correct. if params.UsingGroups { + filtered := params.Groups + if params.GroupFilter != nil { + filtered = make([]string, 0, len(params.Groups)) + for _, group := range params.Groups { + if params.GroupFilter.MatchString(group) { + filtered = append(filtered, group) + } + } + } + //nolint:gocritic - err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), tx, user.ID, params.Groups) + err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), logger, tx, user.ID, filtered, params.CreateMissingGroups) if err != nil { return xerrors.Errorf("set user groups: %w", err) } @@ -1362,7 +1384,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C } //nolint:gocritic - err := api.Options.SetUserSiteRoles(dbauthz.AsSystemRestricted(ctx), tx, user.ID, filtered) + err := api.Options.SetUserSiteRoles(dbauthz.AsSystemRestricted(ctx), logger, tx, user.ID, filtered) if err != nil { return httpError{ code: http.StatusBadRequest, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index fdc5b92af28de..96cb9d19516f3 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -271,6 +271,8 @@ type OIDCConfig struct { EmailField clibase.String `json:"email_field" typescript:",notnull"` AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"` IgnoreUserInfo clibase.Bool `json:"ignore_user_info" typescript:",notnull"` + GroupAutoCreate clibase.Bool `json:"group_auto_create" typescript:",notnull"` + GroupRegexFilter clibase.Regexp `json:"group_regex_filter" typescript:",notnull"` GroupField clibase.String `json:"groups_field" typescript:",notnull"` GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"` UserRoleField clibase.String `json:"user_role_field" typescript:",notnull"` @@ -1066,6 +1068,26 @@ when required by your organization's security policy.`, Group: &deploymentGroupOIDC, YAML: "groupMapping", }, + { + Name: "Enable OIDC Group Auto Create", + Description: "Automatically creates missing groups from a user's groups claim.", + Flag: "oidc-group-auto-create", + Env: "CODER_OIDC_GROUP_AUTO_CREATE", + Default: "false", + Value: &c.OIDC.GroupAutoCreate, + Group: &deploymentGroupOIDC, + YAML: "enableGroupAutoCreate", + }, + { + Name: "OIDC Regex Group Filter", + Description: "If provided any group name not matching the regex is ignored. This allows for filtering out groups that are not needed. This filter is applied after the group mapping.", + Flag: "oidc-group-regex-filter", + Env: "CODER_OIDC_GROUP_REGEX_FILTER", + Default: ".*", + Value: &c.OIDC.GroupRegexFilter, + Group: &deploymentGroupOIDC, + YAML: "groupRegexFilter", + }, { Name: "OIDC User Role Field", Description: "This field must be set if using the user roles sync feature. Set this to the name of the claim used to store the user's role. The roles should be sent as an array of strings.", diff --git a/codersdk/groups.go b/codersdk/groups.go index c04267e4e0eb2..b50455d693469 100644 --- a/codersdk/groups.go +++ b/codersdk/groups.go @@ -10,6 +10,13 @@ import ( "golang.org/x/xerrors" ) +type GroupSource string + +const ( + GroupSourceUser GroupSource = "user" + GroupSourceOIDC GroupSource = "oidc" +) + type CreateGroupRequest struct { Name string `json:"name"` DisplayName string `json:"display_name"` @@ -18,13 +25,14 @@ type CreateGroupRequest struct { } type Group struct { - ID uuid.UUID `json:"id" format:"uuid"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - Members []User `json:"members"` - AvatarURL string `json:"avatar_url"` - QuotaAllowance int `json:"quota_allowance"` + ID uuid.UUID `json:"id" format:"uuid"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + Members []User `json:"members"` + AvatarURL string `json:"avatar_url"` + QuotaAllowance int `json:"quota_allowance"` + Source GroupSource `json:"source"` } func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGroupRequest) (Group, error) { diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 882a7274c737f..230e99be53f7e 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -13,7 +13,7 @@ We track the following resources: | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| | AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
restart_requirement_days_of_weektrue
restart_requirement_weekstrue
updated_atfalse
user_acltrue
| diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index d859ac59ad09b..fc887cd12b6e3 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -197,7 +197,8 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` @@ -258,7 +259,8 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` @@ -319,7 +321,8 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` @@ -455,7 +458,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ] ``` @@ -470,28 +474,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups Status Code **200** -| Name | Type | Required | Restrictions | Description | -| --------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- | -| `[array item]` | array | false | | | -| `» avatar_url` | string | false | | | -| `» display_name` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» members` | array | false | | | -| `»» avatar_url` | string(uri) | false | | | -| `»» created_at` | string(date-time) | true | | | -| `»» email` | string(email) | true | | | -| `»» id` | string(uuid) | true | | | -| `»» last_seen_at` | string(date-time) | false | | | -| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | -| `»» organization_ids` | array | false | | | -| `»» roles` | array | false | | | -| `»»» display_name` | string | false | | | -| `»»» name` | string | false | | | -| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | -| `»» username` | string | true | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» quota_allowance` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| --------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» avatar_url` | string | false | | | +| `» display_name` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» members` | array | false | | | +| `»» avatar_url` | string(uri) | false | | | +| `»» created_at` | string(date-time) | true | | | +| `»» email` | string(email) | true | | | +| `»» id` | string(uuid) | true | | | +| `»» last_seen_at` | string(date-time) | false | | | +| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | +| `»» organization_ids` | array | false | | | +| `»» roles` | array | false | | | +| `»»» display_name` | string | false | | | +| `»»» name` | string | false | | | +| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | +| `»» username` | string | true | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» quota_allowance` | integer | false | | | +| `» source` | [codersdk.GroupSource](schemas.md#codersdkgroupsource) | false | | | #### Enumerated Values @@ -504,6 +509,8 @@ Status Code **200** | `login_type` | `none` | | `status` | `active` | | `status` | `suspended` | +| `source` | `user` | +| `source` | `oidc` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -569,7 +576,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` @@ -631,7 +639,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` @@ -1202,7 +1211,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ], "users": [ @@ -1238,30 +1248,31 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ---------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- | -| `[array item]` | array | false | | | -| `» groups` | array | false | | | -| `»» avatar_url` | string | false | | | -| `»» display_name` | string | false | | | -| `»» id` | string(uuid) | false | | | -| `»» members` | array | false | | | -| `»»» avatar_url` | string(uri) | false | | | -| `»»» created_at` | string(date-time) | true | | | -| `»»» email` | string(email) | true | | | -| `»»» id` | string(uuid) | true | | | -| `»»» last_seen_at` | string(date-time) | false | | | -| `»»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | -| `»»» organization_ids` | array | false | | | -| `»»» roles` | array | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» name` | string | false | | | -| `»»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | -| `»»» username` | string | true | | | -| `»» name` | string | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» quota_allowance` | integer | false | | | -| `» users` | array | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» groups` | array | false | | | +| `»» avatar_url` | string | false | | | +| `»» display_name` | string | false | | | +| `»» id` | string(uuid) | false | | | +| `»» members` | array | false | | | +| `»»» avatar_url` | string(uri) | false | | | +| `»»» created_at` | string(date-time) | true | | | +| `»»» email` | string(email) | true | | | +| `»»» id` | string(uuid) | true | | | +| `»»» last_seen_at` | string(date-time) | false | | | +| `»»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | +| `»»» organization_ids` | array | false | | | +| `»»» roles` | array | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» name` | string | false | | | +| `»»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | +| `»»» username` | string | true | | | +| `»» name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» quota_allowance` | integer | false | | | +| `»» source` | [codersdk.GroupSource](schemas.md#codersdkgroupsource) | false | | | +| `» users` | array | false | | | #### Enumerated Values @@ -1274,6 +1285,8 @@ Status Code **200** | `login_type` | `none` | | `status` | `active` | | `status` | `suspended` | +| `source` | `user` | +| `source` | `oidc` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/general.md b/docs/api/general.md index 3f1f90a02d851..62341f1dcbf27 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -260,7 +260,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "client_secret": "string", "email_domain": ["string"], "email_field": "string", + "group_auto_create": true, "group_mapping": {}, + "group_regex_filter": {}, "groups_field": "string", "icon_url": { "forceQuery": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 4ab6eceeef96d..7aed5d4f60022 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -595,6 +595,16 @@ | `value_source` | [clibase.ValueSource](#clibasevaluesource) | false | | | | `yaml` | string | false | | Yaml is the YAML key used to configure this option. If unset, YAML configuring is disabled. | +## clibase.Regexp + +```json +{} +``` + +### Properties + +_None_ + ## clibase.Struct-array_codersdk_GitAuthConfig ```json @@ -788,7 +798,8 @@ ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ], "users": [ @@ -2054,7 +2065,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_secret": "string", "email_domain": ["string"], "email_field": "string", + "group_auto_create": true, "group_mapping": {}, + "group_regex_filter": {}, "groups_field": "string", "icon_url": { "forceQuery": true, @@ -2412,7 +2425,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_secret": "string", "email_domain": ["string"], "email_field": "string", + "group_auto_create": true, "group_mapping": {}, + "group_regex_filter": {}, "groups_field": "string", "icon_url": { "forceQuery": true, @@ -2959,21 +2974,38 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ], "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "quota_allowance": 0 + "quota_allowance": 0, + "source": "user" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------- | --------------------------------------- | -------- | ------------ | ----------- | -| `avatar_url` | string | false | | | -| `display_name` | string | false | | | -| `id` | string | false | | | -| `members` | array of [codersdk.User](#codersdkuser) | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `quota_allowance` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------------- | -------------------------------------------- | -------- | ------------ | ----------- | +| `avatar_url` | string | false | | | +| `display_name` | string | false | | | +| `id` | string | false | | | +| `members` | array of [codersdk.User](#codersdkuser) | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `quota_allowance` | integer | false | | | +| `source` | [codersdk.GroupSource](#codersdkgroupsource) | false | | | + +## codersdk.GroupSource + +```json +"user" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ------ | +| `user` | +| `oidc` | ## codersdk.Healthcheck @@ -3305,7 +3337,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_secret": "string", "email_domain": ["string"], "email_field": "string", + "group_auto_create": true, "group_mapping": {}, + "group_regex_filter": {}, "groups_field": "string", "icon_url": { "forceQuery": true, @@ -3334,26 +3368,28 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------------- | -------------------------- | -------- | ------------ | ----------- | -| `allow_signups` | boolean | false | | | -| `auth_url_params` | object | false | | | -| `client_id` | string | false | | | -| `client_secret` | string | false | | | -| `email_domain` | array of string | false | | | -| `email_field` | string | false | | | -| `group_mapping` | object | false | | | -| `groups_field` | string | false | | | -| `icon_url` | [clibase.URL](#clibaseurl) | false | | | -| `ignore_email_verified` | boolean | false | | | -| `ignore_user_info` | boolean | false | | | -| `issuer_url` | string | false | | | -| `scopes` | array of string | false | | | -| `sign_in_text` | string | false | | | -| `user_role_field` | string | false | | | -| `user_role_mapping` | object | false | | | -| `user_roles_default` | array of string | false | | | -| `username_field` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------------------- | -------------------------------- | -------- | ------------ | ----------- | +| `allow_signups` | boolean | false | | | +| `auth_url_params` | object | false | | | +| `client_id` | string | false | | | +| `client_secret` | string | false | | | +| `email_domain` | array of string | false | | | +| `email_field` | string | false | | | +| `group_auto_create` | boolean | false | | | +| `group_mapping` | object | false | | | +| `group_regex_filter` | [clibase.Regexp](#clibaseregexp) | false | | | +| `groups_field` | string | false | | | +| `icon_url` | [clibase.URL](#clibaseurl) | false | | | +| `ignore_email_verified` | boolean | false | | | +| `ignore_user_info` | boolean | false | | | +| `issuer_url` | string | false | | | +| `scopes` | array of string | false | | | +| `sign_in_text` | string | false | | | +| `user_role_field` | string | false | | | +| `user_role_mapping` | object | false | | | +| `user_roles_default` | array of string | false | | | +| `username_field` | string | false | | | ## codersdk.Organization diff --git a/docs/cli/server.md b/docs/cli/server.md index 9591dc8041f9f..27658ee0d16e8 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -243,6 +243,17 @@ Disable automatic session expiry bumping due to activity. This forces all sessio Specifies the custom docs URL. +### --oidc-group-auto-create + +| | | +| ----------- | ------------------------------------------ | +| Type | bool | +| Environment | $CODER_OIDC_GROUP_AUTO_CREATE | +| YAML | oidc.enableGroupAutoCreate | +| Default | false | + +Automatically creates missing groups from a user's groups claim. + ### --enable-terraform-debug-mode | | | @@ -521,6 +532,17 @@ Ignore the userinfo endpoint and only use the ID token for user information. Issuer URL to use for Login with OIDC. +### --oidc-group-regex-filter + +| | | +| ----------- | ------------------------------------------- | +| Type | regexp | +| Environment | $CODER_OIDC_GROUP_REGEX_FILTER | +| YAML | oidc.groupRegexFilter | +| Default | .\* | + +If provided any group name not matching the regex is ignored. This allows for filtering out groups that are not needed. This filter is applied after the group mapping. + ### --oidc-scopes | | | diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index b5ab50457c963..17c91ff8adfb6 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -156,6 +156,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "avatar_url": ActionTrack, "quota_allowance": ActionTrack, "members": ActionTrack, + "source": ActionIgnore, }, &database.APIKey{}: { "id": ActionIgnore, diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 121ce98a98bd7..4e1274bb7ad20 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -298,6 +298,9 @@ can safely ignore these settings. GitHub. OIDC Options + --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false) + Automatically creates missing groups from a user's groups claim. + --oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true) Whether new users can sign up with OIDC. @@ -334,6 +337,11 @@ can safely ignore these settings. --oidc-issuer-url string, $CODER_OIDC_ISSUER_URL Issuer URL to use for Login with OIDC. + --oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*) + If provided any group name not matching the regex is ignored. This + allows for filtering out groups that are not needed. This filter is + applied after the group mapping. + --oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email) Scopes to grant when authenticating with OIDC. diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index b6f126e1f62e0..5119f3f91a5d8 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -409,6 +409,7 @@ func convertGroup(g database.Group, users []database.User) codersdk.Group { AvatarURL: g.AvatarURL, QuotaAllowance: int(g.QuotaAllowance), Members: convertUsers(users, orgs), + Source: codersdk.GroupSource(g.Source), } } diff --git a/enterprise/coderd/userauth.go b/enterprise/coderd/userauth.go index 86aa9f0ddf88b..98833263355e3 100644 --- a/enterprise/coderd/userauth.go +++ b/enterprise/coderd/userauth.go @@ -9,10 +9,12 @@ import ( "cdr.dev/slog" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/codersdk" ) -func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uuid.UUID, groupNames []string) error { +// nolint: revive +func (api *API) setUserGroups(ctx context.Context, logger slog.Logger, db database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error { api.entitlementsMu.RLock() enabled := api.entitlements.Features[codersdk.FeatureTemplateRBAC].Enabled api.entitlementsMu.RUnlock() @@ -39,6 +41,25 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui return xerrors.Errorf("delete user groups: %w", err) } + if createMissingGroups { + // This is the system creating these additional groups, so we use the system restricted context. + // nolint:gocritic + created, err := tx.InsertMissingGroups(dbauthz.AsSystemRestricted(ctx), database.InsertMissingGroupsParams{ + OrganizationID: orgs[0].ID, + GroupNames: groupNames, + Source: database.GroupSourceOidc, + }) + if err != nil { + return xerrors.Errorf("insert missing groups: %w", err) + } + if len(created) > 0 { + logger.Debug(ctx, "auto created missing groups", + slog.F("org_id", orgs[0].ID), + slog.F("created", created), + ) + } + } + // Re-add the user to all groups returned by the auth provider. err = tx.InsertUserGroupsByName(ctx, database.InsertUserGroupsByNameParams{ UserID: userID, @@ -53,13 +74,13 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui }, nil) } -func (api *API) setUserSiteRoles(ctx context.Context, db database.Store, userID uuid.UUID, roles []string) error { +func (api *API) setUserSiteRoles(ctx context.Context, logger slog.Logger, db database.Store, userID uuid.UUID, roles []string) error { api.entitlementsMu.RLock() enabled := api.entitlements.Features[codersdk.FeatureUserRoleManagement].Enabled api.entitlementsMu.RUnlock() if !enabled { - api.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged", + logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged", slog.F("user_id", userID), slog.F("roles", roles), ) return nil diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index 2cb110abe987b..176a308595205 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -5,10 +5,9 @@ import ( "fmt" "io" "net/http" + "regexp" "testing" - "github.com/coder/coder/enterprise/coderd/license" - "github.com/golang-jwt/jwt" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -16,9 +15,13 @@ import ( "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -354,6 +357,213 @@ func TestUserOIDC(t *testing.T) { }) } +func TestGroupSync(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + modCfg func(cfg *coderd.OIDCConfig) + // initialOrgGroups is initial groups in the org + initialOrgGroups []string + // initialUserGroups is initial groups for the user + initialUserGroups []string + // expectedUserGroups is expected groups for the user + expectedUserGroups []string + // expectedOrgGroups is expected all groups on the system + expectedOrgGroups []string + claims jwt.MapClaims + }{ + { + name: "NoGroups", + modCfg: func(cfg *coderd.OIDCConfig) { + }, + initialOrgGroups: []string{}, + expectedUserGroups: []string{}, + expectedOrgGroups: []string{}, + claims: jwt.MapClaims{}, + }, + { + name: "GroupSyncDisabled", + modCfg: func(cfg *coderd.OIDCConfig) { + // Disable group sync + cfg.GroupField = "" + cfg.GroupFilter = regexp.MustCompile(".*") + }, + initialOrgGroups: []string{"a", "b", "c", "d"}, + initialUserGroups: []string{"b", "c", "d"}, + expectedUserGroups: []string{"b", "c", "d"}, + expectedOrgGroups: []string{"a", "b", "c", "d"}, + claims: jwt.MapClaims{}, + }, + { + // From a,c,b -> b,c,d + name: "ChangeUserGroups", + modCfg: func(cfg *coderd.OIDCConfig) { + cfg.GroupMapping = map[string]string{ + "D": "d", + } + }, + initialOrgGroups: []string{"a", "b", "c", "d"}, + initialUserGroups: []string{"a", "b", "c"}, + expectedUserGroups: []string{"b", "c", "d"}, + expectedOrgGroups: []string{"a", "b", "c", "d"}, + claims: jwt.MapClaims{ + // D -> d mapped + "groups": []string{"b", "c", "D"}, + }, + }, + { + // From a,c,b -> [] + name: "RemoveAllGroups", + modCfg: func(cfg *coderd.OIDCConfig) { + cfg.GroupFilter = regexp.MustCompile(".*") + }, + initialOrgGroups: []string{"a", "b", "c", "d"}, + initialUserGroups: []string{"a", "b", "c"}, + expectedUserGroups: []string{}, + expectedOrgGroups: []string{"a", "b", "c", "d"}, + claims: jwt.MapClaims{ + // No claim == no groups + }, + }, + { + // From a,c,b -> b,c,d,e,f + name: "CreateMissingGroups", + modCfg: func(cfg *coderd.OIDCConfig) { + cfg.CreateMissingGroups = true + }, + initialOrgGroups: []string{"a", "b", "c", "d"}, + initialUserGroups: []string{"a", "b", "c"}, + expectedUserGroups: []string{"b", "c", "d", "e", "f"}, + expectedOrgGroups: []string{"a", "b", "c", "d", "e", "f"}, + claims: jwt.MapClaims{ + "groups": []string{"b", "c", "d", "e", "f"}, + }, + }, + { + // From a,c,b -> b,c,d,e,f + name: "CreateMissingGroupsFilter", + modCfg: func(cfg *coderd.OIDCConfig) { + cfg.CreateMissingGroups = true + // Only single letter groups + cfg.GroupFilter = regexp.MustCompile("^[a-z]$") + cfg.GroupMapping = map[string]string{ + // Does not match the filter, but does after being mapped! + "zebra": "z", + } + }, + initialOrgGroups: []string{"a", "b", "c", "d"}, + initialUserGroups: []string{"a", "b", "c"}, + expectedUserGroups: []string{"b", "c", "d", "e", "f", "z"}, + expectedOrgGroups: []string{"a", "b", "c", "d", "e", "f", "z"}, + claims: jwt.MapClaims{ + "groups": []string{ + "b", "c", "d", "e", "f", + // These groups are ignored + "excess", "ignore", "dumb", "foobar", "zebra", + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + conf := coderdtest.NewOIDCConfig(t, "") + + config := conf.OIDCConfig(t, jwt.MapClaims{}, tc.modCfg) + + client, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + OIDCConfig: config, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + }, + }) + + admin, err := client.User(ctx, "me") + require.NoError(t, err) + require.Len(t, admin.OrganizationIDs, 1) + + // Setup + initialGroups := make(map[string]codersdk.Group) + for _, group := range tc.initialOrgGroups { + newGroup, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{ + Name: group, + }) + require.NoError(t, err) + require.Len(t, newGroup.Members, 0) + initialGroups[group] = newGroup + } + + // Create the user and add them to their initial groups + _, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationIDs[0]) + for _, group := range tc.initialUserGroups { + _, err := client.PatchGroup(ctx, initialGroups[group].ID, codersdk.PatchGroupRequest{ + AddUsers: []string{user.ID.String()}, + }) + require.NoError(t, err) + } + + // nolint:gocritic + _, err = api.Database.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ + NewLoginType: database.LoginTypeOIDC, + UserID: user.ID, + }) + require.NoError(t, err, "user must be oidc type") + + // Log in the new user + tc.claims["email"] = user.Email + resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.claims)) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + _ = resp.Body.Close() + + orgGroups, err := client.GroupsByOrganization(ctx, admin.OrganizationIDs[0]) + require.NoError(t, err) + + for _, group := range orgGroups { + if slice.Contains(tc.initialOrgGroups, group.Name) { + require.Equal(t, group.Source, codersdk.GroupSourceUser) + } else { + require.Equal(t, group.Source, codersdk.GroupSourceOIDC) + } + } + + orgGroupsMap := make(map[string]struct{}) + for _, group := range orgGroups { + orgGroupsMap[group.Name] = struct{}{} + } + + for _, expected := range tc.expectedOrgGroups { + if _, ok := orgGroupsMap[expected]; !ok { + t.Errorf("expected group %s not found", expected) + } + delete(orgGroupsMap, expected) + } + require.Empty(t, orgGroupsMap, "unexpected groups found") + + expectedUserGroups := make(map[string]struct{}) + for _, group := range tc.expectedUserGroups { + expectedUserGroups[group] = struct{}{} + } + + for _, group := range orgGroups { + userInGroup := slice.ContainsCompare(group.Members, codersdk.User{Email: user.Email}, func(a, b codersdk.User) bool { + return a.Email == b.Email + }) + if _, ok := expectedUserGroups[group.Name]; ok { + require.Truef(t, userInGroup, "user should be in group %s", group.Name) + } else { + require.Falsef(t, userInGroup, "user should not be in group %s", group.Name) + } + } + }) + } +} + func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response { t.Helper() client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c01f4d94385ba..3e5acc5671299 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -513,6 +513,7 @@ export interface Group { readonly members: User[] readonly avatar_url: string readonly quota_allowance: number + readonly source: GroupSource } // From codersdk/workspaceapps.go @@ -626,6 +627,10 @@ export interface OIDCConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly auth_url_params: any readonly ignore_user_info: boolean + readonly group_auto_create: boolean + // Named type "github.com/coder/coder/cli/clibase.Regexp" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly group_regex_filter: any readonly groups_field: string // Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type @@ -1639,6 +1644,10 @@ export const GitProviders: GitProvider[] = [ "gitlab", ] +// From codersdk/groups.go +export type GroupSource = "oidc" | "user" +export const GroupSources: GroupSource[] = ["oidc", "user"] + // From codersdk/insights.go export type InsightsReportInterval = "day" export const InsightsReportIntervals: InsightsReportInterval[] = ["day"] diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3d28ae7ad1ffb..94eaa3add0211 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1670,6 +1670,7 @@ export const MockGroup: TypesGen.Group = { organization_id: MockOrganization.id, members: [MockUser, MockUser2], quota_allowance: 5, + source: "user", } export const MockTemplateACL: TypesGen.TemplateACL = { diff --git a/site/src/utils/groups.ts b/site/src/utils/groups.ts index 9afc048ce7cef..88dad942beddb 100644 --- a/site/src/utils/groups.ts +++ b/site/src/utils/groups.ts @@ -8,6 +8,7 @@ export const everyOneGroup = (organizationId: string): Group => ({ members: [], avatar_url: "", quota_allowance: 0, + source: "user", }) export const getGroupSubtitle = (group: Group): string => { From 76b1594670ca892640a4dfbf157b44223423e866 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 8 Aug 2023 13:43:21 -0300 Subject: [PATCH 046/156] feat(site): add date range picker for the template insights (#8976) --- site/package.json | 2 + site/pnpm-lock.yaml | 44 ++++ .../TemplateInsightsPage/DateRange.tsx | 234 ++++++++++++++++++ .../TemplateInsightsPage.tsx | 86 ++++--- .../TemplateInsightsPage/utils.test.ts | 36 +++ .../TemplateInsightsPage/utils.ts | 26 ++ 6 files changed, 391 insertions(+), 37 deletions(-) create mode 100644 site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx create mode 100644 site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts create mode 100644 site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts diff --git a/site/package.json b/site/package.json index 538e158218292..8d08e287ebf1a 100644 --- a/site/package.json +++ b/site/package.json @@ -48,6 +48,7 @@ "@types/color-convert": "2.0.0", "@types/lodash": "4.14.196", "@types/react-color": "3.0.6", + "@types/react-date-range": "1.4.4", "@types/semver": "7.5.0", "@vitejs/plugin-react": "4.0.1", "@xstate/inspect": "0.8.0", @@ -77,6 +78,7 @@ "react-chartjs-2": "5.2.0", "react-color": "2.19.3", "react-confetti": "6.1.0", + "react-date-range": "1.4.0", "react-dom": "18.2.0", "react-headless-tabs": "6.0.3", "react-helmet-async": "1.3.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index abb876c024c61..0746844223c01 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -60,6 +60,9 @@ dependencies: '@types/react-color': specifier: 3.0.6 version: 3.0.6 + '@types/react-date-range': + specifier: 1.4.4 + version: 1.4.4 '@types/semver': specifier: 7.5.0 version: 7.5.0 @@ -147,6 +150,9 @@ dependencies: react-confetti: specifier: 6.1.0 version: 6.1.0(react@18.2.0) + react-date-range: + specifier: 1.4.0 + version: 1.4.0(date-fns@2.30.0)(react@18.2.0) react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) @@ -4952,6 +4958,13 @@ packages: '@types/reactcss': 1.2.6 dev: false + /@types/react-date-range@1.4.4: + resolution: {integrity: sha512-9Y9NyNgaCsEVN/+O4HKuxzPbVjRVBGdOKRxMDcsTRWVG62lpYgnxefNckTXDWup8FvczoqPW0+ESZR6R1yymDg==} + dependencies: + '@types/react': 18.2.6 + date-fns: 2.30.0 + dev: false + /@types/react-dom@18.2.4: resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} dependencies: @@ -6226,6 +6239,10 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -11179,6 +11196,20 @@ packages: tween-functions: 1.2.0 dev: false + /react-date-range@1.4.0(date-fns@2.30.0)(react@18.2.0): + resolution: {integrity: sha512-+9t0HyClbCqw1IhYbpWecjsiaftCeRN5cdhsi9v06YdimwyMR2yYHWcgVn3URwtN/txhqKpEZB6UX1fHpvK76w==} + peerDependencies: + date-fns: 2.0.0-alpha.7 || >=2.0.0 + react: ^0.14 || ^15.0.0-rc || >=15.0 + dependencies: + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-list: 0.8.17(react@18.2.0) + shallow-equal: 1.2.1 + dev: false + /react-docgen-typescript@2.2.2(typescript@5.1.6): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -11313,6 +11344,15 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + /react-list@0.8.17(react@18.2.0): + resolution: {integrity: sha512-pgmzGi0G5uGrdHzMhgO7KR1wx5ZXVvI3SsJUmkblSAKtewIhMwbQiMuQiTE83ozo04BQJbe0r3WIWzSO0dR1xg==} + peerDependencies: + react: 0.14 || 15 - 18 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /react-markdown@8.0.3(@types/react@18.2.6)(react@18.2.0): resolution: {integrity: sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==} peerDependencies: @@ -12001,6 +12041,10 @@ packages: kind-of: 6.0.3 dev: true + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + /shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} dev: false diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx new file mode 100644 index 0000000000000..60774242dad33 --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx @@ -0,0 +1,234 @@ +import Box from "@mui/material/Box" +import { styled } from "@mui/material/styles" +import { ComponentProps, useRef, useState } from "react" +import "react-date-range/dist/styles.css" +import "react-date-range/dist/theme/default.css" +import Button from "@mui/material/Button" +import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined" +import Popover from "@mui/material/Popover" +import { DateRangePicker, createStaticRanges } from "react-date-range" +import { format, subDays } from "date-fns" + +// The type definition from @types is wrong +declare module "react-date-range" { + export function createStaticRanges( + ranges: Omit[], + ): StaticRange[] +} + +export type DateRangeValue = { + startDate: Date + endDate: Date +} + +type RangesState = NonNullable["ranges"]> + +export const DateRange = ({ + value, + onChange, +}: { + value: DateRangeValue + onChange: (value: DateRangeValue) => void +}) => { + const selectionStatusRef = useRef<"idle" | "selecting">("idle") + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const [ranges, setRanges] = useState([ + { + ...value, + key: "selection", + }, + ]) + const currentRange = { + startDate: ranges[0].startDate as Date, + endDate: ranges[0].endDate as Date, + } + const handleClose = () => { + onChange({ + startDate: currentRange.startDate, + endDate: currentRange.endDate, + }) + setIsOpen(false) + } + + return ( + <> + + + { + const range = item.selection + setRanges([range]) + + // When it is the first selection, we don't want to close the popover + // We have to do that ourselves because the library doesn't provide a way to do it + if (selectionStatusRef.current === "idle") { + selectionStatusRef.current = "selecting" + return + } + + selectionStatusRef.current = "idle" + const startDate = range.startDate as Date + const endDate = range.endDate as Date + onChange({ + startDate, + endDate, + }) + setIsOpen(false) + }} + moveRangeOnFirstSelection={false} + months={2} + ranges={ranges} + maxDate={new Date()} + direction="horizontal" + staticRanges={createStaticRanges([ + { + label: "Today", + range: () => ({ + startDate: new Date(), + endDate: new Date(), + }), + }, + { + label: "Yesterday", + range: () => ({ + startDate: subDays(new Date(), 1), + endDate: subDays(new Date(), 1), + }), + }, + { + label: "Last 7 days", + range: () => ({ + startDate: subDays(new Date(), 6), + endDate: new Date(), + }), + }, + { + label: "Last 14 days", + range: () => ({ + startDate: subDays(new Date(), 13), + endDate: new Date(), + }), + }, + { + label: "Last 30 days", + range: () => ({ + startDate: subDays(new Date(), 29), + endDate: new Date(), + }), + }, + ])} + /> + + + ) +} + +const DateRangePickerWrapper: typeof Box = styled(Box)(({ theme }) => ({ + "& .rdrDefinedRangesWrapper": { + background: theme.palette.background.paper, + borderColor: theme.palette.divider, + }, + + "& .rdrStaticRange": { + background: theme.palette.background.paper, + border: 0, + fontSize: 14, + color: theme.palette.text.secondary, + + "&:hover .rdrStaticRangeLabel": { + background: theme.palette.background.paperLight, + color: theme.palette.text.primary, + }, + + "&.rdrStaticRangeSelected": { + color: `${theme.palette.text.primary} !important`, + }, + }, + + "& .rdrInputRanges": { + display: "none", + }, + + "& .rdrDateDisplayWrapper": { + backgroundColor: theme.palette.background.paper, + }, + + "& .rdrCalendarWrapper": { + backgroundColor: theme.palette.background.paperLight, + }, + + "& .rdrDateDisplayItem": { + background: "transparent", + borderColor: theme.palette.divider, + + "& input": { + color: theme.palette.text.secondary, + }, + + "&.rdrDateDisplayItemActive": { + borderColor: theme.palette.text.primary, + backgroundColor: theme.palette.background.paperLight, + + "& input": { + color: theme.palette.text.primary, + }, + }, + }, + + "& .rdrMonthPicker select, & .rdrYearPicker select": { + color: theme.palette.text.primary, + appearance: "auto", + background: "transparent", + }, + + "& .rdrMonthName, & .rdrWeekDay": { + color: theme.palette.text.secondary, + }, + + "& .rdrDayPassive .rdrDayNumber span": { + color: theme.palette.text.disabled, + }, + + "& .rdrDayNumber span": { + color: theme.palette.text.primary, + }, + + "& .rdrDayToday .rdrDayNumber span": { + fontWeight: 900, + + "&:after": { + display: "none", + }, + }, + + "& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": { + color: theme.palette.primary.main, + }, + + "& .rdrDayDisabled": { + backgroundColor: "transparent", + + "& .rdrDayNumber span": { + color: theme.palette.text.disabled, + }, + }, +})) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 77bf66a0759e9..19de025f0648c 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -25,28 +25,40 @@ import { TemplateParameterValue, UserLatencyInsightsResponse, } from "api/typesGenerated" -import { ComponentProps } from "react" -import { subDays, addHours, startOfHour } from "date-fns" +import { ComponentProps, ReactNode, useState } from "react" +import { subDays, isToday } from "date-fns" +import "react-date-range/dist/styles.css" +import "react-date-range/dist/theme/default.css" +import { DateRange, DateRangeValue } from "./DateRange" import { useDashboard } from "components/Dashboard/DashboardProvider" import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined" import Link from "@mui/material/Link" import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" import CancelOutlined from "@mui/icons-material/CancelOutlined" +import { getDateRangeFilter } from "./utils" export default function TemplateInsightsPage() { const now = new Date() + const [dateRangeValue, setDateRangeValue] = useState({ + startDate: subDays(now, 6), + endDate: now, + }) const { template } = useTemplateLayoutContext() const insightsFilter = { template_ids: template.id, - start_time: toStartTimeFilter(subDays(now, 7)), - end_time: startOfHour(addHours(now, 1)).toISOString(), + ...getDateRangeFilter({ + startDate: dateRangeValue.startDate, + endDate: dateRangeValue.endDate, + now, + isToday, + }), } const { data: templateInsights } = useQuery({ - queryKey: ["templates", template.id, "usage"], + queryKey: ["templates", template.id, "usage", insightsFilter], queryFn: () => getInsightsTemplate(insightsFilter), }) const { data: userLatency } = useQuery({ - queryKey: ["templates", template.id, "user-latency"], + queryKey: ["templates", template.id, "user-latency", insightsFilter], queryFn: () => getInsightsUserLatency(insightsFilter), }) const dashboard = useDashboard() @@ -60,6 +72,9 @@ export default function TemplateInsightsPage() { {getTemplatePageTitle("Insights", template)} + } templateInsights={templateInsights} userLatency={userLatency} shouldDisplayParameters={shouldDisplayParameters} @@ -72,36 +87,41 @@ export const TemplateInsightsPageView = ({ templateInsights, userLatency, shouldDisplayParameters, + dateRange, }: { templateInsights: TemplateInsightsResponse | undefined userLatency: UserLatencyInsightsResponse | undefined shouldDisplayParameters: boolean + dateRange: ReactNode }) => { return ( - theme.spacing(3), - }} - > - - - - {shouldDisplayParameters && ( - + {dateRange} + theme.spacing(3), + }} + > + + + - )} - + {shouldDisplayParameters && ( + + )} + + ) } @@ -551,20 +571,12 @@ function mapToDAUsResponse( entries: data.map((d) => { return { amount: d.active_users, - date: d.end_time, + date: d.start_time, } }), } } -function toStartTimeFilter(date: Date) { - date.setHours(0, 0, 0, 0) - const year = date.getUTCFullYear() - const month = String(date.getUTCMonth() + 1).padStart(2, "0") - const day = String(date.getUTCDate()).padStart(2, "0") - return `${year}-${month}-${day}T00:00:00Z` -} - function formatTime(seconds: number): string { if (seconds < 60) { return seconds + " seconds" diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts new file mode 100644 index 0000000000000..e029f69fb9035 --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts @@ -0,0 +1,36 @@ +import { getDateRangeFilter } from "./utils" + +describe("getDateRangeFilter", () => { + it("returns the start time at the start of the day", () => { + const date = new Date("2020-01-01T12:00:00.000Z") + const { start_time } = getDateRangeFilter({ + startDate: date, + endDate: date, + now: date, + isToday: () => false, + }) + expect(start_time).toEqual("2020-01-01T00:00:00+00:00") + }) + + it("returns the end time at the start of the next day", () => { + const date = new Date("2020-01-01T12:00:00.000Z") + const { end_time } = getDateRangeFilter({ + startDate: date, + endDate: date, + now: date, + isToday: () => false, + }) + expect(end_time).toEqual("2020-01-02T00:00:00+00:00") + }) + + it("returns the end time at the start of the next hour if the end date is today", () => { + const date = new Date("2020-01-01T12:00:00.000Z") + const { end_time } = getDateRangeFilter({ + startDate: date, + endDate: date, + now: date, + isToday: () => true, + }) + expect(end_time).toEqual("2020-01-01T13:00:00+00:00") + }) +}) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts new file mode 100644 index 0000000000000..bfc751efe4c01 --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts @@ -0,0 +1,26 @@ +import { addDays, addHours, format, startOfDay, startOfHour } from "date-fns" + +export function getDateRangeFilter({ + startDate, + endDate, + now, + isToday, +}: { + startDate: Date + endDate: Date + now: Date + isToday: (date: Date) => boolean +}) { + return { + start_time: toISOLocal(startOfDay(startDate)), + end_time: toISOLocal( + isToday(endDate) + ? startOfHour(addHours(now, 1)) + : startOfDay(addDays(endDate, 1)), + ), + } +} + +function toISOLocal(d: Date) { + return format(d, "yyyy-MM-dd'T'HH:mm:ssxxx") +} From a5c59b9934e9c4b9b1a8cb212698a638d1ea1c0b Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 8 Aug 2023 12:21:38 -0500 Subject: [PATCH 047/156] chore: upgrade to alpine `3.18.3` (#8980) --- scripts/Dockerfile.base | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base index 738b66f01090e..d5e8283d0399b 100644 --- a/scripts/Dockerfile.base +++ b/scripts/Dockerfile.base @@ -1,7 +1,7 @@ # This is the base image used for Coder images. It's a multi-arch image that is # built in depot.dev for all supported architectures. Since it's built on real # hardware and not cross-compiled, it can have "RUN" commands. -FROM alpine:3.18.2 +FROM alpine:3.18.3 # We use a single RUN command to reduce the number of layers in the image. # NOTE: Keep the Terraform version in sync with minTerraformVersion and @@ -12,8 +12,6 @@ RUN apk add --no-cache \ bash \ jq \ git \ - # Fixes CVE-2023-3446 and CVE-2023-2975. Only necessary until Alpine 3.18.3. - openssl \ openssh-client && \ # Use the edge repo, since Terraform doesn't seem to be backported to 3.18. apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ From 3c52b018508b40c5d2339148ab838519c6769403 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Aug 2023 10:56:08 -0700 Subject: [PATCH 048/156] chore: add tailscale magicsock debug logging controls (#8982) --- agent/agent.go | 48 +++++++++++++++++++---- agent/agent_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++- tailnet/conn.go | 25 ++++++++++++ 3 files changed, 157 insertions(+), 8 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 52c423787fb44..e435b795d5fc9 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -22,6 +22,7 @@ import ( "time" "github.com/armon/circbuf" + "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/afero" @@ -1408,24 +1409,57 @@ func (a *agent) isClosed() bool { } func (a *agent) HTTPDebug() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r := chi.NewRouter() + + requireNetwork := func(w http.ResponseWriter) (*tailnet.Conn, bool) { a.closeMutex.Lock() network := a.network a.closeMutex.Unlock() if network == nil { - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte("network is not ready yet")) + return nil, false + } + + return network, true + } + + r.Get("/debug/magicsock", func(w http.ResponseWriter, r *http.Request) { + network, ok := requireNetwork(w) + if !ok { return } + network.MagicsockServeHTTPDebug(w, r) + }) - if r.URL.Path == "/debug/magicsock" { - network.MagicsockServeHTTPDebug(w, r) - } else { - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte("404 not found")) + r.Get("/debug/magicsock/debug-logging/{state}", func(w http.ResponseWriter, r *http.Request) { + state := chi.URLParam(r, "state") + stateBool, err := strconv.ParseBool(state) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = fmt.Fprintf(w, "invalid state %q, must be a boolean", state) + return + } + + network, ok := requireNetwork(w) + if !ok { + return } + + network.MagicsockSetDebugLoggingEnabled(stateBool) + a.logger.Info(r.Context(), "updated magicsock debug logging due to debug request", slog.F("new_state", stateBool)) + + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool) }) + + r.NotFound(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("404 not found")) + }) + + return r } func (a *agent) Close() error { diff --git a/agent/agent_test.go b/agent/agent_test.go index 34637992536b7..92be40764f209 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1932,6 +1932,96 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { }, testutil.WaitShort, testutil.IntervalFast) } +func TestAgent_DebugServer(t *testing.T) { + t.Parallel() + + derpMap, _ := tailnettest.RunDERPAndSTUN(t) + //nolint:dogsled + conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{ + DERPMap: derpMap, + }, 0) + + awaitReachableCtx := testutil.Context(t, testutil.WaitLong) + ok := conn.AwaitReachable(awaitReachableCtx) + require.True(t, ok) + _ = conn.Close() + + srv := httptest.NewServer(agnt.HTTPDebug()) + t.Cleanup(srv.Close) + + t.Run("MagicsockDebug", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, string(resBody), "

magicsock

") + }) + + t.Run("MagicsockDebugLogging", func(t *testing.T) { + t.Parallel() + + t.Run("Enable", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/t", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, string(resBody), "updated magicsock debug logging to true") + }) + + t.Run("Disable", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/0", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, string(resBody), "updated magicsock debug logging to false") + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/blah", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`) + }) + }) +} + func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) (*ptytest.PTYCmd, pty.Process) { //nolint:dogsled agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) @@ -2013,7 +2103,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati *agenttest.Client, <-chan *agentsdk.Stats, afero.Fs, - io.Closer, + agent.Agent, ) { logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) if metadata.DERPMap == nil { diff --git a/tailnet/conn.go b/tailnet/conn.go index 089a83ff79ff9..ebe57d2606b1c 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/netip" + "os" "reflect" "strconv" "sync" @@ -51,6 +52,14 @@ const ( WorkspaceAgentSpeedtestPort = 3 ) +// EnvMagicsockDebugLogging enables super-verbose logging for the magicsock +// internals. A logger must be supplied to the connection with the debug level +// enabled. +// +// With this disabled, you still get a lot of output if you have a valid logger +// with the debug level enabled. +const EnvMagicsockDebugLogging = "CODER_MAGICSOCK_DEBUG_LOGGING" + func init() { // Globally disable network namespacing. All networking happens in // userspace. @@ -175,6 +184,18 @@ func NewConn(options *Options) (conn *Conn, err error) { magicConn.SetDERPHeader(options.DERPHeader.Clone()) } + if v, ok := os.LookupEnv(EnvMagicsockDebugLogging); ok { + vBool, err := strconv.ParseBool(v) + if err != nil { + options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging disabled due to invalid value %s=%q, use true or false", EnvMagicsockDebugLogging, v)) + } else { + magicConn.SetDebugLoggingEnabled(vBool) + options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging set by %s=%t", EnvMagicsockDebugLogging, vBool)) + } + } else { + options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging disabled, use %s=true to enable", EnvMagicsockDebugLogging)) + } + // Update the keys for the magic connection! err = magicConn.SetPrivateKey(nodePrivateKey) if err != nil { @@ -361,6 +382,10 @@ type Conn struct { trafficStats *connstats.Statistics } +func (c *Conn) MagicsockSetDebugLoggingEnabled(enabled bool) { + c.magicConn.SetDebugLoggingEnabled(enabled) +} + func (c *Conn) SetAddresses(ips []netip.Prefix) error { c.mutex.Lock() defer c.mutex.Unlock() From f7a35e0559622f4318dde2c0cea674d5d108d8fc Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Aug 2023 11:29:35 -0700 Subject: [PATCH 049/156] chore: add workspace proxies to telemetry (#8963) --- coderd/telemetry/telemetry.go | 38 ++++++++++++++++++++++++++++- coderd/telemetry/telemetry_test.go | 3 +++ enterprise/coderd/workspaceproxy.go | 5 ++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index ced52a58fd273..834118a0d8a98 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -22,7 +22,6 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/coder/coder/buildinfo" "github.com/coder/coder/coderd/database" ) @@ -460,6 +459,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + proxies, err := r.options.Database.GetWorkspaceProxies(ctx) + if err != nil { + return xerrors.Errorf("get workspace proxies: %w", err) + } + snapshot.WorkspaceProxies = make([]WorkspaceProxy, 0, len(proxies)) + for _, proxy := range proxies { + snapshot.WorkspaceProxies = append(snapshot.WorkspaceProxies, ConvertWorkspaceProxy(proxy)) + } + return nil + }) err := eg.Wait() if err != nil { @@ -665,6 +675,19 @@ func ConvertLicense(license database.License) License { } } +// ConvertWorkspaceProxy anonymizes a workspace proxy. +func ConvertWorkspaceProxy(proxy database.WorkspaceProxy) WorkspaceProxy { + return WorkspaceProxy{ + ID: proxy.ID, + Name: proxy.Name, + DisplayName: proxy.DisplayName, + DerpEnabled: proxy.DerpEnabled, + DerpOnly: proxy.DerpOnly, + CreatedAt: proxy.CreatedAt, + UpdatedAt: proxy.UpdatedAt, + } +} + // Snapshot represents a point-in-time anonymized database dump. // Data is aggregated by latest on the server-side, so partial data // can be sent without issue. @@ -684,6 +707,7 @@ type Snapshot struct { WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"` WorkspaceResources []WorkspaceResource `json:"workspace_resources"` WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"` + WorkspaceProxies []WorkspaceProxy `json:"workspace_proxies"` CLIInvocations []CLIInvocation `json:"cli_invocations"` } @@ -872,6 +896,18 @@ type CLIInvocation struct { InvokedAt time.Time `json:"invoked_at"` } +type WorkspaceProxy struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + // No URLs since we don't send deployment URL. + DerpEnabled bool `json:"derp_enabled"` + DerpOnly bool `json:"derp_only"` + // No Status since it may contain sensitive information. + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type noopReporter struct{} func (*noopReporter) Report(_ *Snapshot) {} diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 93e1a5295475b..28569f1ca98e4 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -81,6 +81,8 @@ func TestTelemetry(t *testing.T) { UUID: uuid.New(), }) assert.NoError(t, err) + _, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{}) + _, snapshot := collectSnapshot(t, db) require.Len(t, snapshot.ProvisionerJobs, 1) require.Len(t, snapshot.Licenses, 1) @@ -93,6 +95,7 @@ func TestTelemetry(t *testing.T) { require.Len(t, snapshot.WorkspaceBuilds, 1) require.Len(t, snapshot.WorkspaceResources, 1) require.Len(t, snapshot.WorkspaceAgentStats, 1) + require.Len(t, snapshot.WorkspaceProxies, 1) wsa := snapshot.WorkspaceAgents[0] require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystem) diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 591f2fa33374f..54279953920de 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/telemetry" "github.com/coder/coder/coderd/workspaceapps" "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" @@ -369,6 +370,10 @@ func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) { return } + api.Telemetry.Report(&telemetry.Snapshot{ + WorkspaceProxies: []telemetry.WorkspaceProxy{telemetry.ConvertWorkspaceProxy(proxy)}, + }) + aReq.New = proxy httpapi.Write(ctx, rw, http.StatusCreated, codersdk.UpdateWorkspaceProxyResponse{ Proxy: convertProxy(proxy, proxyhealth.ProxyStatus{ From d4e115d267a1a27f7a28b3820e0ca8d17c37986e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Aug 2023 15:33:08 -0500 Subject: [PATCH 050/156] chore: show basic experiment set value (#8984) This value is pre-parsed, meaning the experiments listed may not be valid. This is a very basic display for helping debuging purposes. --- .../GeneralSettingsPage/GeneralSettingsPageView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index 8e9df03477f0f..f798bcbcdd734 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -43,6 +43,7 @@ export const GeneralSettingsPageView = ({ deploymentOptions, "Access URL", "Wildcard Access URL", + "Experiments", )} /> From 70bd23a40a07cc7bb2342e083c90cfc90c4b58ca Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 8 Aug 2023 18:48:53 -0300 Subject: [PATCH 051/156] refactor(site): add default title (#8985) --- site/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/site/index.html b/site/index.html index 92801202a5d88..fb62af8b53a2d 100644 --- a/site/index.html +++ b/site/index.html @@ -10,6 +10,7 @@ + Coder From 07fd73c4a0f90406c843b69a6a806910fbc56212 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Aug 2023 22:10:28 -0700 Subject: [PATCH 052/156] chore: allow multiple agent subsystems, add exectrace (#8933) --- agent/agent.go | 8 +-- cli/agent.go | 17 +++++- cli/agent_test.go | 10 ++-- coderd/apidoc/docs.go | 22 +++++--- coderd/apidoc/swagger.json | 22 +++++--- coderd/database/dbauthz/dbauthz_test.go | 6 ++- coderd/database/dbfake/dbfake.go | 19 ++++++- coderd/database/dump.sql | 8 +-- .../000148_agent_multiple_subsystems.down.sql | 17 ++++++ .../000148_agent_multiple_subsystems.up.sql | 21 ++++++++ coderd/database/models.go | 11 ++-- coderd/database/queries.sql.go | 40 +++++++------- coderd/database/queries/workspaceagents.sql | 2 +- coderd/telemetry/telemetry.go | 9 +++- coderd/telemetry/telemetry_test.go | 13 +++-- coderd/workspaceagents.go | 54 ++++++++++++++++--- coderd/workspaceagents_test.go | 20 ++++--- codersdk/agentsdk/agentsdk.go | 6 +-- codersdk/workspaceagents.go | 15 +++++- docs/api/agents.md | 2 +- docs/api/builds.md | 18 +++---- docs/api/schemas.md | 32 +++++------ docs/api/templates.md | 10 ++-- docs/api/workspaces.md | 10 ++-- site/src/api/typesGenerated.ts | 10 ++-- site/src/testHelpers/entities.ts | 2 +- 26 files changed, 285 insertions(+), 119 deletions(-) create mode 100644 coderd/database/migrations/000148_agent_multiple_subsystems.down.sql create mode 100644 coderd/database/migrations/000148_agent_multiple_subsystems.up.sql diff --git a/agent/agent.go b/agent/agent.go index e435b795d5fc9..3d9b8947b18b1 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -64,7 +64,7 @@ type Options struct { IgnorePorts map[int]string SSHMaxTimeout time.Duration TailnetListenPort uint16 - Subsystem codersdk.AgentSubsystem + Subsystems []codersdk.AgentSubsystem Addresses []netip.Prefix PrometheusRegistry *prometheus.Registry ReportMetadataInterval time.Duration @@ -145,7 +145,7 @@ func New(options Options) Agent { reportMetadataInterval: options.ReportMetadataInterval, serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval, sshMaxTimeout: options.SSHMaxTimeout, - subsystem: options.Subsystem, + subsystems: options.Subsystems, addresses: options.Addresses, prometheusRegistry: prometheusRegistry, @@ -167,7 +167,7 @@ type agent struct { // listing all listening ports. This is helpful to hide ports that // are used by the agent, that the user does not care about. ignorePorts map[int]string - subsystem codersdk.AgentSubsystem + subsystems []codersdk.AgentSubsystem reconnectingPTYs sync.Map reconnectingPTYTimeout time.Duration @@ -609,7 +609,7 @@ func (a *agent) run(ctx context.Context) error { err = a.client.PostStartup(ctx, agentsdk.PostStartupRequest{ Version: buildinfo.Version(), ExpandedDirectory: manifest.Directory, - Subsystem: a.subsystem, + Subsystems: a.subsystems, }) if err != nil { return xerrors.Errorf("update workspace agent version: %w", err) diff --git a/cli/agent.go b/cli/agent.go index 1d9a2ba02d51c..a217c9b0acc67 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -12,6 +12,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "sync" "time" @@ -253,7 +254,19 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { } prometheusRegistry := prometheus.NewRegistry() - subsystem := inv.Environ.Get(agent.EnvAgentSubsystem) + subsystemsRaw := inv.Environ.Get(agent.EnvAgentSubsystem) + subsystems := []codersdk.AgentSubsystem{} + for _, s := range strings.Split(subsystemsRaw, ",") { + subsystem := codersdk.AgentSubsystem(strings.TrimSpace(s)) + if subsystem == "" { + continue + } + if !subsystem.Valid() { + return xerrors.Errorf("invalid subsystem %q", subsystem) + } + subsystems = append(subsystems, subsystem) + } + agnt := agent.New(agent.Options{ Client: client, Logger: logger, @@ -275,7 +288,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { }, IgnorePorts: ignorePorts, SSHMaxTimeout: sshMaxTimeout, - Subsystem: codersdk.AgentSubsystem(subsystem), + Subsystems: subsystems, PrometheusRegistry: prometheusRegistry, }) diff --git a/cli/agent_test.go b/cli/agent_test.go index 462ef3c204541..34f04b70705b2 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -2,6 +2,7 @@ package cli_test import ( "context" + "fmt" "os" "path/filepath" "runtime" @@ -264,8 +265,8 @@ func TestWorkspaceAgent(t *testing.T) { "--agent-url", client.URL.String(), "--log-dir", logDir, ) - // Set the subsystem for the agent. - inv.Environ.Set(agent.EnvAgentSubsystem, string(codersdk.AgentSubsystemEnvbox)) + // Set the subsystems for the agent. + inv.Environ.Set(agent.EnvAgentSubsystem, fmt.Sprintf("%s,%s", codersdk.AgentSubsystemExectrace, codersdk.AgentSubsystemEnvbox)) pty := ptytest.New(t).Attach(inv) @@ -275,6 +276,9 @@ func TestWorkspaceAgent(t *testing.T) { resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) require.Len(t, resources, 1) require.Len(t, resources[0].Agents, 1) - require.Equal(t, codersdk.AgentSubsystemEnvbox, resources[0].Agents[0].Subsystem) + require.Len(t, resources[0].Agents[0].Subsystems, 2) + // Sorted + require.Equal(t, codersdk.AgentSubsystemEnvbox, resources[0].Agents[0].Subsystems[0]) + require.Equal(t, codersdk.AgentSubsystemExectrace, resources[0].Agents[0].Subsystems[1]) }) } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f930bf74f48fc..e83cf37d3206c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6447,8 +6447,11 @@ const docTemplate = `{ "expanded_directory": { "type": "string" }, - "subsystem": { - "$ref": "#/definitions/codersdk.AgentSubsystem" + "subsystems": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentSubsystem" + } }, "version": { "type": "string" @@ -6903,10 +6906,14 @@ const docTemplate = `{ "codersdk.AgentSubsystem": { "type": "string", "enum": [ - "envbox" + "envbox", + "envbuilder", + "exectrace" ], "x-enum-varnames": [ - "AgentSubsystemEnvbox" + "AgentSubsystemEnvbox", + "AgentSubsystemEnvbuilder", + "AgentSubsystemExectrace" ] }, "codersdk.AppHostResponse": { @@ -10556,8 +10563,11 @@ const docTemplate = `{ "status": { "$ref": "#/definitions/codersdk.WorkspaceAgentStatus" }, - "subsystem": { - "$ref": "#/definitions/codersdk.AgentSubsystem" + "subsystems": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentSubsystem" + } }, "troubleshooting_url": { "type": "string" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0d691b237b655..0206c57062720 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5697,8 +5697,11 @@ "expanded_directory": { "type": "string" }, - "subsystem": { - "$ref": "#/definitions/codersdk.AgentSubsystem" + "subsystems": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentSubsystem" + } }, "version": { "type": "string" @@ -6130,8 +6133,12 @@ }, "codersdk.AgentSubsystem": { "type": "string", - "enum": ["envbox"], - "x-enum-varnames": ["AgentSubsystemEnvbox"] + "enum": ["envbox", "envbuilder", "exectrace"], + "x-enum-varnames": [ + "AgentSubsystemEnvbox", + "AgentSubsystemEnvbuilder", + "AgentSubsystemExectrace" + ] }, "codersdk.AppHostResponse": { "type": "object", @@ -9571,8 +9578,11 @@ "status": { "$ref": "#/definitions/codersdk.WorkspaceAgentStatus" }, - "subsystem": { - "$ref": "#/definitions/codersdk.AgentSubsystem" + "subsystems": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentSubsystem" + } }, "troubleshooting_url": { "type": "string" diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f3313c768053f..d6ad41f51408a 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1064,8 +1064,10 @@ func (s *MethodTestSuite) TestWorkspace() { res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentStartupByIDParams{ - ID: agt.ID, - Subsystem: database.WorkspaceAgentSubsystemNone, + ID: agt.ID, + Subsystems: []database.WorkspaceAgentSubsystem{ + database.WorkspaceAgentSubsystemEnvbox, + }, }).Asserts(ws, rbac.ActionUpdate).Returns() })) s.Run("GetWorkspaceAgentLogsAfter", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index c7ce32c7945b0..156fe957d69a8 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5203,6 +5203,23 @@ func (q *FakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg dat return err } + if len(arg.Subsystems) > 0 { + seen := map[database.WorkspaceAgentSubsystem]struct{}{ + arg.Subsystems[0]: {}, + } + for i := 1; i < len(arg.Subsystems); i++ { + s := arg.Subsystems[i] + if _, ok := seen[s]; ok { + return xerrors.Errorf("duplicate subsystem %q", s) + } + seen[s] = struct{}{} + + if arg.Subsystems[i-1] > arg.Subsystems[i] { + return xerrors.Errorf("subsystems not sorted: %q > %q", arg.Subsystems[i-1], arg.Subsystems[i]) + } + } + } + q.mutex.Lock() defer q.mutex.Unlock() @@ -5213,7 +5230,7 @@ func (q *FakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg dat agent.Version = arg.Version agent.ExpandedDirectory = arg.ExpandedDirectory - agent.Subsystem = arg.Subsystem + agent.Subsystems = arg.Subsystems q.workspaceAgents[index] = agent return nil } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9dfb9ded10e64..95a1e3534ae8b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -148,7 +148,8 @@ CREATE TYPE workspace_agent_log_source AS ENUM ( CREATE TYPE workspace_agent_subsystem AS ENUM ( 'envbuilder', 'envbox', - 'none' + 'none', + 'exectrace' ); CREATE TYPE workspace_app_health AS ENUM ( @@ -775,11 +776,12 @@ CREATE TABLE workspace_agents ( shutdown_script_timeout_seconds integer DEFAULT 0 NOT NULL, logs_length integer DEFAULT 0 NOT NULL, logs_overflowed boolean DEFAULT false NOT NULL, - subsystem workspace_agent_subsystem DEFAULT 'none'::workspace_agent_subsystem NOT NULL, startup_script_behavior startup_script_behavior DEFAULT 'non-blocking'::startup_script_behavior NOT NULL, started_at timestamp with time zone, ready_at timestamp with time zone, - CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)) + subsystems workspace_agent_subsystem[] DEFAULT '{}'::workspace_agent_subsystem[], + CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), + CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) ); COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.'; diff --git a/coderd/database/migrations/000148_agent_multiple_subsystems.down.sql b/coderd/database/migrations/000148_agent_multiple_subsystems.down.sql new file mode 100644 index 0000000000000..05bea6c620502 --- /dev/null +++ b/coderd/database/migrations/000148_agent_multiple_subsystems.down.sql @@ -0,0 +1,17 @@ +BEGIN; + +-- Bring back the subsystem column. +ALTER TABLE workspace_agents ADD COLUMN subsystem workspace_agent_subsystem NOT NULL DEFAULT 'none'; + +-- Update all existing workspace_agents to have subsystem = subsystems[0] unless +-- subsystems is empty. +UPDATE workspace_agents SET subsystem = subsystems[1] WHERE cardinality(subsystems) > 0; + +-- Drop the subsystems column from workspace_agents. +ALTER TABLE workspace_agents DROP COLUMN subsystems; + +-- We cannot drop the "exectrace" value from the workspace_agent_subsystem type +-- because you cannot drop values from an enum type. +UPDATE workspace_agents SET subsystem = 'none' WHERE subsystem = 'exectrace'; + +COMMIT; diff --git a/coderd/database/migrations/000148_agent_multiple_subsystems.up.sql b/coderd/database/migrations/000148_agent_multiple_subsystems.up.sql new file mode 100644 index 0000000000000..9ebb71d5bdf5e --- /dev/null +++ b/coderd/database/migrations/000148_agent_multiple_subsystems.up.sql @@ -0,0 +1,21 @@ +BEGIN; + +-- Add "exectrace" to workspace_agent_subsystem type. +ALTER TYPE workspace_agent_subsystem ADD VALUE 'exectrace'; + +-- Create column subsystems in workspace_agents table, with default value being +-- an empty array. +ALTER TABLE workspace_agents ADD COLUMN subsystems workspace_agent_subsystem[] DEFAULT '{}'; + +-- Add a constraint that the subsystems cannot contain the deprecated value +-- 'none'. +ALTER TABLE workspace_agents ADD CONSTRAINT subsystems_not_none CHECK (NOT ('none' = ANY (subsystems))); + +-- Update all existing workspace_agents to have subsystems = [subsystem] unless +-- the subsystem is 'none'. +UPDATE workspace_agents SET subsystems = ARRAY[subsystem] WHERE subsystem != 'none'; + +-- Drop the subsystem column from workspace_agents. +ALTER TABLE workspace_agents DROP COLUMN subsystem; + +COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index d3b7700b56bfa..0c1b4a7002227 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1307,6 +1307,7 @@ const ( WorkspaceAgentSubsystemEnvbuilder WorkspaceAgentSubsystem = "envbuilder" WorkspaceAgentSubsystemEnvbox WorkspaceAgentSubsystem = "envbox" WorkspaceAgentSubsystemNone WorkspaceAgentSubsystem = "none" + WorkspaceAgentSubsystemExectrace WorkspaceAgentSubsystem = "exectrace" ) func (e *WorkspaceAgentSubsystem) Scan(src interface{}) error { @@ -1348,7 +1349,8 @@ func (e WorkspaceAgentSubsystem) Valid() bool { switch e { case WorkspaceAgentSubsystemEnvbuilder, WorkspaceAgentSubsystemEnvbox, - WorkspaceAgentSubsystemNone: + WorkspaceAgentSubsystemNone, + WorkspaceAgentSubsystemExectrace: return true } return false @@ -1359,6 +1361,7 @@ func AllWorkspaceAgentSubsystemValues() []WorkspaceAgentSubsystem { WorkspaceAgentSubsystemEnvbuilder, WorkspaceAgentSubsystemEnvbox, WorkspaceAgentSubsystemNone, + WorkspaceAgentSubsystemExectrace, } } @@ -1944,14 +1947,14 @@ type WorkspaceAgent struct { // Total length of startup logs LogsLength int32 `db:"logs_length" json:"logs_length"` // Whether the startup logs overflowed in length - LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"` - Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"` + LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"` // When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible. StartupScriptBehavior StartupScriptBehavior `db:"startup_script_behavior" json:"startup_script_behavior"` // The time the agent entered the starting lifecycle state StartedAt sql.NullTime `db:"started_at" json:"started_at"` // The time the agent entered the ready or start_error lifecycle state - ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"` + ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"` + Subsystems []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"` } type WorkspaceAgentLog struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 95efcc4369baa..c3c9552266c99 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6239,7 +6239,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error { const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE @@ -6281,17 +6281,17 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ) return i, err } const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE @@ -6331,17 +6331,17 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ) return i, err } const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE @@ -6383,10 +6383,10 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ) return i, err } @@ -6506,7 +6506,7 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAge const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE @@ -6552,10 +6552,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ); err != nil { return nil, err } @@ -6571,7 +6571,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] } const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many -SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at FROM workspace_agents WHERE created_at > $1 +SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) { @@ -6613,10 +6613,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ); err != nil { return nil, err } @@ -6633,7 +6633,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many SELECT - workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.subsystem, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems FROM workspace_agents JOIN @@ -6691,10 +6691,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ); err != nil { return nil, err } @@ -6735,7 +6735,7 @@ INSERT INTO shutdown_script_timeout_seconds ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems ` type InsertWorkspaceAgentParams struct { @@ -6817,10 +6817,10 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa &i.ShutdownScriptTimeoutSeconds, &i.LogsLength, &i.LogsOverflowed, - &i.Subsystem, &i.StartupScriptBehavior, &i.StartedAt, &i.ReadyAt, + pq.Array(&i.Subsystems), ) return i, err } @@ -7040,16 +7040,16 @@ UPDATE SET version = $2, expanded_directory = $3, - subsystem = $4 + subsystems = $4 WHERE id = $1 ` type UpdateWorkspaceAgentStartupByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - Version string `db:"version" json:"version"` - ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"` - Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"` + ID uuid.UUID `db:"id" json:"id"` + Version string `db:"version" json:"version"` + ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"` + Subsystems []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"` } func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error { @@ -7057,7 +7057,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up arg.ID, arg.Version, arg.ExpandedDirectory, - arg.Subsystem, + pq.Array(arg.Subsystems), ) return err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 4025ac7e59a1b..dcc15081615e2 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -83,7 +83,7 @@ UPDATE SET version = $2, expanded_directory = $3, - subsystem = $4 + subsystems = $4 WHERE id = $1; diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 834118a0d8a98..615be6949da02 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -544,6 +544,11 @@ func ConvertProvisionerJob(job database.ProvisionerJob) ProvisionerJob { // ConvertWorkspaceAgent anonymizes a workspace agent. func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent { + subsystems := []string{} + for _, subsystem := range agent.Subsystems { + subsystems = append(subsystems, string(subsystem)) + } + snapAgent := WorkspaceAgent{ ID: agent.ID, CreatedAt: agent.CreatedAt, @@ -556,7 +561,7 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent { Directory: agent.Directory != "", ConnectionTimeoutSeconds: agent.ConnectionTimeoutSeconds, ShutdownScript: agent.ShutdownScript.Valid, - Subsystem: string(agent.Subsystem), + Subsystems: subsystems, } if agent.FirstConnectedAt.Valid { snapAgent.FirstConnectedAt = &agent.FirstConnectedAt.Time @@ -792,7 +797,7 @@ type WorkspaceAgent struct { DisconnectedAt *time.Time `json:"disconnected_at"` ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"` ShutdownScript bool `json:"shutdown_script"` - Subsystem string `json:"subsystem"` + Subsystems []string `json:"subsystems"` } type WorkspaceAgentStat struct { diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 28569f1ca98e4..5592bda5dbec8 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -54,15 +54,16 @@ func TestTelemetry(t *testing.T) { SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, }) - wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ - Subsystem: database.WorkspaceAgentSubsystemEnvbox, - }) + wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{}) // Update the workspace agent to have a valid subsystem. err = db.UpdateWorkspaceAgentStartupByID(ctx, database.UpdateWorkspaceAgentStartupByIDParams{ ID: wsagent.ID, Version: wsagent.Version, ExpandedDirectory: wsagent.ExpandedDirectory, - Subsystem: database.WorkspaceAgentSubsystemEnvbox, + Subsystems: []database.WorkspaceAgentSubsystem{ + database.WorkspaceAgentSubsystemEnvbox, + database.WorkspaceAgentSubsystemExectrace, + }, }) require.NoError(t, err) @@ -98,7 +99,9 @@ func TestTelemetry(t *testing.T) { require.Len(t, snapshot.WorkspaceProxies, 1) wsa := snapshot.WorkspaceAgents[0] - require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystem) + require.Len(t, wsa.Subsystems, 2) + require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0]) + require.Equal(t, string(database.WorkspaceAgentSubsystemExectrace), wsa.Subsystems[1]) }) t.Run("HashedEmail", func(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 545f3e7c6ed84..3fa5baf231997 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -14,6 +14,7 @@ import ( "net/netip" "net/url" "runtime/pprof" + "sort" "strconv" "strings" "sync" @@ -219,11 +220,31 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques return } + // Validate subsystems. + seen := make(map[codersdk.AgentSubsystem]bool) + for _, s := range req.Subsystems { + if !s.Valid() { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid workspace agent subsystem provided.", + Detail: fmt.Sprintf("invalid subsystem: %q", s), + }) + return + } + if seen[s] { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid workspace agent subsystem provided.", + Detail: fmt.Sprintf("duplicate subsystem: %q", s), + }) + return + } + seen[s] = true + } + if err := api.Database.UpdateWorkspaceAgentStartupByID(ctx, database.UpdateWorkspaceAgentStartupByIDParams{ ID: apiAgent.ID, Version: req.Version, ExpandedDirectory: req.ExpandedDirectory, - Subsystem: convertWorkspaceAgentSubsystem(req.Subsystem), + Subsystems: convertWorkspaceAgentSubsystems(req.Subsystems), }); err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Error setting agent version", @@ -1277,6 +1298,11 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin if dbAgent.TroubleshootingURL != "" { troubleshootingURL = dbAgent.TroubleshootingURL } + subsystems := make([]codersdk.AgentSubsystem, len(dbAgent.Subsystems)) + for i, subsystem := range dbAgent.Subsystems { + subsystems[i] = codersdk.AgentSubsystem(subsystem) + } + workspaceAgent := codersdk.WorkspaceAgent{ ID: dbAgent.ID, CreatedAt: dbAgent.CreatedAt, @@ -1302,7 +1328,7 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin LoginBeforeReady: dbAgent.StartupScriptBehavior != database.StartupScriptBehaviorBlocking, ShutdownScript: dbAgent.ShutdownScript.String, ShutdownScriptTimeoutSeconds: dbAgent.ShutdownScriptTimeoutSeconds, - Subsystem: codersdk.AgentSubsystem(dbAgent.Subsystem), + Subsystems: subsystems, } node := coordinator.Node(dbAgent.ID) if node != nil { @@ -2114,11 +2140,23 @@ func convertWorkspaceAgentLog(logEntry database.WorkspaceAgentLog) codersdk.Work } } -func convertWorkspaceAgentSubsystem(ss codersdk.AgentSubsystem) database.WorkspaceAgentSubsystem { - switch ss { - case codersdk.AgentSubsystemEnvbox: - return database.WorkspaceAgentSubsystemEnvbox - default: - return database.WorkspaceAgentSubsystemNone +func convertWorkspaceAgentSubsystems(ss []codersdk.AgentSubsystem) []database.WorkspaceAgentSubsystem { + out := make([]database.WorkspaceAgentSubsystem, 0, len(ss)) + for _, s := range ss { + switch s { + case codersdk.AgentSubsystemEnvbox: + out = append(out, database.WorkspaceAgentSubsystemEnvbox) + case codersdk.AgentSubsystemEnvbuilder: + out = append(out, database.WorkspaceAgentSubsystemEnvbuilder) + case codersdk.AgentSubsystemExectrace: + out = append(out, database.WorkspaceAgentSubsystemExectrace) + default: + // Invalid, drop it. + } } + + sort.Slice(out, func(i, j int) bool { + return out[i] < out[j] + }) + return out } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 12cb19efac012..b85dd9d3550d9 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1195,16 +1195,23 @@ func TestWorkspaceAgent_Startup(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) - const ( - expectedVersion = "v1.2.3" - expectedDir = "/home/coder" - expectedSubsystem = codersdk.AgentSubsystemEnvbox + var ( + expectedVersion = "v1.2.3" + expectedDir = "/home/coder" + expectedSubsystems = []codersdk.AgentSubsystem{ + codersdk.AgentSubsystemEnvbox, + codersdk.AgentSubsystemExectrace, + } ) err := agentClient.PostStartup(ctx, agentsdk.PostStartupRequest{ Version: expectedVersion, ExpandedDirectory: expectedDir, - Subsystem: expectedSubsystem, + Subsystems: []codersdk.AgentSubsystem{ + // Not sorted. + expectedSubsystems[1], + expectedSubsystems[0], + }, }) require.NoError(t, err) @@ -1215,7 +1222,8 @@ func TestWorkspaceAgent_Startup(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedVersion, wsagent.Version) require.Equal(t, expectedDir, wsagent.ExpandedDirectory) - require.Equal(t, expectedSubsystem, wsagent.Subsystem) + // Sorted + require.Equal(t, expectedSubsystems, wsagent.Subsystems) }) t.Run("InvalidSemver", func(t *testing.T) { diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index e2189e1cc53d2..d5e038bdf6b39 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -612,9 +612,9 @@ func (c *Client) PostLifecycle(ctx context.Context, req PostLifecycleRequest) er } type PostStartupRequest struct { - Version string `json:"version"` - ExpandedDirectory string `json:"expanded_directory"` - Subsystem codersdk.AgentSubsystem `json:"subsystem"` + Version string `json:"version"` + ExpandedDirectory string `json:"expanded_directory"` + Subsystems []codersdk.AgentSubsystem `json:"subsystems"` } func (c *Client) PostStartup(ctx context.Context, req PostStartupRequest) error { diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index e9aad8421e36a..71153a0e89b8a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -167,7 +167,7 @@ type WorkspaceAgent struct { LoginBeforeReady bool `json:"login_before_ready"` ShutdownScript string `json:"shutdown_script,omitempty"` ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"` - Subsystem AgentSubsystem `json:"subsystem"` + Subsystems []AgentSubsystem `json:"subsystems"` Health WorkspaceAgentHealth `json:"health"` // Health reports the health of the agent. } @@ -754,9 +754,20 @@ type WorkspaceAgentLog struct { type AgentSubsystem string const ( - AgentSubsystemEnvbox AgentSubsystem = "envbox" + AgentSubsystemEnvbox AgentSubsystem = "envbox" + AgentSubsystemEnvbuilder AgentSubsystem = "envbuilder" + AgentSubsystemExectrace AgentSubsystem = "exectrace" ) +func (s AgentSubsystem) Valid() bool { + switch s { + case AgentSubsystemEnvbox, AgentSubsystemEnvbuilder, AgentSubsystemExectrace: + return true + default: + return false + } +} + type WorkspaceAgentLogSource string const ( diff --git a/docs/api/agents.md b/docs/api/agents.md index 2316fdaafde51..8bf6f10619e50 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -695,7 +695,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" diff --git a/docs/api/builds.md b/docs/api/builds.md index daf7958de387f..782e41a6f61aa 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -120,7 +120,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -282,7 +282,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -583,7 +583,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -672,7 +672,7 @@ Status Code **200** | `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | | | `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | | `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | | +| `»» subsystems` | array | false | | | | `»» troubleshooting_url` | string | false | | | | `»» updated_at` | string(date-time) | false | | | | `»» version` | string | false | | | @@ -716,7 +716,6 @@ Status Code **200** | `status` | `connected` | | `status` | `disconnected` | | `status` | `timeout` | -| `subsystem` | `envbox` | | `workspace_transition` | `start` | | `workspace_transition` | `stop` | | `workspace_transition` | `delete` | @@ -841,7 +840,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -1008,7 +1007,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -1133,7 +1132,7 @@ Status Code **200** | `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | | | `»»» startup_script_timeout_seconds` | integer | false | | »»startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | | `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | | +| `»»» subsystems` | array | false | | | | `»»» troubleshooting_url` | string | false | | | | `»»» updated_at` | string(date-time) | false | | | | `»»» version` | string | false | | | @@ -1197,7 +1196,6 @@ Status Code **200** | `status` | `connected` | | `status` | `disconnected` | | `status` | `timeout` | -| `subsystem` | `envbox` | | `workspace_transition` | `start` | | `workspace_transition` | `stop` | | `workspace_transition` | `delete` | @@ -1356,7 +1354,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 7aed5d4f60022..048eff66adecb 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -377,18 +377,18 @@ ```json { "expanded_directory": "string", - "subsystem": "envbox", + "subsystems": ["envbox"], "version": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------------- | -------------------------------------------------- | -------- | ------------ | ----------- | -| `expanded_directory` | string | false | | | -| `subsystem` | [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | -| `version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------- | ----------------------------------------------------------- | -------- | ------------ | ----------- | +| `expanded_directory` | string | false | | | +| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | +| `version` | string | false | | | ## agentsdk.Stats @@ -913,9 +913,11 @@ _None_ #### Enumerated Values -| Value | -| -------- | -| `envbox` | +| Value | +| ------------ | +| `envbox` | +| `envbuilder` | +| `exectrace` | ## codersdk.AppHostResponse @@ -5402,7 +5404,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -5543,7 +5545,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -5585,7 +5587,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | | | `startup_script_timeout_seconds` | integer | false | | Startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | | `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | | -| `subsystem` | [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | +| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | | `troubleshooting_url` | string | false | | | | `updated_at` | string | false | | | | `version` | string | false | | | @@ -6001,7 +6003,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -6312,7 +6314,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -6524,7 +6526,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" diff --git a/docs/api/templates.md b/docs/api/templates.md index f8a1612161f81..14ce5dec7d29f 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1633,7 +1633,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -1722,7 +1722,7 @@ Status Code **200** | `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | | | `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | | `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | | +| `»» subsystems` | array | false | | | | `»» troubleshooting_url` | string | false | | | | `»» updated_at` | string(date-time) | false | | | | `»» version` | string | false | | | @@ -1766,7 +1766,6 @@ Status Code **200** | `status` | `connected` | | `status` | `disconnected` | | `status` | `timeout` | -| `subsystem` | `envbox` | | `workspace_transition` | `start` | | `workspace_transition` | `stop` | | `workspace_transition` | `delete` | @@ -2025,7 +2024,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -2114,7 +2113,7 @@ Status Code **200** | `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | | | `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. | | `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | | +| `»» subsystems` | array | false | | | | `»» troubleshooting_url` | string | false | | | | `»» updated_at` | string(date-time) | false | | | | `»» version` | string | false | | | @@ -2158,7 +2157,6 @@ Status Code **200** | `status` | `connected` | | `status` | `disconnected` | | `status` | `timeout` | -| `subsystem` | `envbox` | | `workspace_transition` | `start` | | `workspace_transition` | `stop` | | `workspace_transition` | `delete` | diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 6cfb468f6af50..f5c4aadd729c5 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -148,7 +148,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -336,7 +336,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -523,7 +523,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -712,7 +712,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" @@ -1034,7 +1034,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, "status": "connecting", - "subsystem": "envbox", + "subsystems": ["envbox"], "troubleshooting_url": "string", "updated_at": "2019-08-24T14:15:22Z", "version": "string" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3e5acc5671299..c1aa96d872994 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1340,7 +1340,7 @@ export interface WorkspaceAgent { readonly login_before_ready: boolean readonly shutdown_script?: string readonly shutdown_script_timeout_seconds: number - readonly subsystem: AgentSubsystem + readonly subsystems: AgentSubsystem[] readonly health: WorkspaceAgentHealth } @@ -1545,8 +1545,12 @@ export type APIKeyScope = "all" | "application_connect" export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] // From codersdk/workspaceagents.go -export type AgentSubsystem = "envbox" -export const AgentSubsystems: AgentSubsystem[] = ["envbox"] +export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" +export const AgentSubsystems: AgentSubsystem[] = [ + "envbox", + "envbuilder", + "exectrace", +] // From codersdk/audit.go export type AuditAction = diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 94eaa3add0211..d8ed5bbe20f58 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -563,7 +563,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { logs_overflowed: false, startup_script_timeout_seconds: 120, shutdown_script_timeout_seconds: 120, - subsystem: "envbox", + subsystems: ["envbox", "exectrace"], health: { healthy: true, }, From 00a8221e514da7ca7b17484c84ae3fe6289fb3eb Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Aug 2023 22:49:13 -0700 Subject: [PATCH 053/156] fix: rename duplicate migration (#8989) --- ...systems.down.sql => 000149_agent_multiple_subsystems.down.sql} | 0 ..._subsystems.up.sql => 000149_agent_multiple_subsystems.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000148_agent_multiple_subsystems.down.sql => 000149_agent_multiple_subsystems.down.sql} (100%) rename coderd/database/migrations/{000148_agent_multiple_subsystems.up.sql => 000149_agent_multiple_subsystems.up.sql} (100%) diff --git a/coderd/database/migrations/000148_agent_multiple_subsystems.down.sql b/coderd/database/migrations/000149_agent_multiple_subsystems.down.sql similarity index 100% rename from coderd/database/migrations/000148_agent_multiple_subsystems.down.sql rename to coderd/database/migrations/000149_agent_multiple_subsystems.down.sql diff --git a/coderd/database/migrations/000148_agent_multiple_subsystems.up.sql b/coderd/database/migrations/000149_agent_multiple_subsystems.up.sql similarity index 100% rename from coderd/database/migrations/000148_agent_multiple_subsystems.up.sql rename to coderd/database/migrations/000149_agent_multiple_subsystems.up.sql From 9941f4905679fdacf5aeb5a6cacdaae106bc465b Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 9 Aug 2023 02:31:25 -0700 Subject: [PATCH 054/156] fix: remove stun nodes from workspace proxy regions (#8990) --- enterprise/coderd/coderd.go | 39 ++++++++++++++++-------------- enterprise/wsproxy/wsproxy_test.go | 26 +++++++++++--------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 71d975e3ef1d6..1679165750b16 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -659,7 +659,7 @@ var ( lastDerpConflictLog time.Time ) -func derpMapper(logger slog.Logger, cfg *codersdk.DeploymentValues, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap { +func derpMapper(logger slog.Logger, _ *codersdk.DeploymentValues, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap { return func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap { derpMap = derpMap.Clone() @@ -754,25 +754,28 @@ func derpMapper(logger slog.Logger, cfg *codersdk.DeploymentValues, proxyHealth } var stunNodes []*tailcfg.DERPNode - if !cfg.DERP.Config.BlockDirect.Value() { - stunNodes, err = agpltailnet.STUNNodes(regionID, cfg.DERP.Server.STUNAddresses) - if err != nil { - // Log a warning if we haven't logged one in the last - // minute. - lastDerpConflictMutex.Lock() - shouldLog := lastDerpConflictLog.IsZero() || time.Since(lastDerpConflictLog) > time.Minute - if shouldLog { - lastDerpConflictLog = time.Now() + // TODO(@dean): potentially re-enable this depending on impact + /* + if !cfg.DERP.Config.BlockDirect.Value() { + stunNodes, err = agpltailnet.STUNNodes(regionID, cfg.DERP.Server.STUNAddresses) + if err != nil { + // Log a warning if we haven't logged one in the last + // minute. + lastDerpConflictMutex.Lock() + shouldLog := lastDerpConflictLog.IsZero() || time.Since(lastDerpConflictLog) > time.Minute + if shouldLog { + lastDerpConflictLog = time.Now() + } + lastDerpConflictMutex.Unlock() + if shouldLog { + logger.Error(context.Background(), "failed to calculate STUN nodes", slog.Error(err)) + } + + // No continue because we can keep going. + stunNodes = []*tailcfg.DERPNode{} } - lastDerpConflictMutex.Unlock() - if shouldLog { - logger.Error(context.Background(), "failed to calculate STUN nodes", slog.Error(err)) - } - - // No continue because we can keep going. - stunNodes = []*tailcfg.DERPNode{} } - } + */ nodes := append(stunNodes, &tailcfg.DERPNode{ Name: fmt.Sprintf("%da", regionID), diff --git a/enterprise/wsproxy/wsproxy_test.go b/enterprise/wsproxy/wsproxy_test.go index afcb3d1f16143..d9d4e1a96bc8d 100644 --- a/enterprise/wsproxy/wsproxy_test.go +++ b/enterprise/wsproxy/wsproxy_test.go @@ -244,24 +244,24 @@ resourceLoop: require.Equal(t, "coder_best-proxy", proxy1Region.RegionCode) require.Equal(t, 10001, proxy1Region.RegionID) require.False(t, proxy1Region.EmbeddedRelay) - require.Len(t, proxy1Region.Nodes, 2) // proxy + stun - require.Equal(t, "10001a", proxy1Region.Nodes[1].Name) - require.Equal(t, 10001, proxy1Region.Nodes[1].RegionID) - require.Equal(t, proxyAPI1.Options.AccessURL.Hostname(), proxy1Region.Nodes[1].HostName) - require.Equal(t, proxyAPI1.Options.AccessURL.Port(), fmt.Sprint(proxy1Region.Nodes[1].DERPPort)) - require.Equal(t, proxyAPI1.Options.AccessURL.Scheme == "http", proxy1Region.Nodes[1].ForceHTTP) + require.Len(t, proxy1Region.Nodes, 1) + require.Equal(t, "10001a", proxy1Region.Nodes[0].Name) + require.Equal(t, 10001, proxy1Region.Nodes[0].RegionID) + require.Equal(t, proxyAPI1.Options.AccessURL.Hostname(), proxy1Region.Nodes[0].HostName) + require.Equal(t, proxyAPI1.Options.AccessURL.Port(), fmt.Sprint(proxy1Region.Nodes[0].DERPPort)) + require.Equal(t, proxyAPI1.Options.AccessURL.Scheme == "http", proxy1Region.Nodes[0].ForceHTTP) // The second proxy region: require.Equal(t, "worst-proxy", proxy2Region.RegionName) require.Equal(t, "coder_worst-proxy", proxy2Region.RegionCode) require.Equal(t, 10002, proxy2Region.RegionID) require.False(t, proxy2Region.EmbeddedRelay) - require.Len(t, proxy2Region.Nodes, 2) // proxy + stun - require.Equal(t, "10002a", proxy2Region.Nodes[1].Name) - require.Equal(t, 10002, proxy2Region.Nodes[1].RegionID) - require.Equal(t, proxyAPI2.Options.AccessURL.Hostname(), proxy2Region.Nodes[1].HostName) - require.Equal(t, proxyAPI2.Options.AccessURL.Port(), fmt.Sprint(proxy2Region.Nodes[1].DERPPort)) - require.Equal(t, proxyAPI2.Options.AccessURL.Scheme == "http", proxy2Region.Nodes[1].ForceHTTP) + require.Len(t, proxy2Region.Nodes, 1) + require.Equal(t, "10002a", proxy2Region.Nodes[0].Name) + require.Equal(t, 10002, proxy2Region.Nodes[0].RegionID) + require.Equal(t, proxyAPI2.Options.AccessURL.Hostname(), proxy2Region.Nodes[0].HostName) + require.Equal(t, proxyAPI2.Options.AccessURL.Port(), fmt.Sprint(proxy2Region.Nodes[0].DERPPort)) + require.Equal(t, proxyAPI2.Options.AccessURL.Scheme == "http", proxy2Region.Nodes[0].ForceHTTP) }) t.Run("ConnectDERP", func(t *testing.T) { @@ -313,6 +313,8 @@ resourceLoop: func TestDERPMapStunNodes(t *testing.T) { t.Parallel() + // See: enterprise/coderd/coderd.go + t.Skip("STUN nodes are removed from proxy regions in the DERP map for now") deploymentValues := coderdtest.DeploymentValues(t) deploymentValues.Experiments = []string{ From 1730d354673803ece5981438c0f82471e05087d8 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 9 Aug 2023 03:05:46 -0700 Subject: [PATCH 055/156] Revert "fix: hide experiment CTA from OIDC copy (#8695)" (#8825) This reverts commit adbabe4e09b890fbf25691d3ce580b9389c34547. --- coderd/userauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/userauth.go b/coderd/userauth.go index 03ae7a6379672..a61b6d126d0e0 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -1673,7 +1673,7 @@ func clearOAuthConvertCookie() *http.Cookie { func wrongLoginTypeHTTPError(user database.LoginType, params database.LoginType) httpError { addedMsg := "" if user == database.LoginTypePassword { - addedMsg = " Try logging in with your password." + addedMsg = " You can convert your account to use this login type by visiting your account settings." } return httpError{ code: http.StatusForbidden, From 0d382d1e050cbadaed41526478b8f65e4b8cfc96 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Aug 2023 13:00:25 +0200 Subject: [PATCH 056/156] feat(cli): provide parameter values via command line (#8898) --- cli/create.go | 153 +++++----------- cli/create_test.go | 36 ++++ cli/parameter.go | 123 ++++++++----- cli/parameter_internal_test.go | 14 +- cli/parameterresolver.go | 224 +++++++++++++++++++++++ cli/restart.go | 27 ++- cli/restart_test.go | 54 ++++++ cli/start.go | 84 ++++----- cli/start_test.go | 40 ++++ cli/testdata/coder_create_--help.golden | 3 + cli/testdata/coder_restart_--help.golden | 3 + cli/testdata/coder_start_--help.golden | 3 + cli/testdata/coder_update_--help.golden | 6 + cli/update.go | 58 +++--- cli/update_test.go | 56 +++++- docs/cli/create.md | 9 + docs/cli/restart.md | 9 + docs/cli/start.md | 9 + docs/cli/update.md | 18 ++ 19 files changed, 682 insertions(+), 247 deletions(-) create mode 100644 cli/parameterresolver.go diff --git a/cli/create.go b/cli/create.go index 602b7b40a45bc..7a8c4ec417fac 100644 --- a/cli/create.go +++ b/cli/create.go @@ -18,11 +18,12 @@ import ( func (r *RootCmd) create() *clibase.Cmd { var ( - richParameterFile string - templateName string - startAt string - stopAfter time.Duration - workspaceName string + templateName string + startAt string + stopAfter time.Duration + workspaceName string + + parameterFlags workspaceParameterFlags ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -129,10 +130,18 @@ func (r *RootCmd) create() *clibase.Cmd { schedSpec = ptr.Ref(sched.String()) } - buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{ - Template: template, - RichParameterFile: richParameterFile, - NewWorkspaceName: workspaceName, + cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters) + if err != nil { + return xerrors.Errorf("can't parse given parameter values: %w", err) + } + + richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{ + Action: WorkspaceCreate, + Template: template, + NewWorkspaceName: workspaceName, + + RichParameterFile: parameterFlags.richParameterFile, + RichParameters: cliRichParameters, }) if err != nil { return xerrors.Errorf("prepare build: %w", err) @@ -156,7 +165,7 @@ func (r *RootCmd) create() *clibase.Cmd { Name: workspaceName, AutostartSchedule: schedSpec, TTLMillis: ttlMillis, - RichParameterValues: buildParams.richParameters, + RichParameterValues: richParameters, }) if err != nil { return xerrors.Errorf("create workspace: %w", err) @@ -179,12 +188,6 @@ func (r *RootCmd) create() *clibase.Cmd { Description: "Specify a template name.", Value: clibase.StringOf(&templateName), }, - clibase.Option{ - Flag: "rich-parameter-file", - Env: "CODER_RICH_PARAMETER_FILE", - Description: "Specify a file path with values for rich parameters defined in the template.", - Value: clibase.StringOf(&richParameterFile), - }, clibase.Option{ Flag: "start-at", Env: "CODER_WORKSPACE_START_AT", @@ -199,99 +202,59 @@ func (r *RootCmd) create() *clibase.Cmd { }, cliui.SkipPromptOption(), ) + cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...) return cmd } type prepWorkspaceBuildArgs struct { - Template codersdk.Template - ExistingRichParams []codersdk.WorkspaceBuildParameter - RichParameterFile string - NewWorkspaceName string - - UpdateWorkspace bool - BuildOptions bool - WorkspaceID uuid.UUID -} + Action WorkspaceCLIAction + Template codersdk.Template + NewWorkspaceName string + WorkspaceID uuid.UUID + + LastBuildParameters []codersdk.WorkspaceBuildParameter + + PromptBuildOptions bool + BuildOptions []codersdk.WorkspaceBuildParameter -type buildParameters struct { - // Rich parameters stores values for build parameters annotated with description, icon, type, etc. - richParameters []codersdk.WorkspaceBuildParameter + PromptRichParameters bool + RichParameters []codersdk.WorkspaceBuildParameter + RichParameterFile string } // prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version. -// Any missing params will be prompted to the user. It supports legacy and rich parameters. -func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) { +// Any missing params will be prompted to the user. It supports rich parameters. +func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) { ctx := inv.Context() templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID) if err != nil { - return nil, err + return nil, xerrors.Errorf("get template version: %w", err) } - // Rich parameters templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID) if err != nil { return nil, xerrors.Errorf("get template version rich parameters: %w", err) } - parameterMapFromFile := map[string]string{} - useParamFile := false + parameterFile := map[string]string{} if args.RichParameterFile != "" { - useParamFile = true - _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n") - parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile) - if err != nil { - return nil, err - } - } - disclaimerPrinted := false - richParameters := make([]codersdk.WorkspaceBuildParameter, 0) -PromptRichParamLoop: - for _, templateVersionParameter := range templateVersionParameters { - if !args.BuildOptions && templateVersionParameter.Ephemeral { - continue - } - - if !disclaimerPrinted { - _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n") - disclaimerPrinted = true - } - - // Param file is all or nothing - if !useParamFile && !templateVersionParameter.Ephemeral { - for _, e := range args.ExistingRichParams { - if e.Name == templateVersionParameter.Name { - // If the param already exists, we do not need to prompt it again. - // The workspace scope will reuse params for each build. - continue PromptRichParamLoop - } - } - } - - if args.UpdateWorkspace && !templateVersionParameter.Mutable { - // Check if the immutable parameter was used in the previous build. If so, then it isn't a fresh one - // and the user should be warned. - exists, err := workspaceBuildParameterExists(ctx, client, args.WorkspaceID, templateVersionParameter) - if err != nil { - return nil, err - } - - if exists { - _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name))) - continue - } - } - - parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(inv, parameterMapFromFile, templateVersionParameter) + parameterFile, err = parseParameterMapFile(args.RichParameterFile) if err != nil { - return nil, err + return nil, xerrors.Errorf("can't parse parameter map file: %w", err) } - - richParameters = append(richParameters, *parameterValue) } - if disclaimerPrinted { - _, _ = fmt.Fprintln(inv.Stdout) + resolver := new(ParameterResolver). + WithLastBuildParameters(args.LastBuildParameters). + WithPromptBuildOptions(args.PromptBuildOptions). + WithBuildOptions(args.BuildOptions). + WithPromptRichParameters(args.PromptRichParameters). + WithRichParameters(args.RichParameters). + WithRichParametersFile(parameterFile) + buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters) + if err != nil { + return nil, err } err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{ @@ -306,7 +269,7 @@ PromptRichParamLoop: // Run a dry-run with the given parameters to check correctness dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{ WorkspaceName: args.NewWorkspaceName, - RichParameterValues: richParameters, + RichParameterValues: buildParameters, }) if err != nil { return nil, xerrors.Errorf("begin workspace dry-run: %w", err) @@ -346,21 +309,5 @@ PromptRichParamLoop: return nil, xerrors.Errorf("get resources: %w", err) } - return &buildParameters{ - richParameters: richParameters, - }, nil -} - -func workspaceBuildParameterExists(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, templateVersionParameter codersdk.TemplateVersionParameter) (bool, error) { - lastBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID) - if err != nil { - return false, xerrors.Errorf("can't fetch last workspace build parameters: %w", err) - } - - for _, p := range lastBuildParameters { - if p.Name == templateVersionParameter.Name { - return true, nil - } - } - return false, nil + return buildParameters, nil } diff --git a/cli/create_test.go b/cli/create_test.go index 8f2bb6719a377..a86113eea854d 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -2,6 +2,7 @@ package cli_test import ( "context" + "fmt" "net/http" "os" "regexp" @@ -357,6 +358,41 @@ func TestCreateWithRichParameters(t *testing.T) { } <-doneChan }) + + t.Run("ParameterFlags", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, + "--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue), + "--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue), + "--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue)) + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + "Confirm create?", "yes", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + pty.WriteLine(value) + } + <-doneChan + }) } func TestCreateValidateRichParameters(t *testing.T) { diff --git a/cli/parameter.go b/cli/parameter.go index 77e8ccdc8ee67..efc415692eae0 100644 --- a/cli/parameter.go +++ b/cli/parameter.go @@ -4,71 +4,98 @@ import ( "encoding/json" "fmt" "os" + "strings" "golang.org/x/xerrors" "gopkg.in/yaml.v3" "github.com/coder/coder/cli/clibase" - "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" ) -// Reads a YAML file and populates a string -> string map. -// Throws an error if the file name is empty. -func createParameterMapFromFile(parameterFile string) (map[string]string, error) { - if parameterFile != "" { - parameterFileContents, err := os.ReadFile(parameterFile) - if err != nil { - return nil, err - } +// workspaceParameterFlags are used by commands processing rich parameters and/or build options. +type workspaceParameterFlags struct { + promptBuildOptions bool + buildOptions []string - mapStringInterface := make(map[string]interface{}) - err = yaml.Unmarshal(parameterFileContents, &mapStringInterface) - if err != nil { - return nil, err - } + richParameterFile string + richParameters []string +} - parameterMap := map[string]string{} - for k, v := range mapStringInterface { - switch val := v.(type) { - case string, bool, int: - parameterMap[k] = fmt.Sprintf("%v", val) - case []interface{}: - b, err := json.Marshal(&val) - if err != nil { - return nil, err - } - parameterMap[k] = string(b) - default: - return nil, xerrors.Errorf("invalid parameter type: %T", v) - } - } - return parameterMap, nil +func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option { + return clibase.OptionSet{ + { + Flag: "build-option", + Env: "CODER_BUILD_OPTION", + Description: `Build option value in the format "name=value".`, + Value: clibase.StringArrayOf(&wpf.buildOptions), + }, + { + Flag: "build-options", + Description: "Prompt for one-time build options defined with ephemeral parameters.", + Value: clibase.BoolOf(&wpf.promptBuildOptions), + }, + } +} + +func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option { + return clibase.OptionSet{ + clibase.Option{ + Flag: "parameter", + Env: "CODER_RICH_PARAMETER", + Description: `Rich parameter value in the format "name=value".`, + Value: clibase.StringArrayOf(&wpf.richParameters), + }, + clibase.Option{ + Flag: "rich-parameter-file", + Env: "CODER_RICH_PARAMETER_FILE", + Description: "Specify a file path with values for rich parameters defined in the template.", + Value: clibase.StringOf(&wpf.richParameterFile), + }, } +} - return nil, xerrors.Errorf("Parameter file name is not specified") +func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) { + var params []codersdk.WorkspaceBuildParameter + for _, nameValue := range nameValuePairs { + split := strings.SplitN(nameValue, "=", 2) + if len(split) < 2 { + return nil, xerrors.Errorf("format key=value expected, but got %s", nameValue) + } + params = append(params, codersdk.WorkspaceBuildParameter{ + Name: split[0], + Value: split[1], + }) + } + return params, nil } -func getWorkspaceBuildParameterValueFromMapOrInput(inv *clibase.Invocation, parameterMap map[string]string, templateVersionParameter codersdk.TemplateVersionParameter) (*codersdk.WorkspaceBuildParameter, error) { - var parameterValue string - var err error - if parameterMap != nil { - var ok bool - parameterValue, ok = parameterMap[templateVersionParameter.Name] - if !ok { - parameterValue, err = cliui.RichParameter(inv, templateVersionParameter) +func parseParameterMapFile(parameterFile string) (map[string]string, error) { + parameterFileContents, err := os.ReadFile(parameterFile) + if err != nil { + return nil, err + } + + mapStringInterface := make(map[string]interface{}) + err = yaml.Unmarshal(parameterFileContents, &mapStringInterface) + if err != nil { + return nil, err + } + + parameterMap := map[string]string{} + for k, v := range mapStringInterface { + switch val := v.(type) { + case string, bool, int: + parameterMap[k] = fmt.Sprintf("%v", val) + case []interface{}: + b, err := json.Marshal(&val) if err != nil { return nil, err } - } - } else { - parameterValue, err = cliui.RichParameter(inv, templateVersionParameter) - if err != nil { - return nil, err + parameterMap[k] = string(b) + default: + return nil, xerrors.Errorf("invalid parameter type: %T", v) } } - return &codersdk.WorkspaceBuildParameter{ - Name: templateVersionParameter.Name, - Value: parameterValue, - }, nil + return parameterMap, nil } diff --git a/cli/parameter_internal_test.go b/cli/parameter_internal_test.go index 81dfcefdf49b2..935486c6eae26 100644 --- a/cli/parameter_internal_test.go +++ b/cli/parameter_internal_test.go @@ -16,7 +16,7 @@ func TestCreateParameterMapFromFile(t *testing.T) { parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml") _, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n") - parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name()) + parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name()) expectedMap := map[string]string{ "region": "bananas", @@ -28,18 +28,10 @@ func TestCreateParameterMapFromFile(t *testing.T) { removeTmpDirUntilSuccess(t, tempDir) }) - t.Run("WithEmptyFilename", func(t *testing.T) { - t.Parallel() - - parameterMapFromFile, err := createParameterMapFromFile("") - - assert.Nil(t, parameterMapFromFile) - assert.EqualError(t, err, "Parameter file name is not specified") - }) t.Run("WithInvalidFilename", func(t *testing.T) { t.Parallel() - parameterMapFromFile, err := createParameterMapFromFile("invalidFile.yaml") + parameterMapFromFile, err := parseParameterMapFile("invalidFile.yaml") assert.Nil(t, parameterMapFromFile) @@ -57,7 +49,7 @@ func TestCreateParameterMapFromFile(t *testing.T) { parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml") _, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n") - parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name()) + parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name()) assert.Nil(t, parameterMapFromFile) assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]interface {}") diff --git a/cli/parameterresolver.go b/cli/parameterresolver.go new file mode 100644 index 0000000000000..9e803356b45bf --- /dev/null +++ b/cli/parameterresolver.go @@ -0,0 +1,224 @@ +package cli + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" +) + +type WorkspaceCLIAction int + +const ( + WorkspaceCreate WorkspaceCLIAction = iota + WorkspaceStart + WorkspaceUpdate + WorkspaceRestart +) + +type ParameterResolver struct { + lastBuildParameters []codersdk.WorkspaceBuildParameter + + richParameters []codersdk.WorkspaceBuildParameter + richParametersFile map[string]string + buildOptions []codersdk.WorkspaceBuildParameter + + promptRichParameters bool + promptBuildOptions bool +} + +func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver { + pr.lastBuildParameters = params + return pr +} + +func (pr *ParameterResolver) WithRichParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver { + pr.richParameters = params + return pr +} + +func (pr *ParameterResolver) WithBuildOptions(params []codersdk.WorkspaceBuildParameter) *ParameterResolver { + pr.buildOptions = params + return pr +} + +func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *ParameterResolver { + pr.richParametersFile = fileMap + return pr +} + +func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver { + pr.promptRichParameters = promptRichParameters + return pr +} + +func (pr *ParameterResolver) WithPromptBuildOptions(promptBuildOptions bool) *ParameterResolver { + pr.promptBuildOptions = promptBuildOptions + return pr +} + +func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) { + var staged []codersdk.WorkspaceBuildParameter + var err error + + staged = pr.resolveWithParametersMapFile(staged) + staged = pr.resolveWithCommandLineOrEnv(staged) + staged = pr.resolveWithLastBuildParameters(staged, templateVersionParameters) + if err = pr.verifyConstraints(staged, action, templateVersionParameters); err != nil { + return nil, err + } + if staged, err = pr.resolveWithInput(staged, inv, action, templateVersionParameters); err != nil { + return nil, err + } + return staged, nil +} + +func (pr *ParameterResolver) resolveWithParametersMapFile(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { + for name, value := range pr.richParametersFile { + for i, r := range resolved { + if r.Name == name { + resolved[i].Value = value + goto done + } + } + + resolved = append(resolved, codersdk.WorkspaceBuildParameter{ + Name: name, + Value: value, + }) + done: + } + return resolved +} + +func (pr *ParameterResolver) resolveWithCommandLineOrEnv(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { + for _, richParameter := range pr.richParameters { + for i, r := range resolved { + if r.Name == richParameter.Name { + resolved[i].Value = richParameter.Value + goto richParameterDone + } + } + + resolved = append(resolved, richParameter) + richParameterDone: + } + + for _, buildOption := range pr.buildOptions { + for i, r := range resolved { + if r.Name == buildOption.Name { + resolved[i].Value = buildOption.Value + goto buildOptionDone + } + } + + resolved = append(resolved, buildOption) + buildOptionDone: + } + return resolved +} + +func (pr *ParameterResolver) resolveWithLastBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter { + if pr.promptRichParameters { + return resolved // don't pull parameters from last build + } + + for _, buildParameter := range pr.lastBuildParameters { + tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters) + if tvp == nil { + continue // it looks like this parameter is not present anymore + } + + if tvp.Ephemeral { + continue // ephemeral parameters should not be passed to consecutive builds + } + + if !tvp.Mutable { + continue // immutables should not be passed to consecutive builds + } + + for i, r := range resolved { + if r.Name == buildParameter.Name { + resolved[i].Value = buildParameter.Value + goto done + } + } + + resolved = append(resolved, buildParameter) + done: + } + return resolved +} + +func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error { + for _, r := range resolved { + tvp := findTemplateVersionParameter(r, templateVersionParameters) + if tvp == nil { + return xerrors.Errorf("parameter %q is not present in the template", r.Name) + } + + if tvp.Ephemeral && !pr.promptBuildOptions && len(pr.buildOptions) == 0 { + return xerrors.Errorf("ephemeral parameter %q can be used only with --build-options or --build-option flag", r.Name) + } + + if !tvp.Mutable && action != WorkspaceCreate { + return xerrors.Errorf("parameter %q is immutable and cannot be updated", r.Name) + } + } + return nil +} + +func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) { + for _, tvp := range templateVersionParameters { + p := findWorkspaceBuildParameter(tvp, resolved) + if p != nil { + continue + } + + firstTimeUse := pr.isFirstTimeUse(tvp) + + if (tvp.Ephemeral && pr.promptBuildOptions) || + tvp.Required || + (action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) || + (action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) || + (action == WorkspaceCreate && !tvp.Ephemeral) { + parameterValue, err := cliui.RichParameter(inv, tvp) + if err != nil { + return nil, err + } + + resolved = append(resolved, codersdk.WorkspaceBuildParameter{ + Name: tvp.Name, + Value: parameterValue, + }) + } else if action == WorkspaceUpdate && !tvp.Mutable && !firstTimeUse { + _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name))) + } + } + return resolved, nil +} + +func (pr *ParameterResolver) isFirstTimeUse(tvp codersdk.TemplateVersionParameter) bool { + return findWorkspaceBuildParameter(tvp, pr.lastBuildParameters) == nil +} + +func findTemplateVersionParameter(workspaceBuildParameter codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) *codersdk.TemplateVersionParameter { + for _, tvp := range templateVersionParameters { + if tvp.Name == workspaceBuildParameter.Name { + return &tvp + } + } + return nil +} + +func findWorkspaceBuildParameter(tvp codersdk.TemplateVersionParameter, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter { + for _, p := range params { + if p.Name == tvp.Name { + return &p + } + } + return nil +} diff --git a/cli/restart.go b/cli/restart.go index 4cff7ac7571d7..f03b1ab0c4755 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + "golang.org/x/xerrors" + "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" @@ -21,7 +23,7 @@ func (r *RootCmd) restart() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: append(parameterFlags.options(), cliui.SkipPromptOption()), + Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()), Handler: func(inv *clibase.Invocation) error { ctx := inv.Context() out := inv.Stdout @@ -31,14 +33,29 @@ func (r *RootCmd) restart() *clibase.Cmd { return err } + lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID) + if err != nil { + return err + } + template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { return err } - buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ - Template: template, - BuildOptions: parameterFlags.buildOptions, + buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions) + if err != nil { + return xerrors.Errorf("can't parse build options: %w", err) + } + + buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Action: WorkspaceRestart, + Template: template, + + LastBuildParameters: lastBuildParameters, + + PromptBuildOptions: parameterFlags.promptBuildOptions, + BuildOptions: buildOptions, }) if err != nil { return err @@ -65,7 +82,7 @@ func (r *RootCmd) restart() *clibase.Cmd { build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStart, - RichParameterValues: buildParams.richParameters, + RichParameterValues: buildParameters, }) if err != nil { return err diff --git a/cli/restart_test.go b/cli/restart_test.go index 83b066e4defc5..be2c6ea423416 100644 --- a/cli/restart_test.go +++ b/cli/restart_test.go @@ -2,6 +2,7 @@ package cli_test import ( "context" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -126,4 +127,57 @@ func TestRestart(t *testing.T) { Value: ephemeralParameterValue, }) }) + + t.Run("BuildOptionFlags", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + inv, root := clitest.New(t, "restart", workspace.Name, + "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue)) + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + matches := []string{ + "Confirm restart workspace?", "yes", + "Stopping workspace", "", + "Starting workspace", "", + "workspace has been restarted", "", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + pty.ExpectMatch(match) + + if value != "" { + pty.WriteLine(value) + } + } + <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspace.Name, codersdk.WorkspaceOptions{}) + require.NoError(t, err) + actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID) + require.NoError(t, err) + require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{ + Name: ephemeralParameterName, + Value: ephemeralParameterValue, + }) + }) } diff --git a/cli/start.go b/cli/start.go index 5bd35867fd105..212ee1abbcb85 100644 --- a/cli/start.go +++ b/cli/start.go @@ -11,21 +11,6 @@ import ( "github.com/coder/coder/codersdk" ) -// workspaceParameterFlags are used by "start", "restart", and "update". -type workspaceParameterFlags struct { - buildOptions bool -} - -func (wpf *workspaceParameterFlags) options() []clibase.Option { - return clibase.OptionSet{ - { - Flag: "build-options", - Description: "Prompt for one-time build options defined with ephemeral parameters.", - Value: clibase.BoolOf(&wpf.buildOptions), - }, - } -} - func (r *RootCmd) start() *clibase.Cmd { var parameterFlags workspaceParameterFlags @@ -38,21 +23,36 @@ func (r *RootCmd) start() *clibase.Cmd { clibase.RequireNArgs(1), r.InitClient(client), ), - Options: append(parameterFlags.options(), cliui.SkipPromptOption()), + Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()), Handler: func(inv *clibase.Invocation) error { workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err } + lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID) + if err != nil { + return err + } + template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { return err } - buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ - Template: template, - BuildOptions: parameterFlags.buildOptions, + buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions) + if err != nil { + return xerrors.Errorf("unable to parse build options: %w", err) + } + + buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{ + Action: WorkspaceStart, + Template: template, + + LastBuildParameters: lastBuildParameters, + + PromptBuildOptions: parameterFlags.promptBuildOptions, + BuildOptions: buildOptions, }) if err != nil { return err @@ -60,7 +60,7 @@ func (r *RootCmd) start() *clibase.Cmd { build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionStart, - RichParameterValues: buildParams.richParameters, + RichParameterValues: buildParameters, }) if err != nil { return err @@ -79,16 +79,21 @@ func (r *RootCmd) start() *clibase.Cmd { } type prepStartWorkspaceArgs struct { - Template codersdk.Template - BuildOptions bool + Action WorkspaceCLIAction + Template codersdk.Template + + LastBuildParameters []codersdk.WorkspaceBuildParameter + + PromptBuildOptions bool + BuildOptions []codersdk.WorkspaceBuildParameter } -func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) (*buildParameters, error) { +func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) ([]codersdk.WorkspaceBuildParameter, error) { ctx := inv.Context() templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID) if err != nil { - return nil, err + return nil, xerrors.Errorf("get template version: %w", err) } templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID) @@ -96,30 +101,9 @@ func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args p return nil, xerrors.Errorf("get template version rich parameters: %w", err) } - richParameters := make([]codersdk.WorkspaceBuildParameter, 0) - if !args.BuildOptions { - return &buildParameters{ - richParameters: richParameters, - }, nil - } - - for _, templateVersionParameter := range templateVersionParameters { - if !templateVersionParameter.Ephemeral { - continue - } - - parameterValue, err := cliui.RichParameter(inv, templateVersionParameter) - if err != nil { - return nil, err - } - - richParameters = append(richParameters, codersdk.WorkspaceBuildParameter{ - Name: templateVersionParameter.Name, - Value: parameterValue, - }) - } - - return &buildParameters{ - richParameters: richParameters, - }, nil + resolver := new(ParameterResolver). + WithLastBuildParameters(args.LastBuildParameters). + WithPromptBuildOptions(args.PromptBuildOptions). + WithBuildOptions(args.BuildOptions) + return resolver.Resolve(inv, args.Action, templateVersionParameters) } diff --git a/cli/start_test.go b/cli/start_test.go index a302fe2ac1c40..b532500fb0777 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -1,6 +1,7 @@ package cli_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -99,4 +100,43 @@ func TestStart(t *testing.T) { Value: ephemeralParameterValue, }) }) + + t.Run("BuildOptionFlags", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + inv, root := clitest.New(t, "start", workspace.Name, + "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue)) + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch("workspace has been started") + <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{}) + require.NoError(t, err) + actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID) + require.NoError(t, err) + require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{ + Name: ephemeralParameterName, + Value: ephemeralParameterValue, + }) + }) } diff --git a/cli/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden index 6c8bcc46908c9..2e080b6e85ca7 100644 --- a/cli/testdata/coder_create_--help.golden +++ b/cli/testdata/coder_create_--help.golden @@ -7,6 +7,9 @@ Create a workspace  $ coder create /  Options + --parameter string-array, $CODER_RICH_PARAMETER + Rich parameter value in the format "name=value". + --rich-parameter-file string, $CODER_RICH_PARAMETER_FILE Specify a file path with values for rich parameters defined in the template. diff --git a/cli/testdata/coder_restart_--help.golden b/cli/testdata/coder_restart_--help.golden index c2079b9065dca..e16a6f9ff7e99 100644 --- a/cli/testdata/coder_restart_--help.golden +++ b/cli/testdata/coder_restart_--help.golden @@ -3,6 +3,9 @@ Usage: coder restart [flags] Restart a workspace Options + --build-option string-array, $CODER_BUILD_OPTION + Build option value in the format "name=value". + --build-options bool Prompt for one-time build options defined with ephemeral parameters. diff --git a/cli/testdata/coder_start_--help.golden b/cli/testdata/coder_start_--help.golden index aa447240e9bbb..b03c9975925f4 100644 --- a/cli/testdata/coder_start_--help.golden +++ b/cli/testdata/coder_start_--help.golden @@ -3,6 +3,9 @@ Usage: coder start [flags] Start a workspace Options + --build-option string-array, $CODER_BUILD_OPTION + Build option value in the format "name=value". + --build-options bool Prompt for one-time build options defined with ephemeral parameters. diff --git a/cli/testdata/coder_update_--help.golden b/cli/testdata/coder_update_--help.golden index 40e899cd37348..669bda831caa6 100644 --- a/cli/testdata/coder_update_--help.golden +++ b/cli/testdata/coder_update_--help.golden @@ -9,9 +9,15 @@ Use --always-prompt to change the parameter values of the workspace. Always prompt all parameters. Does not pull parameter values from existing workspace. + --build-option string-array, $CODER_BUILD_OPTION + Build option value in the format "name=value". + --build-options bool Prompt for one-time build options defined with ephemeral parameters. + --parameter string-array, $CODER_RICH_PARAMETER + Rich parameter value in the format "name=value". + --rich-parameter-file string, $CODER_RICH_PARAMETER_FILE Specify a file path with values for rich parameters defined in the template. diff --git a/cli/update.go b/cli/update.go index 64710217bb996..02ef238b36e21 100644 --- a/cli/update.go +++ b/cli/update.go @@ -3,14 +3,15 @@ package cli import ( "fmt" + "golang.org/x/xerrors" + "github.com/coder/coder/cli/clibase" "github.com/coder/coder/codersdk" ) func (r *RootCmd) update() *clibase.Cmd { var ( - richParameterFile string - alwaysPrompt bool + alwaysPrompt bool parameterFlags workspaceParameterFlags ) @@ -30,33 +31,45 @@ func (r *RootCmd) update() *clibase.Cmd { if err != nil { return err } - if !workspace.Outdated && !alwaysPrompt && !parameterFlags.buildOptions { + if !workspace.Outdated && !alwaysPrompt && !parameterFlags.promptBuildOptions && len(parameterFlags.buildOptions) == 0 { _, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n") return nil } + + buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions) + if err != nil { + return err + } + template, err := client.Template(inv.Context(), workspace.TemplateID) if err != nil { return err } - var existingRichParams []codersdk.WorkspaceBuildParameter - if !alwaysPrompt { - existingRichParams, err = client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID) - if err != nil { - return err - } + lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID) + if err != nil { + return err + } + + cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters) + if err != nil { + return xerrors.Errorf("can't parse given parameter values: %w", err) } - buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{ - Template: template, - ExistingRichParams: existingRichParams, - RichParameterFile: richParameterFile, - NewWorkspaceName: workspace.Name, + buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{ + Action: WorkspaceUpdate, + Template: template, + NewWorkspaceName: workspace.Name, + WorkspaceID: workspace.LatestBuild.ID, - UpdateWorkspace: true, - WorkspaceID: workspace.LatestBuild.ID, + LastBuildParameters: lastBuildParameters, - BuildOptions: parameterFlags.buildOptions, + PromptBuildOptions: parameterFlags.promptBuildOptions, + BuildOptions: buildOptions, + + PromptRichParameters: alwaysPrompt, + RichParameters: cliRichParameters, + RichParameterFile: parameterFlags.richParameterFile, }) if err != nil { return err @@ -65,7 +78,7 @@ func (r *RootCmd) update() *clibase.Cmd { build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: codersdk.WorkspaceTransitionStart, - RichParameterValues: buildParams.richParameters, + RichParameterValues: buildParameters, }) if err != nil { return err @@ -92,13 +105,8 @@ func (r *RootCmd) update() *clibase.Cmd { Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.", Value: clibase.BoolOf(&alwaysPrompt), }, - { - Flag: "rich-parameter-file", - Description: "Specify a file path with values for rich parameters defined in the template.", - Env: "CODER_RICH_PARAMETER_FILE", - Value: clibase.StringOf(&richParameterFile), - }, } - cmd.Options = append(cmd.Options, parameterFlags.options()...) + cmd.Options = append(cmd.Options, parameterFlags.cliBuildOptions()...) + cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...) return cmd } diff --git a/cli/update_test.go b/cli/update_test.go index 886adf9bea264..e830aabbde435 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -159,7 +159,7 @@ func TestUpdateWithRichParameters(t *testing.T) { matches := []string{ firstParameterDescription, firstParameterValue, - fmt.Sprintf("Parameter %q is not mutable, so can't be customized after workspace creation.", immutableParameterName), "", + fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", immutableParameterName), "", secondParameterDescription, secondParameterValue, } for i := 0; i < len(matches); i += 2 { @@ -236,6 +236,55 @@ func TestUpdateWithRichParameters(t *testing.T) { Value: ephemeralParameterValue, }) }) + + t.Run("BuildOptionFlags", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + const workspaceName = "my-workspace" + + inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", + "--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue), + "--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue), + "--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue)) + clitest.SetupConfig(t, client, root) + err := inv.Run() + assert.NoError(t, err) + + inv, root = clitest.New(t, "update", workspaceName, + "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue)) + clitest.SetupConfig(t, client, root) + + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch("Planning workspace") + <-doneChan + + // Verify if build option is set + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, codersdk.WorkspaceOptions{}) + require.NoError(t, err) + actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID) + require.NoError(t, err) + require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{ + Name: ephemeralParameterName, + Value: ephemeralParameterValue, + }) + }) } func TestUpdateValidateRichParameters(t *testing.T) { @@ -545,14 +594,11 @@ func TestUpdateValidateRichParameters(t *testing.T) { }() matches := []string{ - "added_parameter", "", - `Enter a value (default: "foobar")`, "abc", + "Planning workspace...", "", } for i := 0; i < len(matches); i += 2 { match := matches[i] - value := matches[i+1] pty.ExpectMatch(match) - pty.WriteLine(value) } <-doneChan }) diff --git a/docs/cli/create.md b/docs/cli/create.md index 3f7ff099b16c8..8c36ea44a3dd7 100644 --- a/docs/cli/create.md +++ b/docs/cli/create.md @@ -20,6 +20,15 @@ coder create [flags] [name] ## Options +### --parameter + +| | | +| ----------- | ---------------------------------- | +| Type | string-array | +| Environment | $CODER_RICH_PARAMETER | + +Rich parameter value in the format "name=value". + ### --rich-parameter-file | | | diff --git a/docs/cli/restart.md b/docs/cli/restart.md index 72daa5dec405d..d3b6010a92c2e 100644 --- a/docs/cli/restart.md +++ b/docs/cli/restart.md @@ -12,6 +12,15 @@ coder restart [flags] ## Options +### --build-option + +| | | +| ----------- | -------------------------------- | +| Type | string-array | +| Environment | $CODER_BUILD_OPTION | + +Build option value in the format "name=value". + ### --build-options | | | diff --git a/docs/cli/start.md b/docs/cli/start.md index 8c3fd7f71276e..120edfde679eb 100644 --- a/docs/cli/start.md +++ b/docs/cli/start.md @@ -12,6 +12,15 @@ coder start [flags] ## Options +### --build-option + +| | | +| ----------- | -------------------------------- | +| Type | string-array | +| Environment | $CODER_BUILD_OPTION | + +Build option value in the format "name=value". + ### --build-options | | | diff --git a/docs/cli/update.md b/docs/cli/update.md index 0b3d31a9755b8..b81172df6b9ca 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -26,6 +26,15 @@ Use --always-prompt to change the parameter values of the workspace. Always prompt all parameters. Does not pull parameter values from existing workspace. +### --build-option + +| | | +| ----------- | -------------------------------- | +| Type | string-array | +| Environment | $CODER_BUILD_OPTION | + +Build option value in the format "name=value". + ### --build-options | | | @@ -34,6 +43,15 @@ Always prompt all parameters. Does not pull parameter values from existing works Prompt for one-time build options defined with ephemeral parameters. +### --parameter + +| | | +| ----------- | ---------------------------------- | +| Type | string-array | +| Environment | $CODER_RICH_PARAMETER | + +Rich parameter value in the format "name=value". + ### --rich-parameter-file | | | From e0f644c5989948b64405886cf591635402ca3bb0 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 9 Aug 2023 16:09:25 +0400 Subject: [PATCH 057/156] test(coderd): fix TestWorkspaceWatcher hang waiting for update (#8992) Signed-off-by: Spike Curtis --- coderd/workspaces_test.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 2213cd4657a70..5fecd02dbea88 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2121,9 +2121,14 @@ func TestWorkspaceWatcher(t *testing.T) { case w, ok := <-wc: require.True(t, ok, "watch channel closed: %s", event) if ready == nil || ready(w) { - logger.Info(ctx, "done waiting for event", slog.F("event", event)) + logger.Info(ctx, "done waiting for event", + slog.F("event", event), + slog.F("workspace", w)) return } + logger.Info(ctx, "skipped update for event", + slog.F("event", event), + slog.F("workspace", w)) } } } @@ -2194,12 +2199,23 @@ func TestWorkspaceWatcher(t *testing.T) { }) // We want to verify pending state here, but it's possible that we reach // failed state fast enough that we never see pending. + sawFailed := false wait("workspace build pending or failed", func(w codersdk.Workspace) bool { - return w.LatestBuild.Status == codersdk.WorkspaceStatusPending || w.LatestBuild.Status == codersdk.WorkspaceStatusFailed - }) - wait("workspace build failed", func(w codersdk.Workspace) bool { - return w.LatestBuild.Status == codersdk.WorkspaceStatusFailed + switch w.LatestBuild.Status { + case codersdk.WorkspaceStatusPending: + return true + case codersdk.WorkspaceStatusFailed: + sawFailed = true + return true + default: + return false + } }) + if !sawFailed { + wait("workspace build failed", func(w codersdk.Workspace) bool { + return w.LatestBuild.Status == codersdk.WorkspaceStatusFailed + }) + } closeFunc.Close() build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart) From 73e518b0fb25d89bef9fd12226ffdb54ec459476 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 09:46:14 -0300 Subject: [PATCH 058/156] refactor(site): remove last 7 days label (#8986) --- .../TemplateInsightsPage/TemplateInsightsPage.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 19de025f0648c..584f36828139e 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -137,7 +137,6 @@ const DailyUsersPanel = ({ - Last 7 days {!data && } @@ -167,7 +166,6 @@ const UserLatencyPanel = ({ - Last 7 days {!data && } @@ -228,7 +226,6 @@ const TemplateUsagePanel = ({ App & IDE Usage - Last 7 days {!data && } @@ -311,7 +308,6 @@ const TemplateParametersUsagePanel = ({ Parameters usage - Last 7 days {!data && } @@ -533,11 +529,6 @@ const PanelTitle = styled(Box)(() => ({ fontWeight: 500, })) -const PanelSubtitle = styled(Box)(({ theme }) => ({ - fontSize: 13, - color: theme.palette.text.secondary, -})) - const PanelContent = styled(Box)(({ theme }) => ({ padding: theme.spacing(0, 3, 3), flex: 1, From a6716ca829f6428dbbcef55f358763e03c97ab56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:54:03 -0300 Subject: [PATCH 059/156] chore: bump chart.js from 3.9.1 to 4.3.3 in /site (#8874) * chore: bump chart.js from 3.9.1 to 4.3.3 in /site Bumps [chart.js](https://github.com/chartjs/Chart.js) from 3.9.1 to 4.3.3. - [Release notes](https://github.com/chartjs/Chart.js/releases) - [Commits](https://github.com/chartjs/Chart.js/compare/v3.9.1...v4.3.3) --- updated-dependencies: - dependency-name: chart.js dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Remove timescale * Fix step size --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Muhammad Atif Ali Co-authored-by: BrunoQuaresma --- site/package.json | 2 +- site/pnpm-lock.yaml | 27 ++++++++++++++--------- site/src/components/DAUChart/DAUChart.tsx | 5 +++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/site/package.json b/site/package.json index 8d08e287ebf1a..d3b89d966336b 100644 --- a/site/package.json +++ b/site/package.json @@ -56,7 +56,7 @@ "ansi-to-html": "0.7.2", "axios": "1.3.4", "canvas": "2.11.0", - "chart.js": "3.9.1", + "chart.js": "4.3.3", "chartjs-adapter-date-fns": "3.0.0", "chroma-js": "2.4.2", "color-convert": "2.0.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 0746844223c01..00d76bb9c3efc 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -85,11 +85,11 @@ dependencies: specifier: 2.11.0 version: 2.11.0 chart.js: - specifier: 3.9.1 - version: 3.9.1 + specifier: 4.3.3 + version: 4.3.3 chartjs-adapter-date-fns: specifier: 3.0.0 - version: 3.0.0(chart.js@3.9.1)(date-fns@2.30.0) + version: 3.0.0(chart.js@4.3.3)(date-fns@2.30.0) chroma-js: specifier: 2.4.2 version: 2.4.2 @@ -143,7 +143,7 @@ dependencies: version: 18.2.0 react-chartjs-2: specifier: 5.2.0 - version: 5.2.0(chart.js@3.9.1)(react@18.2.0) + version: 5.2.0(chart.js@4.3.3)(react@18.2.0) react-color: specifier: 2.19.3 version: 2.19.3(react@18.2.0) @@ -2516,6 +2516,10 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: true + /@kurkle/color@0.3.2: + resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} + dev: false + /@mapbox/node-pre-gyp@1.0.11: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -6185,17 +6189,20 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /chart.js@3.9.1: - resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==} + /chart.js@4.3.3: + resolution: {integrity: sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==} + engines: {pnpm: '>=7'} + dependencies: + '@kurkle/color': 0.3.2 dev: false - /chartjs-adapter-date-fns@3.0.0(chart.js@3.9.1)(date-fns@2.30.0): + /chartjs-adapter-date-fns@3.0.0(chart.js@4.3.3)(date-fns@2.30.0): resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} peerDependencies: chart.js: '>=2.8.0' date-fns: '>=2.0.0' dependencies: - chart.js: 3.9.1 + chart.js: 4.3.3 date-fns: 2.30.0 dev: false @@ -11151,13 +11158,13 @@ packages: unpipe: 1.0.0 dev: true - /react-chartjs-2@5.2.0(chart.js@3.9.1)(react@18.2.0): + /react-chartjs-2@5.2.0(chart.js@4.3.3)(react@18.2.0): resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==} peerDependencies: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - chart.js: 3.9.1 + chart.js: 4.3.3 react: 18.2.0 dev: false diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx index add0b6aa17db0..fe6d92e5fb332 100644 --- a/site/src/components/DAUChart/DAUChart.tsx +++ b/site/src/components/DAUChart/DAUChart.tsx @@ -69,11 +69,12 @@ export const DAUChart: FC = ({ daus }) => { }, }, x: { - ticks: {}, + ticks: { + stepSize: 2, + }, type: "time", time: { unit: "day", - stepSize: 2, }, }, }, From d73e3ad3f36688fcb8c4a8ea1fccdb96a3fcb4c9 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 11:26:43 -0300 Subject: [PATCH 060/156] fix(site): show user avatar on group page (#8997) --- site/src/components/UserAvatar/UserAvatar.tsx | 1 - site/src/pages/GroupsPage/GroupPage.tsx | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/site/src/components/UserAvatar/UserAvatar.tsx b/site/src/components/UserAvatar/UserAvatar.tsx index 95183a8001546..49349a328901b 100644 --- a/site/src/components/UserAvatar/UserAvatar.tsx +++ b/site/src/components/UserAvatar/UserAvatar.tsx @@ -9,7 +9,6 @@ export type UserAvatarProps = { export const UserAvatar: FC = ({ username, avatarURL, - ...avatarProps }) => { return ( diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 13bec3399bcb8..30801f2823362 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -34,6 +34,7 @@ import { groupMachine } from "xServices/groups/groupXService" import { Maybe } from "components/Conditionals/Maybe" import { makeStyles } from "@mui/styles" import { PaginationStatus } from "components/PaginationStatus/PaginationStatus" +import { UserAvatar } from "components/UserAvatar/UserAvatar" const AddGroupMember: React.FC<{ isLoading: boolean @@ -188,6 +189,12 @@ export const GroupPage: React.FC = () => { + } title={member.username} subtitle={member.email} /> From 7fceb9aaff6ef9fd2e90d8458cf4ae664487fbb6 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 11:26:56 -0300 Subject: [PATCH 061/156] fix(site): make stats bar scrollable on smaller viewports (#8996) --- .../components/DeploymentBanner/DeploymentBannerView.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index edee6dc836491..d27511911a74f 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -268,12 +268,8 @@ const useStyles = makeStyles((theme) => ({ fontSize: 12, gap: theme.spacing(4), borderTop: `1px solid ${theme.palette.divider}`, - - [theme.breakpoints.down("lg")]: { - flexDirection: "column", - gap: theme.spacing(1), - alignItems: "left", - }, + overflowX: "auto", + whiteSpace: "nowrap", }, group: { display: "flex", From 7b35f3b3adeb5ced1e591e92474f63b15d6cc87d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 11:28:25 -0300 Subject: [PATCH 062/156] fix(site): add horizontal scroll when having many tabs (#8998) --- site/src/components/TemplateFiles/TemplateFiles.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/components/TemplateFiles/TemplateFiles.tsx b/site/src/components/TemplateFiles/TemplateFiles.tsx index 11bc49397f4bb..73aaad4b1f38b 100644 --- a/site/src/components/TemplateFiles/TemplateFiles.tsx +++ b/site/src/components/TemplateFiles/TemplateFiles.tsx @@ -87,6 +87,7 @@ const useStyles = makeStyles((theme) => ({ alignItems: "baseline", borderBottom: `1px solid ${theme.palette.divider}`, gap: 1, + overflowX: "auto", }, tab: { @@ -101,6 +102,7 @@ const useStyles = makeStyles((theme) => ({ gap: theme.spacing(0.5), position: "relative", color: theme.palette.text.secondary, + whiteSpace: "nowrap", "& svg": { width: 22, From a9e01bf3f1c43c2ed373e47c1c32162a38a5ad46 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 9 Aug 2023 13:11:03 -0500 Subject: [PATCH 063/156] chore: fix terraform tests (#9006) --- .github/actions/setup-tf/action.yaml | 2 +- provisioner/terraform/provision_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-tf/action.yaml b/.github/actions/setup-tf/action.yaml index 16472c9fafd6e..63a539a3fd922 100644 --- a/.github/actions/setup-tf/action.yaml +++ b/.github/actions/setup-tf/action.yaml @@ -7,5 +7,5 @@ runs: - name: Install Terraform uses: hashicorp/setup-terraform@v2 with: - terraform_version: ~1.5 + terraform_version: 1.5.5 terraform_wrapper: false diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index c882807a8d434..28d88f568c123 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -351,7 +351,7 @@ func TestProvision(t *testing.T) { Files: map[string]string{ "main.tf": `a`, }, - ErrorContains: "plan terraform", + ErrorContains: "initialize terraform", ExpectLogContains: "Argument or block definition required", }, { @@ -359,7 +359,7 @@ func TestProvision(t *testing.T) { Files: map[string]string{ "main.tf": `;asdf;`, }, - ErrorContains: "plan terraform", + ErrorContains: "initialize terraform", ExpectLogContains: `The ";" character is not valid.`, }, { From 919f5c6fe94a5731898e677fd9c6d7f686ff81a8 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 9 Aug 2023 13:50:27 -0500 Subject: [PATCH 064/156] chore: increase e2e timeout to 60s (#9007) --- site/e2e/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 0ff9a6fe74622..78eb4495ce371 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ use: { storageState: STORAGE_STATE, }, + timeout: 60000, }, ], use: { From f334b66178c4861a51fbbd4ae23c0a3ae6965e78 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Aug 2023 13:56:13 -0500 Subject: [PATCH 065/156] chore: do not allow resetting password of non password users (#9003) --- coderd/users.go | 7 +++++++ site/src/components/UsersTable/UsersTable.stories.tsx | 8 ++++++++ site/src/components/UsersTable/UsersTableBody.tsx | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/coderd/users.go b/coderd/users.go index 017e20d408586..b34b447b8c456 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -733,6 +733,13 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { return } + if user.LoginType != database.LoginTypePassword { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Users without password login type cannot change their password.", + }) + return + } + err := userpassword.Validate(params.Password) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ diff --git a/site/src/components/UsersTable/UsersTable.stories.tsx b/site/src/components/UsersTable/UsersTable.stories.tsx index ce019b4d3e72b..536c39253fa44 100644 --- a/site/src/components/UsersTable/UsersTable.stories.tsx +++ b/site/src/components/UsersTable/UsersTable.stories.tsx @@ -42,6 +42,14 @@ Editable.args = { roles: [], status: "suspended", }, + { + ...MockUser, + username: "OIDC User", + email: "oidc.user@coder.com", + roles: [], + status: "active", + login_type: "oidc", + }, ], roles: MockAssignableSiteRoles, canEditUsers: true, diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index ad25de524f3a8..82e154a3d0d0b 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -205,7 +205,7 @@ export const UsersTableBody: FC< { label: t("resetPasswordMenuItem"), onClick: onResetUserPassword, - disabled: false, + disabled: user.login_type !== "password", }, { label: t("listWorkspacesMenuItem"), From 612f1c6a5544eef429b975d79c62d7cdc33d2f09 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 9 Aug 2023 14:03:02 -0500 Subject: [PATCH 066/156] chore: use echo provisioners in logging tests (#9008) --- cli/server_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/server_test.go b/cli/server_test.go index ee00499c4d2a6..ad971398c8e02 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1309,6 +1309,7 @@ func TestServer(t *testing.T) { "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", + "--provisioner-daemons-echo", "--log-human", fiName, ) clitest.Start(t, root) @@ -1326,6 +1327,7 @@ func TestServer(t *testing.T) { "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", + "--provisioner-daemons-echo", "--log-human", fi, ) clitest.Start(t, root) @@ -1343,6 +1345,7 @@ func TestServer(t *testing.T) { "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", + "--provisioner-daemons-echo", "--log-json", fi, ) clitest.Start(t, root) @@ -1363,6 +1366,7 @@ func TestServer(t *testing.T) { "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", + "--provisioner-daemons-echo", "--log-stackdriver", fi, ) // Attach pty so we get debug output from the command if this test @@ -1397,6 +1401,7 @@ func TestServer(t *testing.T) { "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", + "--provisioner-daemons-echo", "--log-human", fi1, "--log-json", fi2, "--log-stackdriver", fi3, From 53f26b313dc0f75e73e1ccea70f7c8052af6408c Mon Sep 17 00:00:00 2001 From: phorcys420 <57866459+phorcys420@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:22:46 +0200 Subject: [PATCH 067/156] fix(scripts): check if PR list is empty (#8805) Co-authored-by: Mathias Fredriksson --- scripts/release/check_commit_metadata.sh | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh index 7ad78992d0e0c..02a39525365d4 100755 --- a/scripts/release/check_commit_metadata.sh +++ b/scripts/release/check_commit_metadata.sh @@ -82,16 +82,20 @@ main() { --json mergeCommit,labels,author \ --jq '.[] | "\( .mergeCommit.oid ) author:\( .author.login ) labels:\(["label:\( .labels[].name )"] | join(" "))"' )" - mapfile -t pr_metadata_raw <<<"$pr_list_out" + declare -A authors labels - for entry in "${pr_metadata_raw[@]}"; do - commit_sha_long=${entry%% *} - commit_author=${entry#* author:} - commit_author=${commit_author%% *} - authors[$commit_sha_long]=$commit_author - all_labels=${entry#* labels:} - labels[$commit_sha_long]=$all_labels - done + if [[ -n $pr_list_out ]]; then + mapfile -t pr_metadata_raw <<<"$pr_list_out" + + for entry in "${pr_metadata_raw[@]}"; do + commit_sha_long=${entry%% *} + commit_author=${entry#* author:} + commit_author=${commit_author%% *} + authors[$commit_sha_long]=$commit_author + all_labels=${entry#* labels:} + labels[$commit_sha_long]=$all_labels + done + fi for commit in "${commits[@]}"; do mapfile -d ' ' -t parts <<<"$commit" From 5b9dc2ee8b19bcc140e83b712265d46ca4b3d195 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 16:31:12 -0300 Subject: [PATCH 068/156] fix(site): add search params to auth redirect (#9005) --- site/src/components/RequireAuth/RequireAuth.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/components/RequireAuth/RequireAuth.tsx b/site/src/components/RequireAuth/RequireAuth.tsx index dd7bcddf25662..c4031603774d0 100644 --- a/site/src/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/RequireAuth/RequireAuth.tsx @@ -10,7 +10,9 @@ export const RequireAuth: FC = () => { const [authState] = useAuth() const location = useLocation() const isHomePage = location.pathname === "/" - const navigateTo = isHomePage ? "/login" : embedRedirect(location.pathname) + const navigateTo = isHomePage + ? "/login" + : embedRedirect(`${location.pathname}${location.search}`) if (authState.matches("signedOut")) { return From bc862fa493b81d9d21cfc0992f84f413e18e26cc Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 9 Aug 2023 14:50:26 -0500 Subject: [PATCH 069/156] chore: upgrade tailscale to v1.46.1 (#8913) --- cli/clibase/cmd.go | 10 +- cli/configssh.go | 5 +- cli/create.go | 5 +- coderd/apidoc/docs.go | 26 ++- coderd/apidoc/swagger.json | 26 ++- coderd/database/dbfake/dbfake.go | 68 +++---- coderd/devtunnel/servers.go | 5 +- coderd/healthcheck/derp.go | 2 +- coderd/insights.go | 5 +- coderd/metricscache/metricscache.go | 10 +- coderd/prometheusmetrics/collector_test.go | 6 +- coderd/users_test.go | 7 +- coderd/util/slice/slice.go | 18 ++ coderd/util/slice/slice_test.go | 16 ++ coderd/workspaceagents.go | 5 +- docs/api/agents.md | 16 ++ docs/api/debug.md | 4 + docs/api/schemas.md | 71 +++++++- enterprise/tailnet/pgcoord.go | 17 +- go.mod | 88 +++++---- go.sum | 198 +++++++++++---------- scripts/ci-report/main.go | 7 +- scripts/metricsdocgen/main.go | 8 +- tailnet/conn.go | 120 ++++--------- tailnet/coordinator.go | 21 +-- 25 files changed, 467 insertions(+), 297 deletions(-) diff --git a/cli/clibase/cmd.go b/cli/clibase/cmd.go index 3e7dfe3903633..3e4328dbc05e4 100644 --- a/cli/clibase/cmd.go +++ b/cli/clibase/cmd.go @@ -14,6 +14,8 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" "gopkg.in/yaml.v3" + + "github.com/coder/coder/coderd/util/slice" ) // Cmd describes an executable command. @@ -102,11 +104,11 @@ func (c *Cmd) PrepareAll() error { } } - slices.SortFunc(c.Options, func(a, b Option) bool { - return a.Name < b.Name + slices.SortFunc(c.Options, func(a, b Option) int { + return slice.Ascending(a.Name, b.Name) }) - slices.SortFunc(c.Children, func(a, b *Cmd) bool { - return a.Name() < b.Name() + slices.SortFunc(c.Children, func(a, b *Cmd) int { + return slice.Ascending(a.Name(), b.Name()) }) for _, child := range c.Children { child.Parent = c diff --git a/cli/configssh.go b/cli/configssh.go index 162c3c2a95855..897742fd5a7bb 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" ) @@ -367,8 +368,8 @@ func (r *RootCmd) configSSH() *clibase.Cmd { } // Ensure stable sorting of output. - slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) bool { - return a.Name < b.Name + slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) int { + return slice.Ascending(a.Name, b.Name) }) for _, wc := range workspaceConfigs { sort.Strings(wc.Hosts) diff --git a/cli/create.go b/cli/create.go index 7a8c4ec417fac..9ed55af0b853c 100644 --- a/cli/create.go +++ b/cli/create.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/util/ptr" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" ) @@ -81,8 +82,8 @@ func (r *RootCmd) create() *clibase.Cmd { return err } - slices.SortFunc(templates, func(a, b codersdk.Template) bool { - return a.ActiveUserCount > b.ActiveUserCount + slices.SortFunc(templates, func(a, b codersdk.Template) int { + return slice.Descending(a.ActiveUserCount, b.ActiveUserCount) }) templateNames := make([]string, 0, len(templates)) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e83cf37d3206c..6d590b02d4904 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11543,11 +11543,31 @@ const docTemplate = `{ } } }, + "tailcfg.DERPHomeParams": { + "type": "object", + "properties": { + "regionScore": { + "description": "RegionScore scales latencies of DERP regions by a given scaling\nfactor when determining which region to use as the home\n(\"preferred\") DERP. Scores in the range (0, 1) will cause this\nregion to be proportionally more preferred, and scores in the range\n(1, ∞) will penalize a region.\n\nIf a region is not present in this map, it is treated as having a\nscore of 1.0.\n\nScores should not be 0 or negative; such scores will be ignored.\n\nA nil map means no change from the previous value (if any); an empty\nnon-nil map can be sent to reset all scores back to 1.0.", + "type": "object", + "additionalProperties": { + "type": "number" + } + } + } + }, "tailcfg.DERPMap": { "type": "object", "properties": { + "homeParams": { + "description": "HomeParams, if non-nil, is a change in home parameters.\n\nThe rest of the DEPRMap fields, if zero, means unchanged.", + "allOf": [ + { + "$ref": "#/definitions/tailcfg.DERPHomeParams" + } + ] + }, "omitDefaultRegions": { - "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.", + "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.\n\nThis field is only meaningful if the Regions map is non-nil (indicating a change).", "type": "boolean" }, "regions": { @@ -11562,6 +11582,10 @@ const docTemplate = `{ "tailcfg.DERPNode": { "type": "object", "properties": { + "canPort80": { + "description": "CanPort80 specifies whether this DERP node is accessible over HTTP\non port 80 specifically. This is used for captive portal checks.", + "type": "boolean" + }, "certName": { "description": "CertName optionally specifies the expected TLS cert common\nname. If empty, HostName is used. If CertName is non-empty,\nHostName is only used for the TCP dial (if IPv4/IPv6 are\nnot present) + TLS ClientHello.", "type": "string" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0206c57062720..09aef2e0872b4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10521,11 +10521,31 @@ } } }, + "tailcfg.DERPHomeParams": { + "type": "object", + "properties": { + "regionScore": { + "description": "RegionScore scales latencies of DERP regions by a given scaling\nfactor when determining which region to use as the home\n(\"preferred\") DERP. Scores in the range (0, 1) will cause this\nregion to be proportionally more preferred, and scores in the range\n(1, ∞) will penalize a region.\n\nIf a region is not present in this map, it is treated as having a\nscore of 1.0.\n\nScores should not be 0 or negative; such scores will be ignored.\n\nA nil map means no change from the previous value (if any); an empty\nnon-nil map can be sent to reset all scores back to 1.0.", + "type": "object", + "additionalProperties": { + "type": "number" + } + } + } + }, "tailcfg.DERPMap": { "type": "object", "properties": { + "homeParams": { + "description": "HomeParams, if non-nil, is a change in home parameters.\n\nThe rest of the DEPRMap fields, if zero, means unchanged.", + "allOf": [ + { + "$ref": "#/definitions/tailcfg.DERPHomeParams" + } + ] + }, "omitDefaultRegions": { - "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.", + "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.\n\nThis field is only meaningful if the Regions map is non-nil (indicating a change).", "type": "boolean" }, "regions": { @@ -10540,6 +10560,10 @@ "tailcfg.DERPNode": { "type": "object", "properties": { + "canPort80": { + "description": "CanPort80 specifies whether this DERP node is accessible over HTTP\non port 80 specifically. This is used for captive portal checks.", + "type": "boolean" + }, "certName": { "description": "CertName optionally specifies the expected TLS cert common\nname. If empty, HostName is used. If CertName is non-empty,\nHostName is only used for the TCP dial (if IPv4/IPv6 are\nnot present) + TLS ClientHello.", "type": "string" diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 156fe957d69a8..b9502c92c8f20 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -605,8 +605,8 @@ func uniqueSortedUUIDs(uuids []uuid.UUID) []uuid.UUID { for id := range set { unique = append(unique, id) } - slices.SortFunc(unique, func(a, b uuid.UUID) bool { - return a.String() < b.String() + slices.SortFunc(unique, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) }) return unique } @@ -2060,8 +2060,8 @@ func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.G for templateID := range ds.templateIDSet { templateIDs = append(templateIDs, templateID) } - slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool { - return a.String() < b.String() + slices.SortFunc(templateIDs, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) }) result = append(result, database.GetTemplateDailyInsightsRow{ StartTime: ds.startTime, @@ -2119,8 +2119,8 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem for templateID := range templateIDSet { templateIDs = append(templateIDs, templateID) } - slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool { - return a.String() < b.String() + slices.SortFunc(templateIDs, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) }) result := database.GetTemplateInsightsRow{ TemplateIDs: templateIDs, @@ -2343,13 +2343,16 @@ func (q *FakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg dat } // Database orders by created_at - slices.SortFunc(version, func(a, b database.TemplateVersion) bool { + slices.SortFunc(version, func(a, b database.TemplateVersion) int { if a.CreatedAt.Equal(b.CreatedAt) { // Technically the postgres database also orders by uuid. So match // that behavior - return a.ID.String() < b.ID.String() + return slice.Ascending(a.ID.String(), b.ID.String()) } - return a.CreatedAt.Before(b.CreatedAt) + if a.CreatedAt.Before(b.CreatedAt) { + return -1 + } + return 1 }) if arg.AfterID != uuid.Nil { @@ -2408,11 +2411,11 @@ func (q *FakeQuerier) GetTemplates(_ context.Context) ([]database.Template, erro defer q.mutex.RUnlock() templates := slices.Clone(q.templates) - slices.SortFunc(templates, func(i, j database.TemplateTable) bool { - if i.Name != j.Name { - return i.Name < j.Name + slices.SortFunc(templates, func(a, b database.TemplateTable) int { + if a.Name != b.Name { + return slice.Ascending(a.Name, b.Name) } - return i.ID.String() < j.ID.String() + return slice.Ascending(a.ID.String(), b.ID.String()) }) return q.templatesWithUserNoLock(templates), nil @@ -2525,8 +2528,8 @@ func (q *FakeQuerier) GetUserLatencyInsights(_ context.Context, arg database.Get for templateID := range templateIDSet { templateIDs = append(templateIDs, templateID) } - slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool { - return a.String() < b.String() + slices.SortFunc(templateIDs, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) }) user, err := q.getUserByIDNoLock(userID) if err != nil { @@ -2542,8 +2545,8 @@ func (q *FakeQuerier) GetUserLatencyInsights(_ context.Context, arg database.Get } rows = append(rows, row) } - slices.SortFunc(rows, func(a, b database.GetUserLatencyInsightsRow) bool { - return a.UserID.String() < b.UserID.String() + slices.SortFunc(rows, func(a, b database.GetUserLatencyInsightsRow) int { + return slice.Ascending(a.UserID.String(), b.UserID.String()) }) return rows, nil @@ -2590,8 +2593,8 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams copy(users, q.users) // Database orders by username - slices.SortFunc(users, func(a, b database.User) bool { - return strings.ToLower(a.Username) < strings.ToLower(b.Username) + slices.SortFunc(users, func(a, b database.User) int { + return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username)) }) // Filter out deleted since they should never be returned.. @@ -2799,14 +2802,14 @@ func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter tim agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { + if agentStat.CreatedAt.After(createdAfter) || agentStat.CreatedAt.Equal(createdAfter) { agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) } } latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { + if agentStat.CreatedAt.After(createdAfter) || agentStat.CreatedAt.Equal(createdAfter) { latestAgentStats[agentStat.AgentID] = agentStat } } @@ -3132,9 +3135,8 @@ func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, } // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { - // use greater than since we want descending order - return a.BuildNumber > b.BuildNumber + slices.SortFunc(history, func(a, b database.WorkspaceBuild) int { + return slice.Descending(a.BuildNumber, b.BuildNumber) }) if params.AfterID != uuid.Nil { @@ -3533,8 +3535,14 @@ func (q *FakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit alog := database.AuditLog(arg) q.auditLogs = append(q.auditLogs, alog) - slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) bool { - return a.Time.Before(b.Time) + slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) int { + if a.Time.Before(b.Time) { + return -1 + } else if a.Time.Equal(b.Time) { + return 0 + } else { + return 1 + } }) return alog, nil @@ -5588,11 +5596,11 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G templates = append(templates, template) } if len(templates) > 0 { - slices.SortFunc(templates, func(i, j database.Template) bool { - if i.Name != j.Name { - return i.Name < j.Name + slices.SortFunc(templates, func(a, b database.Template) int { + if a.Name != b.Name { + return slice.Ascending(a.Name, b.Name) } - return i.ID.String() < j.ID.String() + return slice.Ascending(a.ID.String(), b.ID.String()) }) return templates, nil } diff --git a/coderd/devtunnel/servers.go b/coderd/devtunnel/servers.go index 1ac1b6ce26a7c..e2b92d4982eb8 100644 --- a/coderd/devtunnel/servers.go +++ b/coderd/devtunnel/servers.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/cryptorand" ) @@ -115,8 +116,8 @@ func FindClosestNode(nodes []Node) (Node, error) { return Node{}, err } - slices.SortFunc(nodes, func(i, j Node) bool { - return i.AvgLatency < j.AvgLatency + slices.SortFunc(nodes, func(a, b Node) int { + return slice.Ascending(a.AvgLatency, b.AvgLatency) }) return nodes[0], nil } diff --git a/coderd/healthcheck/derp.go b/coderd/healthcheck/derp.go index 9fc88a37e40db..fd718b55031fa 100644 --- a/coderd/healthcheck/derp.go +++ b/coderd/healthcheck/derp.go @@ -118,7 +118,7 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) { mu.Unlock() } nc := &netcheck.Client{ - PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil), + PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil, nil, nil), Logf: tslogger.WithPrefix(ncLogf, "netcheck: "), } ncReport, netcheckErr := nc.GetReport(ctx, opts.DERPMap) diff --git a/coderd/insights.go b/coderd/insights.go index 509e78a18e234..b452b963277ed 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/coderd/database/db2sdk" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" ) @@ -131,8 +132,8 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) { for templateID := range templateIDSet { seenTemplateIDs = append(seenTemplateIDs, templateID) } - slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) bool { - return a.String() < b.String() + slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) }) resp := codersdk.UserLatencyInsightsResponse{ diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index d25b716f2df15..c99c72702a3d6 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -146,8 +146,14 @@ func convertDAUResponse[T dauRow](rows []T, tzOffset int) codersdk.DAUsResponse } dates := maps.Keys(respMap) - slices.SortFunc(dates, func(a, b time.Time) bool { - return a.Before(b) + slices.SortFunc(dates, func(a, b time.Time) int { + if a.Before(b) { + return -1 + } else if a.Equal(b) { + return 0 + } else { + return 1 + } }) var resp codersdk.DAUsResponse diff --git a/coderd/prometheusmetrics/collector_test.go b/coderd/prometheusmetrics/collector_test.go index 9d63f6669113d..df50182e61618 100644 --- a/coderd/prometheusmetrics/collector_test.go +++ b/coderd/prometheusmetrics/collector_test.go @@ -115,11 +115,11 @@ func TestCollector_Set_Add(t *testing.T) { assert.Equal(t, 6, int(metrics[1].Gauge.GetValue())) // Metric value } -func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count int) []dto.Metric { +func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count int) []*dto.Metric { ch := make(chan prometheus.Metric, count) defer close(ch) - var metrics []dto.Metric + var metrics []*dto.Metric collector.Collect(ch) for i := 0; i < count; i++ { @@ -129,7 +129,7 @@ func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count i err := m.Write(&metric) require.NoError(t, err) - metrics = append(metrics, metric) + metrics = append(metrics, &metric) } // Ensure always the same order of metrics diff --git a/coderd/users_test.go b/coderd/users_test.go index eff3174ad83a2..9b130133cd58a 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "sort" "strings" "testing" "time" @@ -12,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" "github.com/coder/coder/cli/clibase" @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" "github.com/coder/coder/testutil" ) @@ -1804,8 +1805,8 @@ func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client // sortUsers sorts by (created_at, id) func sortUsers(users []codersdk.User) { - sort.Slice(users, func(i, j int) bool { - return strings.ToLower(users[i].Username) < strings.ToLower(users[j].Username) + slices.SortFunc(users, func(a, b codersdk.User) int { + return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username)) }) } diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 0bc5920ce3760..c366b04f91d8d 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -1,5 +1,9 @@ package slice +import ( + "golang.org/x/exp/constraints" +) + // SameElements returns true if the 2 lists have the same elements in any // order. func SameElements[T comparable](a []T, b []T) bool { @@ -69,3 +73,17 @@ func OverlapCompare[T any](a []T, b []T, equal func(a, b T) bool) bool { func New[T any](items ...T) []T { return items } + +func Ascending[T constraints.Ordered](a, b T) int { + if a < b { + return -1 + } else if a == b { + return 0 + } else { + return 1 + } +} + +func Descending[T constraints.Ordered](a, b T) int { + return -Ascending[T](a, b) +} diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index b21e0cc0b52a5..73f38e9a3f255 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -107,3 +107,19 @@ func assertSetContains[T comparable](t *testing.T, set []T, in []T, out []T) { require.False(t, slice.Contains(set, e), "expect element in set") } } + +func TestAscending(t *testing.T) { + t.Parallel() + + assert.Equal(t, -1, slice.Ascending(1, 2)) + assert.Equal(t, 0, slice.Ascending(1, 1)) + assert.Equal(t, 1, slice.Ascending(2, 1)) +} + +func TestDescending(t *testing.T) { + t.Parallel() + + assert.Equal(t, 1, slice.Descending(1, 2)) + assert.Equal(t, 0, slice.Descending(1, 1)) + assert.Equal(t, -1, slice.Descending(2, 1)) +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 3fa5baf231997..53808d75d4241 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -39,6 +39,7 @@ import ( "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/util/ptr" + "github.com/coder/coder/coderd/util/slice" "github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/tailnet" @@ -1616,8 +1617,8 @@ func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Requ }) return } - slices.SortFunc(lastDBMeta, func(i, j database.WorkspaceAgentMetadatum) bool { - return i.Key < j.Key + slices.SortFunc(lastDBMeta, func(a, b database.WorkspaceAgentMetadatum) int { + return slice.Ascending(a.Key, b.Key) }) // Avoid sending refresh if the client is about to get a diff --git a/docs/api/agents.md b/docs/api/agents.md index 8bf6f10619e50..73b4abf8ec84b 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -393,6 +393,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \ } ], "derpmap": { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, "omitDefaultRegions": true, "regions": { "property1": { @@ -400,6 +406,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -423,6 +430,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -736,6 +744,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con ```json { "derp_map": { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, "omitDefaultRegions": true, "regions": { "property1": { @@ -743,6 +757,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -766,6 +781,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, diff --git a/docs/api/debug.md b/docs/api/debug.md index e3382c6586504..5016f6a87b256 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -102,6 +102,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -134,6 +135,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -164,6 +166,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -196,6 +199,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 048eff66adecb..cfcf28701ac1f 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -202,6 +202,12 @@ } ], "derpmap": { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, "omitDefaultRegions": true, "regions": { "property1": { @@ -209,6 +215,7 @@ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -232,6 +239,7 @@ "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -5597,6 +5605,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { "derp_map": { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, "omitDefaultRegions": true, "regions": { "property1": { @@ -5604,6 +5618,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -5627,6 +5642,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6637,6 +6653,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6695,6 +6712,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6727,6 +6745,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6807,6 +6826,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6839,6 +6859,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6869,6 +6890,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -6901,6 +6923,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7043,6 +7066,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7075,6 +7099,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7105,6 +7130,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "node": { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7137,6 +7163,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7281,10 +7308,38 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `time` | string | false | | | | `valid` | boolean | false | | Valid is true if Time is not NULL | +## tailcfg.DERPHomeParams + +```json +{ + "regionScore": { + "property1": 0, + "property2": 0 + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------- | ------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `regionScore` | object | false | | Regionscore scales latencies of DERP regions by a given scaling factor when determining which region to use as the home ("preferred") DERP. Scores in the range (0, 1) will cause this region to be proportionally more preferred, and scores in the range (1, ∞) will penalize a region. | + +If a region is not present in this map, it is treated as having a score of 1.0. +Scores should not be 0 or negative; such scores will be ignored. +A nil map means no change from the previous value (if any); an empty non-nil map can be sent to reset all scores back to 1.0.| +|» `[any property]`|number|false||| + ## tailcfg.DERPMap ```json { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, "omitDefaultRegions": true, "regions": { "property1": { @@ -7292,6 +7347,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7315,6 +7371,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7339,10 +7396,13 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `omitDefaultRegions` | boolean | false | | Omitdefaultregions specifies to not use Tailscale's DERP servers, and only use those specified in this DERPMap. If there are none set outside of the defaults, this is a noop. | -| `regions` | object | false | | Regions is the set of geographic regions running DERP node(s). | +| Name | Type | Required | Restrictions | Description | +| ---------------------------------------------------------------------------------- | ------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `homeParams` | [tailcfg.DERPHomeParams](#tailcfgderphomeparams) | false | | Homeparams if non-nil, is a change in home parameters. | +| The rest of the DEPRMap fields, if zero, means unchanged. | +| `omitDefaultRegions` | boolean | false | | Omitdefaultregions specifies to not use Tailscale's DERP servers, and only use those specified in this DERPMap. If there are none set outside of the defaults, this is a noop. | +| This field is only meaningful if the Regions map is non-nil (indicating a change). | +| `regions` | object | false | | Regions is the set of geographic regions running DERP node(s). | It's keyed by the DERPRegion.RegionID. The numbers are not necessarily contiguous.| @@ -7352,6 +7412,7 @@ The numbers are not necessarily contiguous.| ```json { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, @@ -7371,6 +7432,7 @@ The numbers are not necessarily contiguous.| | Name | Type | Required | Restrictions | Description | | --------------------------------------------------------------------------------------------------------------------- | ------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `canPort80` | boolean | false | | Canport80 specifies whether this DERP node is accessible over HTTP on port 80 specifically. This is used for captive portal checks. | | `certName` | string | false | | Certname optionally specifies the expected TLS cert common name. If empty, HostName is used. If CertName is non-empty, HostName is only used for the TCP dial (if IPv4/IPv6 are not present) + TLS ClientHello. | | `derpport` | integer | false | | Derpport optionally provides an alternate TLS port number for the DERP HTTPS server. | | If zero, 443 is used. | @@ -7394,6 +7456,7 @@ The numbers are not necessarily contiguous.| "embeddedRelay": true, "nodes": [ { + "canPort80": true, "certName": "string", "derpport": 0, "forceHTTP": true, diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go index 8693d8e9a5bdd..03593a238201e 100644 --- a/enterprise/tailnet/pgcoord.go +++ b/enterprise/tailnet/pgcoord.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/database/pubsub" "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/coderd/util/slice" agpl "github.com/coder/coder/tailnet" ) @@ -1351,8 +1352,8 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) { Node: conn.Node, }) } - slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) bool { - return a.Name < b.Name + slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) int { + return slice.Ascending(a.Name, b.Name) }) data.Agents = append(data.Agents, htmlAgent) @@ -1362,8 +1363,8 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) { Node: agent.Node, }) } - slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) bool { - return a.Name < b.Name + slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) int { + return slice.Ascending(a.Name, b.Name) }) for agentID, conns := range clients { @@ -1389,14 +1390,14 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) { Node: conn.Node, }) } - slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) bool { - return a.Name < b.Name + slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) int { + return slice.Ascending(a.Name, b.Name) }) data.MissingAgents = append(data.MissingAgents, agent) } - slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) bool { - return a.Name < b.Name + slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) int { + return slice.Ascending(a.Name, b.Name) }) return data, nil diff --git a/go.mod b/go.mod index 2bb28a73b9d28..4ac487adf127c 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,14 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0 // There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here: // https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main -replace tailscale.com => github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191 +replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c + +// This is replaced to include a fix that causes a deadlock when closing the +// wireguard network. +// The branch used is from https://github.com/coder/wireguard-go/tree/colin/tailscale +// It is based on https://github.com/tailscale/wireguard-go/tree/tailscale, but +// includes the upstream fix https://github.com/WireGuard/wireguard-go/commit/b7cd547315bed421a648d0a0f1ee5a0fc1b1151e +replace github.com/tailscale/wireguard-go => github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5 // Use our tempfork of gvisor that includes a fix for TCP connection stalls: // https://github.com/coder/coder/issues/7388 @@ -117,6 +124,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 github.com/google/uuid v1.3.0 + github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.1 @@ -130,7 +138,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/justinas/nosurf v1.1.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.16.3 + github.com/klauspost/compress v1.16.5 github.com/lib/pq v1.10.6 github.com/mattn/go-isatty v0.0.19 github.com/mitchellh/go-wordwrap v1.0.1 @@ -144,7 +152,7 @@ require ( github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.6-0.20221018182125-7da137aa03f0 github.com/prometheus/client_golang v1.16.0 - github.com/prometheus/client_model v0.3.0 + github.com/prometheus/client_model v0.4.0 github.com/prometheus/common v0.42.0 github.com/quasilyte/go-ruleguard/dsl v0.3.21 github.com/robfig/cron/v3 v3.0.1 @@ -168,15 +176,16 @@ require ( go.opentelemetry.io/otel/trace v1.16.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.2.1 - go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf + go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 golang.org/x/crypto v0.12.0 - golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 + golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b golang.org/x/mod v0.12.0 golang.org/x/net v0.14.0 golang.org/x/oauth2 v0.11.0 golang.org/x/sync v0.3.0 golang.org/x/sys v0.11.0 golang.org/x/term v0.11.0 + golang.org/x/text v0.12.0 golang.org/x/tools v0.11.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b @@ -185,23 +194,23 @@ require ( google.golang.org/protobuf v1.31.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 - gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 + gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f nhooyr.io/websocket v1.8.7 storj.io/drpc v0.0.33-0.20230420154621-9716137f6037 - tailscale.com v1.32.3 + tailscale.com v1.46.1 ) require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/logging v1.7.0 // indirect cloud.google.com/go/longrunning v0.5.1 // indirect - filippo.io/edwards25519 v1.0.0-rc.1 // indirect + filippo.io/edwards25519 v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/akutz/memconn v0.1.0 // indirect @@ -210,6 +219,19 @@ require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.18.22 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 // indirect + github.com/aws/smithy-go v1.13.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -225,8 +247,8 @@ require ( github.com/containerd/continuity v0.4.1 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/docker/cli v20.10.17+incompatible // indirect - github.com/docker/docker v23.0.3+incompatible // indirect + github.com/docker/cli v23.0.5+incompatible // indirect + github.com/docker/docker v23.0.5+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/elastic/go-windows v1.0.0 // indirect @@ -234,12 +256,13 @@ require ( github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-test/deep v1.0.8 // indirect @@ -255,17 +278,18 @@ require ( github.com/google/flatbuffers v23.1.21+incompatible // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl/v2 v2.17.0 // indirect @@ -273,14 +297,15 @@ require ( github.com/hashicorp/terraform-plugin-go v0.12.0 // indirect github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.20.0 // indirect - github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect + github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect - github.com/imdario/mergo v0.3.13 // indirect - github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect - github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect + github.com/jsimonetti/rtnetlink v1.3.2 // indirect github.com/juju/errors v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect @@ -292,13 +317,13 @@ require ( github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/mdlayher/genetlink v1.2.0 // indirect - github.com/mdlayher/netlink v1.6.2 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect - github.com/mdlayher/socket v0.2.3 // indirect + github.com/mdlayher/socket v0.4.1 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/microcosm-cc/bluemonday v1.0.23 // indirect - github.com/miekg/dns v1.1.45 // indirect + github.com/miekg/dns v1.1.55 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -314,6 +339,7 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc4 // indirect github.com/opencontainers/runc v1.1.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pion/transport v0.14.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -326,18 +352,18 @@ require ( github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect - github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect - github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 // indirect + github.com/tailscale/wireguard-go v0.0.0-20230710185534-bb2c8f22eccf // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tdewolff/parse/v2 v2.6.6 // indirect github.com/tdewolff/test v1.0.9 // indirect - github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect + github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vishvananda/netns v0.0.4 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect @@ -355,8 +381,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect - golang.org/x/text v0.12.0 + go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect golang.org/x/time v0.3.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde // indirect @@ -369,8 +394,3 @@ require ( howett.net/plist v1.0.0 // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect ) - -require ( - github.com/go-ini/ini v1.67.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect -) diff --git a/go.sum b/go.sum index 5e85b47943f66..3fc732489b5e3 100644 --- a/go.sum +++ b/go.sum @@ -46,15 +46,14 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= -filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -68,8 +67,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 h1:L6S7kR7SlhQKplIBpkra3s6yhcZV51lhRnXmYc4HohI= +github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -109,6 +108,32 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= +github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.18.22 h1:7vkUEmjjv+giht4wIROqLs+49VWmiQMMHSduxmoNKLU= +github.com/aws/aws-sdk-go-v2/config v1.18.22/go.mod h1:mN7Li1wxaPxSSy4Xkr6stFuinJGf3VZW3ZSNvO0q6sI= +github.com/aws/aws-sdk-go-v2/credentials v1.13.21 h1:VRiXnPEaaPeGeoFcXvMZOB5K/yfIXOYE3q97Kgb0zbU= +github.com/aws/aws-sdk-go-v2/credentials v1.13.21/go.mod h1:90Dk1lJoMyspa/EDUrldTxsPns0wn6+KpRKpdAWc0uA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= +github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3 h1:TQZH0Djie8VVgTBDOQ02M4zVHJFrNzLMsYMbNfRitVM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3/go.mod h1:p6MaesK9061w6NTiFmZpUzEkKUY5blKlwD2zYyErxKA= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 h1:GAiaQWuQhQQui76KjuXeShmyXqECwQ0mGRMc/rwsL+c= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.9/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 h1:TraLwncRJkWqtIBVKI/UqBymq4+hL+3MzUOtUATuzkA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 h1:6UbNM/KJhMBfOI5+lpVcJ/8OA7cBSz0O6OX37SRKlSw= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.10/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -161,8 +186,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhD github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= -github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= -github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= +github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ= github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0= github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= @@ -196,12 +220,14 @@ github.com/coder/retry v1.4.0 h1:g0fojHFxcdgM3sBULqgjFDxw1UIvaCqk4ngUDu0EWag= github.com/coder/retry v1.4.0/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY= github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c h1:TI7TzdFI0UvQmwgyQhtI1HeyYNRxAQpr8Tw/rjT8VSA= github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ= -github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191 h1:FweUPlasdC67APP0jS8LTUdVlWcl49cX4NqTkr0ZL/4= -github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA= +github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c h1:4NR1TCdxl6Dw2iQ37KDyvsLZgYYWvBSKCnsotofZrFg= +github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4= github.com/coder/terraform-provider-coder v0.11.1 h1:1sXcHfQrX8XhmLbtKxBED2lZ5jk3/ezBtaw6uVhpJZ4= github.com/coder/terraform-provider-coder v0.11.1/go.mod h1:UIfU3bYNeSzJJvHyJ30tEKjD6Z9utloI+HUM/7n94CY= github.com/coder/wgtunnel v0.1.5 h1:WP3sCj/3iJ34eKvpMQEp1oJHvm24RYh0NHbj1kfUKfs= github.com/coder/wgtunnel v0.1.5/go.mod h1:bokoUrHnUFY4lu9KOeSYiIcHTI2MO1KwqumU4DPDyJI= +github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5 h1:eDk/42Kj4xN4yfE504LsvcFEo3dWUiCOaBiWJ2uIH2A= +github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU= @@ -221,7 +247,7 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= -github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg= +github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -232,11 +258,11 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8 github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= -github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE= +github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho= -github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k= +github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -255,7 +281,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -269,7 +294,6 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= @@ -314,15 +338,18 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -423,6 +450,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54= @@ -433,6 +461,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c h1:06RMfw+TMMHtRuUOroMeatRCCgSMWXCJQeABvHU69YQ= +github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c/go.mod h1:BVIYo3cdnT4qSylnYqcd5YtmXhr51cJPGtnLBe/uLBU= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -458,7 +488,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -516,45 +545,42 @@ github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b57 github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= -github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= +github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= +github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg= -github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E= +github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI= github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= -github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= -github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM= -github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY= +github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI= +github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= @@ -566,8 +592,8 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDS github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= -github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= @@ -632,30 +658,21 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= -github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= -github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ= -github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= -github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= -github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= -github.com/mdlayher/netlink v1.6.2 h1:D2zGSkvYsJ6NreeED3JiVTu1lj2sIYATqSaZlhPzUgQ= -github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU= -github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= -github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= -github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= -github.com/miekg/dns v1.1.45 h1:g5fRIhm9nx7g8osrAvgb16QJfmyMsyOCb+J7LSv+Qzk= -github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -717,6 +734,9 @@ github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4a github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= +github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= @@ -738,8 +758,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= @@ -769,8 +789,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= @@ -796,6 +814,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -812,14 +831,12 @@ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG0 github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= -github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17 h1:cSm67hIDABvL13S0n9TNoVhzYwjb24M46znbABLll18= -github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU= +github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ= +github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= -github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 h1:etWp6uUwKu8NEj37K2OuMBnZ7EnVMKA7gJg5AqPFy/o= -github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667/go.mod h1:iiClgxBTruKI+nmzlQxbFw6c3nB/wb4Td/WCyX2berY= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tdewolff/parse/v2 v2.6.6 h1:Yld+0CrKUJaCV78DL1G2nk3C9lKrxyRTux5aaK/AkDo= @@ -831,8 +848,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0= github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -850,8 +867,8 @@ github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0m github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -925,12 +942,10 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= -go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4= -go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= -go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf h1:IdwJUzqoIo5lkr2EOyKoe5qipUaEjbOKKY5+fzPBZ3A= -go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf/go.mod h1:+QXzaoURFd0rGDIjDNpyIkv+F9R7EmeKorvlKRnhqgA= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= +go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= +go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE= +go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -947,6 +962,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -959,9 +975,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -987,6 +1002,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -996,14 +1012,11 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1018,7 +1031,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1027,14 +1039,12 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1061,7 +1071,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -1069,20 +1078,15 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1105,8 +1109,6 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1131,7 +1133,6 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1145,6 +1146,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1152,6 +1154,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1165,6 +1169,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1178,7 +1184,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -1224,8 +1229,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1356,6 +1361,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= @@ -1367,7 +1373,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= @@ -1378,7 +1383,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg= diff --git a/scripts/ci-report/main.go b/scripts/ci-report/main.go index 9e3ae7e39d6bb..e6a2cf736b524 100644 --- a/scripts/ci-report/main.go +++ b/scripts/ci-report/main.go @@ -12,6 +12,8 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/util/slice" ) func main() { @@ -161,9 +163,8 @@ func parseCIReport(report GotestsumReport) (CIReport, error) { } timeouts = timeoutsNorm - sortAZ := func(a, b string) bool { return a < b } - slices.SortFunc(packagesSortedByName, sortAZ) - slices.SortFunc(testSortedByName, sortAZ) + slices.SortFunc(packagesSortedByName, slice.Ascending[string]) + slices.SortFunc(testSortedByName, slice.Ascending[string]) var rep CIReport diff --git a/scripts/metricsdocgen/main.go b/scripts/metricsdocgen/main.go index fbeb148715c54..8589653172005 100644 --- a/scripts/metricsdocgen/main.go +++ b/scripts/metricsdocgen/main.go @@ -56,13 +56,13 @@ func main() { } } -func readMetrics() ([]dto.MetricFamily, error) { +func readMetrics() ([]*dto.MetricFamily, error) { f, err := os.Open(metricsFile) if err != nil { return nil, xerrors.New("can't open metrics file") } - var metrics []dto.MetricFamily + var metrics []*dto.MetricFamily decoder := expfmt.NewDecoder(f, expfmt.FmtProtoText) for { @@ -73,7 +73,7 @@ func readMetrics() ([]dto.MetricFamily, error) { } else if err != nil { return nil, err } - metrics = append(metrics, m) + metrics = append(metrics, &m) } sort.Slice(metrics, func(i, j int) bool { @@ -90,7 +90,7 @@ func readPrometheusDoc() ([]byte, error) { return doc, nil } -func updatePrometheusDoc(doc []byte, metricFamilies []dto.MetricFamily) ([]byte, error) { +func updatePrometheusDoc(doc []byte, metricFamilies []*dto.MetricFamily) ([]byte, error) { i := bytes.Index(doc, generatorPrefix) if i < 0 { return nil, xerrors.New("generator prefix tag not found") diff --git a/tailnet/conn.go b/tailnet/conn.go index ebe57d2606b1c..945402b43da89 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "net" "net/http" "net/netip" @@ -24,10 +23,12 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/connstats" "tailscale.com/net/dns" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/tsdial" "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/tsd" "tailscale.com/types/ipproto" "tailscale.com/types/key" tslogger "tailscale.com/types/logger" @@ -36,7 +37,6 @@ import ( "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" - "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" "tailscale.com/wgengine/wgcfg/nmcfg" @@ -138,7 +138,7 @@ func NewConn(options *Options) (conn *Conn, err error) { AllowedIPs: options.Addresses, } - wireguardMonitor, err := monitor.New(Logger(options.Logger.Named("net.wgmonitor"))) + wireguardMonitor, err := netmon.New(Logger(options.Logger.Named("net.wgmonitor"))) if err != nil { return nil, xerrors.Errorf("create wireguard link monitor: %w", err) } @@ -148,14 +148,15 @@ func NewConn(options *Options) (conn *Conn, err error) { } }() - IP() dialer := &tsdial.Dialer{ Logf: Logger(options.Logger.Named("net.tsdial")), } + sys := new(tsd.System) wireguardEngine, err := wgengine.NewUserspaceEngine(Logger(options.Logger.Named("net.wgengine")), wgengine.Config{ - LinkMonitor: wireguardMonitor, - Dialer: dialer, - ListenPort: options.ListenPort, + NetMon: wireguardMonitor, + Dialer: dialer, + ListenPort: options.ListenPort, + SetSubsystem: sys.Set, }) if err != nil { return nil, xerrors.Errorf("create wgengine: %w", err) @@ -170,16 +171,9 @@ func NewConn(options *Options) (conn *Conn, err error) { return ok } - // This is taken from Tailscale: - // https://github.com/tailscale/tailscale/blob/0f05b2c13ff0c305aa7a1655fa9c17ed969d65be/tsnet/tsnet.go#L247-L255 - wireguardInternals, ok := wireguardEngine.(wgengine.InternalsGetter) - if !ok { - return nil, xerrors.Errorf("wireguard engine isn't the correct type %T", wireguardEngine) - } - tunDevice, magicConn, dnsManager, ok := wireguardInternals.GetInternals() - if !ok { - return nil, xerrors.New("get wireguard internals") - } + sys.Set(wireguardEngine) + + magicConn := sys.MagicSock.Get() if options.DERPHeader != nil { magicConn.SetDERPHeader(options.DERPHeader.Clone()) } @@ -205,11 +199,11 @@ func NewConn(options *Options) (conn *Conn, err error) { netStack, err := netstack.Create( Logger(options.Logger.Named("net.netstack")), - tunDevice, + sys.Tun.Get(), wireguardEngine, magicConn, dialer, - dnsManager, + sys.DNSManager.Get(), ) if err != nil { return nil, xerrors.Errorf("create netstack: %w", err) @@ -252,7 +246,7 @@ func NewConn(options *Options) (conn *Conn, err error) { listeners: map[listenKey]*listener{}, peerMap: map[tailcfg.NodeID]*tailcfg.Node{}, lastDERPForcedWebsockets: map[int]string{}, - tunDevice: tunDevice, + tunDevice: sys.Tun.Get(), netMap: netMap, netStack: netStack, wireguardMonitor: wireguardMonitor, @@ -313,8 +307,7 @@ func NewConn(options *Options) (conn *Conn, err error) { server.sendNode() }) - netStack.ForwardTCPIn = server.forwardTCP - netStack.ForwardTCPSockOpts = server.forwardTCPSockOpts + netStack.GetTCPHandlerForFlow = server.forwardTCP err = netStack.Start(nil) if err != nil { @@ -363,7 +356,7 @@ type Conn struct { netMap *netmap.NetworkMap netStack *netstack.Impl magicConn *magicsock.Conn - wireguardMonitor *monitor.Mon + wireguardMonitor *netmon.Monitor wireguardRouter *router.Config wireguardEngine wgengine.Engine listeners map[listenKey]*listener @@ -449,6 +442,11 @@ func (c *Conn) SetDERPRegionDialer(dialer func(ctx context.Context, region *tail func (c *Conn) UpdateNodes(nodes []*Node, replacePeers bool) error { c.mutex.Lock() defer c.mutex.Unlock() + + if c.isClosed() { + return xerrors.New("connection closed") + } + status := c.Status() if replacePeers { c.netMap.Peers = []*tailcfg.Node{} @@ -481,7 +479,6 @@ func (c *Conn) UpdateNodes(nodes []*Node, replacePeers bool) error { } c.logger.Debug(context.Background(), "adding node", slog.F("node", node)) - peerStatus, ok := status.Peer[node.Key] peerNode := &tailcfg.Node{ ID: node.ID, Created: time.Now(), @@ -492,10 +489,6 @@ func (c *Conn) UpdateNodes(nodes []*Node, replacePeers bool) error { Endpoints: node.Endpoints, DERP: fmt.Sprintf("%s:%d", tailcfg.DerpMagicIP, node.PreferredDERP), Hostinfo: hostinfo.New().View(), - // Starting KeepAlive messages at the initialization - // of a connection cause it to hang for an unknown - // reason. TODO: @kylecarbs debug this! - KeepAlive: ok && peerStatus.Active, } if c.blockEndpoints { peerNode.Endpoints = nil @@ -823,68 +816,31 @@ func (c *Conn) DialContextUDP(ctx context.Context, ipp netip.AddrPort) (*gonet.U return c.netStack.DialContextUDP(ctx, ipp) } -func (c *Conn) forwardTCP(conn net.Conn, port uint16) { +func (c *Conn) forwardTCP(_, dst netip.AddrPort) (handler func(net.Conn), opts []tcpip.SettableSocketOption, intercept bool) { c.mutex.Lock() - ln, ok := c.listeners[listenKey{"tcp", "", fmt.Sprint(port)}] + ln, ok := c.listeners[listenKey{"tcp", "", fmt.Sprint(dst.Port())}] c.mutex.Unlock() if !ok { - c.forwardTCPToLocal(conn, port) - return + return nil, nil, false } - - t := time.NewTimer(time.Second) - defer t.Stop() - select { - case ln.conn <- conn: - return - case <-ln.closed: - case <-c.closed: - case <-t.C: - } - _ = conn.Close() -} - -func (*Conn) forwardTCPSockOpts(port uint16) []tcpip.SettableSocketOption { - opts := []tcpip.SettableSocketOption{} - // See: https://github.com/tailscale/tailscale/blob/c7cea825aea39a00aca71ea02bab7266afc03e7c/wgengine/netstack/netstack.go#L888 - if port == WorkspaceAgentSSHPort || port == 22 { - opt := tcpip.KeepaliveIdleOption(72*time.Hour + time.Minute) // Default ssh-max-timeout is 72h, so let's add some extra time. + if dst.Port() == WorkspaceAgentSSHPort || dst.Port() == 22 { + opt := tcpip.KeepaliveIdleOption(72 * time.Hour) opts = append(opts, &opt) } - return opts -} - -func (c *Conn) forwardTCPToLocal(conn net.Conn, port uint16) { - defer conn.Close() - dialAddrStr := net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))) - var stdDialer net.Dialer - server, err := stdDialer.DialContext(c.dialContext, "tcp", dialAddrStr) - if err != nil { - c.logger.Debug(c.dialContext, "dial local port", slog.F("port", port), slog.Error(err)) - return - } - defer server.Close() - - connClosed := make(chan error, 2) - go func() { - _, err := io.Copy(server, conn) - connClosed <- err - }() - go func() { - _, err := io.Copy(conn, server) - connClosed <- err - }() - select { - case err = <-connClosed: - case <-c.closed: - return - } - if err != nil { - c.logger.Debug(c.dialContext, "proxy connection closed with error", slog.Error(err)) - } - c.logger.Debug(c.dialContext, "forwarded connection closed", slog.F("local_addr", dialAddrStr)) + return func(conn net.Conn) { + t := time.NewTimer(time.Second) + defer t.Stop() + select { + case ln.conn <- conn: + return + case <-ln.closed: + case <-c.closed: + case <-t.C: + } + _ = conn.Close() + }, opts, true } // SetConnStatsCallback sets a callback to be called after maxPeriod or diff --git a/tailnet/coordinator.go b/tailnet/coordinator.go index d37ea5c290b5f..de0248527f3fd 100644 --- a/tailnet/coordinator.go +++ b/tailnet/coordinator.go @@ -21,6 +21,7 @@ import ( "tailscale.com/types/key" "cdr.dev/slog" + "github.com/coder/coder/coderd/util/slice" ) // Coordinator exchanges nodes with agents to establish connections. @@ -683,14 +684,14 @@ func HTTPDebugFromLocal( LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second), }) } - slices.SortFunc(agent.Connections, func(a, b *HTMLClient) bool { - return a.Name < b.Name + slices.SortFunc(agent.Connections, func(a, b *HTMLClient) int { + return slice.Ascending(a.Name, b.Name) }) data.Agents = append(data.Agents, agent) } - slices.SortFunc(data.Agents, func(a, b *HTMLAgent) bool { - return a.Name < b.Name + slices.SortFunc(data.Agents, func(a, b *HTMLAgent) int { + return slice.Ascending(a.Name, b.Name) }) for agentID, conns := range agentToConnectionSocketsMap { @@ -719,14 +720,14 @@ func HTTPDebugFromLocal( LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second), }) } - slices.SortFunc(agent.Connections, func(a, b *HTMLClient) bool { - return a.Name < b.Name + slices.SortFunc(agent.Connections, func(a, b *HTMLClient) int { + return slice.Ascending(a.Name, b.Name) }) data.MissingAgents = append(data.MissingAgents, agent) } - slices.SortFunc(data.MissingAgents, func(a, b *HTMLAgent) bool { - return a.Name < b.Name + slices.SortFunc(data.MissingAgents, func(a, b *HTMLAgent) int { + return slice.Ascending(a.Name, b.Name) }) for id, node := range nodesMap { @@ -737,8 +738,8 @@ func HTTPDebugFromLocal( Node: node, }) } - slices.SortFunc(data.Nodes, func(a, b *HTMLNode) bool { - return a.Name+a.ID.String() < b.Name+b.ID.String() + slices.SortFunc(data.Nodes, func(a, b *HTMLNode) int { + return slice.Ascending(a.Name+a.ID.String(), b.Name+b.ID.String()) }) return data From 3245e91a328d43e826287da3c254b3588158a085 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 16:53:32 -0300 Subject: [PATCH 070/156] fix(site): set default color and display error on appearance form (#9004) --- .../AppearanceSettingsPageView.tsx | 4 +++- .../appearance/appearanceXService.ts | 23 +++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 78e23d7434afb..d14aad57ca3ae 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -21,6 +21,7 @@ import { Stack } from "components/Stack/Stack" import { useFormik } from "formik" import { useTheme } from "@mui/styles" import Link from "@mui/material/Link" +import { colors } from "theme/colors" export type AppearanceSettingsPageViewProps = { appearance: UpdateAppearanceConfig @@ -53,7 +54,8 @@ export const AppearanceSettingsPageView = ({ initialValues: { message: appearance.service_banner.message, enabled: appearance.service_banner.enabled, - background_color: appearance.service_banner.background_color, + background_color: + appearance.service_banner.background_color ?? colors.blue[7], }, onSubmit: (values) => updateAppearance( diff --git a/site/src/xServices/appearance/appearanceXService.ts b/site/src/xServices/appearance/appearanceXService.ts index 267d07608b726..901854a4529c6 100644 --- a/site/src/xServices/appearance/appearanceXService.ts +++ b/site/src/xServices/appearance/appearanceXService.ts @@ -1,12 +1,12 @@ -import { displaySuccess } from "components/GlobalSnackbar/utils" +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils" import { assign, createMachine } from "xstate" import * as API from "../../api/api" import { AppearanceConfig } from "../../api/typesGenerated" +import { getErrorMessage } from "api/errors" export type AppearanceContext = { appearance?: AppearanceConfig getAppearanceError?: unknown - setAppearanceError?: unknown preview: boolean } @@ -39,11 +39,7 @@ export const appearanceMachine = createMachine( idle: { on: { SET_PREVIEW_APPEARANCE: { - actions: [ - "clearGetAppearanceError", - "clearSetAppearanceError", - "assignPreviewAppearance", - ], + actions: ["clearGetAppearanceError", "assignPreviewAppearance"], }, SAVE_APPEARANCE: "savingAppearance", }, @@ -64,7 +60,6 @@ export const appearanceMachine = createMachine( }, }, savingAppearance: { - entry: "clearSetAppearanceError", invoke: { id: "setAppearance", src: "setAppearance", @@ -74,7 +69,11 @@ export const appearanceMachine = createMachine( }, onError: { target: "idle", - actions: ["assignSetAppearanceError"], + actions: (_, error) => { + displayError( + getErrorMessage(error, "Failed to update appearance settings."), + ) + }, }, }, }, @@ -99,12 +98,6 @@ export const appearanceMachine = createMachine( clearGetAppearanceError: assign({ getAppearanceError: (_) => undefined, }), - assignSetAppearanceError: assign({ - setAppearanceError: (_, event) => event.data, - }), - clearSetAppearanceError: assign({ - setAppearanceError: (_) => undefined, - }), }, services: { getAppearance: async () => { From fb5e0c4bbad2e236beead79025e15b8d7d449187 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Wed, 9 Aug 2023 17:00:22 -0400 Subject: [PATCH 071/156] docs: add TLS config steps for K8s (#9011) * docs: add TLS config steps for K8s * add note on wildcard cert --- docs/admin/configure.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/admin/configure.md b/docs/admin/configure.md index e74d447c0b4e1..25f6d87583763 100644 --- a/docs/admin/configure.md +++ b/docs/admin/configure.md @@ -55,6 +55,36 @@ The Coder server can directly use TLS certificates with `CODER_TLS_ENABLE` and a - [Caddy](https://github.com/coder/coder/tree/main/examples/web-server/caddy) - [NGINX](https://github.com/coder/coder/tree/main/examples/web-server/nginx) +### Kubernetes TLS configuration + +Below are the steps to configure Coder to terminate TLS when running on Kubernetes. +You must have the certificate `.key` and `.crt` files in your working directory prior to step 1. + +1. Create the TLS secret in your Kubernetes cluster + +```console +kubectl create secret tls coder-tls -n --key="tls.key" --cert="tls.crt" +``` + +> You can use a single certificate for the both the access URL and wildcard access URL. +> The certificate CN must match the wildcard domain, such as `*.example.coder.com`. + +1. Reference the TLS secret in your Coder Helm chart values + +```yaml +coder: + tls: + secretName: + - coder-tls + + # Alternatively, if you use an Ingress controller to terminate TLS, + # set the following values: + ingress: + enable: true + secretName: coder-tls + wildcardSecretName: coder-tls +``` + ## PostgreSQL Database Coder uses a PostgreSQL database to store users, workspace metadata, and other deployment information. From c0d1cacc4946e1af4341aece37566fb0909ec606 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 9 Aug 2023 18:22:13 -0300 Subject: [PATCH 072/156] fix(site): fix storybook error and inconsistent snapshots (#9010) --- .../components/Resources/AgentMetadata.tsx | 3 +- .../GeneralSettingsPageView.stories.tsx | 63 ++++++++++++------- .../WorkspaceBuildLogsSection.tsx | 5 ++ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/site/src/components/Resources/AgentMetadata.tsx b/site/src/components/Resources/AgentMetadata.tsx index 53d6ad4feeb92..0c309492b86f4 100644 --- a/site/src/components/Resources/AgentMetadata.tsx +++ b/site/src/components/Resources/AgentMetadata.tsx @@ -201,7 +201,8 @@ const StaticWidth = (props: BoxProps) => { const ref = useRef(null) useEffect(() => { - if (!ref.current) { + // Ignore this in storybook + if (!ref.current || process.env.STORYBOOK === "true") { return } diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx index 2a027ef7e9c41..9e65fca98cc3a 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx @@ -1,11 +1,8 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { Meta, StoryObj } from "@storybook/react" import { mockApiError, MockDeploymentDAUResponse } from "testHelpers/entities" -import { - GeneralSettingsPageView, - GeneralSettingsPageViewProps, -} from "./GeneralSettingsPageView" +import { GeneralSettingsPageView } from "./GeneralSettingsPageView" -export default { +const meta: Meta = { title: "pages/GeneralSettingsPageView", component: GeneralSettingsPageView, args: { @@ -13,34 +10,58 @@ export default { { name: "Access URL", description: - "External URL to access your deployment. This must be accessible by all provisioned workspaces.", + "The URL that users will use to access the Coder deployment.", + flag: "access-url", + flag_shorthand: "", value: "https://dev.coder.com", + hidden: false, }, { name: "Wildcard Access URL", description: 'Specifies the wildcard hostname to use for workspace applications in the form "*.example.com".', + flag: "wildcard-access-url", + flag_shorthand: "", value: "*--apps.dev.coder.com", + hidden: false, + }, + { + name: "Experiments", + description: + "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", + flag: "experiments", + value: [ + "*", + "moons", + "workspace_actions", + "single_tailnet", + "deployment_health_page", + "template_parameters_insights", + ], + flag_shorthand: "", + hidden: false, }, ], deploymentDAUs: MockDeploymentDAUResponse, }, -} as ComponentMeta +} + +export default meta +type Story = StoryObj -const Template: Story = (args) => ( - -) -export const Page = Template.bind({}) +export const Page: Story = {} -export const NoDAUs = Template.bind({}) -NoDAUs.args = { - deploymentDAUs: undefined, +export const NoDAUs: Story = { + args: { + deploymentDAUs: undefined, + }, } -export const DAUError = Template.bind({}) -DAUError.args = { - deploymentDAUs: undefined, - getDeploymentDAUsError: mockApiError({ - message: "Error fetching DAUs.", - }), +export const DAUError: Story = { + args: { + deploymentDAUs: undefined, + getDeploymentDAUsError: mockApiError({ + message: "Error fetching DAUs.", + }), + }, } diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx index 7c062d23614aa..795c77bc3521b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx @@ -12,6 +12,11 @@ export const WorkspaceBuildLogsSection = ({ const scrollRef = useRef(null) useEffect(() => { + // Auto scrolling makes hard to snapshot test using Chromatic + if (process.env.STORYBOOK === "true") { + return + } + const scrollEl = scrollRef.current if (scrollEl) { scrollEl.scrollTop = scrollEl.scrollHeight From 370bdd6a03b3f04b918bbb551af943be40be878a Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 9 Aug 2023 19:17:57 -0500 Subject: [PATCH 073/156] fix(cli): only init `clistat.Client` when calling `coder stat` (#9013) --- cli/stat.go | 110 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/cli/stat.go b/cli/stat.go index 6311596ecea2a..6a49ffb8437b5 100644 --- a/cli/stat.go +++ b/cli/stat.go @@ -12,31 +12,43 @@ import ( "github.com/coder/coder/cli/cliui" ) -func (r *RootCmd) stat() *clibase.Cmd { - fs := afero.NewReadOnlyFs(afero.NewOsFs()) - defaultCols := []string{ - "host_cpu", - "host_memory", - "home_disk", - "container_cpu", - "container_memory", - } - formatter := cliui.NewOutputFormatter( - cliui.TableFormat([]statsRow{}, defaultCols), - cliui.JSONFormat(), - ) - st, err := clistat.New(clistat.WithFS(fs)) - if err != nil { - panic(xerrors.Errorf("initialize workspace stats collector: %w", err)) +func initStatterMW(tgt **clistat.Statter, fs afero.Fs) clibase.MiddlewareFunc { + return func(next clibase.HandlerFunc) clibase.HandlerFunc { + return func(i *clibase.Invocation) error { + var err error + stat, err := clistat.New(clistat.WithFS(fs)) + if err != nil { + return xerrors.Errorf("initialize workspace stats collector: %w", err) + } + *tgt = stat + return next(i) + } } +} +func (r *RootCmd) stat() *clibase.Cmd { + var ( + st *clistat.Statter + fs = afero.NewReadOnlyFs(afero.NewOsFs()) + formatter = cliui.NewOutputFormatter( + cliui.TableFormat([]statsRow{}, []string{ + "host_cpu", + "host_memory", + "home_disk", + "container_cpu", + "container_memory", + }), + cliui.JSONFormat(), + ) + ) cmd := &clibase.Cmd{ - Use: "stat", - Short: "Show resource usage for the current workspace.", + Use: "stat", + Short: "Show resource usage for the current workspace.", + Middleware: initStatterMW(&st, fs), Children: []*clibase.Cmd{ - r.statCPU(st, fs), - r.statMem(st, fs), - r.statDisk(st), + r.statCPU(fs), + r.statMem(fs), + r.statDisk(fs), }, Handler: func(inv *clibase.Invocation) error { var sr statsRow @@ -118,12 +130,16 @@ func (r *RootCmd) stat() *clibase.Cmd { return cmd } -func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { - var hostArg bool - formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) +func (*RootCmd) statCPU(fs afero.Fs) *clibase.Cmd { + var ( + hostArg bool + st *clistat.Statter + formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) + ) cmd := &clibase.Cmd{ - Use: "cpu", - Short: "Show CPU usage, in cores.", + Use: "cpu", + Short: "Show CPU usage, in cores.", + Middleware: initStatterMW(&st, fs), Options: clibase.OptionSet{ { Flag: "host", @@ -135,9 +151,9 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { var cs *clistat.Result var err error if ok, _ := clistat.IsContainerized(fs); ok && !hostArg { - cs, err = s.ContainerCPU() + cs, err = st.ContainerCPU() } else { - cs, err = s.HostCPU() + cs, err = st.HostCPU() } if err != nil { return err @@ -155,13 +171,17 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { return cmd } -func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { - var hostArg bool - var prefixArg string - formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) +func (*RootCmd) statMem(fs afero.Fs) *clibase.Cmd { + var ( + hostArg bool + prefixArg string + st *clistat.Statter + formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) + ) cmd := &clibase.Cmd{ - Use: "mem", - Short: "Show memory usage, in gigabytes.", + Use: "mem", + Short: "Show memory usage, in gigabytes.", + Middleware: initStatterMW(&st, fs), Options: clibase.OptionSet{ { Flag: "host", @@ -185,9 +205,9 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { var ms *clistat.Result var err error if ok, _ := clistat.IsContainerized(fs); ok && !hostArg { - ms, err = s.ContainerMemory(pfx) + ms, err = st.ContainerMemory(pfx) } else { - ms, err = s.HostMemory(pfx) + ms, err = st.HostMemory(pfx) } if err != nil { return err @@ -205,13 +225,17 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd { return cmd } -func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd { - var pathArg string - var prefixArg string - formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) +func (*RootCmd) statDisk(fs afero.Fs) *clibase.Cmd { + var ( + pathArg string + prefixArg string + st *clistat.Statter + formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) + ) cmd := &clibase.Cmd{ - Use: "disk", - Short: "Show disk usage, in gigabytes.", + Use: "disk", + Short: "Show disk usage, in gigabytes.", + Middleware: initStatterMW(&st, fs), Options: clibase.OptionSet{ { Flag: "path", @@ -237,7 +261,7 @@ func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd { if len(inv.Args) > 0 { pathArg = inv.Args[0] } - ds, err := s.Disk(pfx, pathArg) + ds, err := st.Disk(pfx, pathArg) if err != nil { if os.IsNotExist(err) { //nolint:gocritic // fmt.Errorf produces a more concise error. From cdb089049e2d67daeb28a681ea886934319aa0ef Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Aug 2023 22:43:44 -0500 Subject: [PATCH 074/156] chore: add docs for creating missing groups on oidc sync (#8983) --- docs/admin/auth.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/admin/auth.md b/docs/admin/auth.md index 16807c159fc37..794def7f67e9e 100644 --- a/docs/admin/auth.md +++ b/docs/admin/auth.md @@ -288,6 +288,28 @@ OIDC provider will be added to the `myCoderGroupName` group in Coder. Some common issues when enabling group sync. +#### User not being assigned / Group does not exist + +If you want Coder to create groups that do not exist, you can set the following environment variable. If you enable this, your OIDC provider might be sending over many unnecessary groups. Use filtering options on the OIDC provider to limit the groups sent over to prevent creating excess groups. + +```console +# as an environment variable +CODER_OIDC_GROUP_AUTO_CREATE=true + +# as a flag +--oidc-group-auto-create=true +``` + +A basic regex filtering option on the Coder side is available. This is applied **after** the group mapping (`CODER_OIDC_GROUP_MAPPING`), meaning if the group is remapped, the remapped value is tested in the regex. This is useful if you want to filter out groups that do not match a certain pattern. For example, if you want to only allow groups that start with `my-group-` to be created, you can set the following environment variable. + +```console +# as an environment variable +CODER_OIDC_GROUP_REGEX_FILTER="^my-group-.*$" + +# as a flag +--oidc-group-regex-filter="^my-group-.*$" +``` + #### Invalid Scope If you see an error like the following, you may have an invalid scope. From 21af02038603f3c9177ffc0de6806d4f232c18a1 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 10 Aug 2023 13:59:43 +0400 Subject: [PATCH 075/156] feat: add external provisioner daemon helm chart (#8939) * Refactor helm to extract common templates to libcoder Signed-off-by: Spike Curtis * Remove comment from libcoder Chart.yaml Signed-off-by: Spike Curtis * Add provisioner helm chart * Fix prettier, linting, docs Signed-off-by: Spike Curtis * Log at INFO when provisionerd connects to coderd Signed-off-by: Spike Curtis * remove unnecessary exports in helm tests Signed-off-by: Spike Curtis --------- Signed-off-by: Spike Curtis --- .prettierignore | 2 +- .prettierignore.include | 2 +- Makefile | 10 +- docs/admin/configure.md | 2 +- docs/admin/scale.md | 2 +- docs/install/kubernetes.md | 2 +- helm/Makefile | 11 +- helm/{ => coder}/.helmignore | 0 helm/coder/Chart.lock | 6 + helm/{ => coder}/Chart.yaml | 7 +- helm/{ => coder}/README.md | 0 helm/coder/charts/libcoder-0.1.0.tgz | Bin 0 -> 2999 bytes helm/{ => coder}/templates/NOTES.txt | 0 helm/coder/templates/_coder.tpl | 102 ++++++++ helm/coder/templates/coder.yaml | 5 + .../templates/extra-templates.yaml | 0 helm/{ => coder}/templates/ingress.yaml | 0 helm/coder/templates/rbac.yaml | 1 + helm/{ => coder}/templates/service.yaml | 0 helm/{ => coder}/tests/chart_test.go | 24 +- .../{ => coder}/tests/testdata/command.golden | 141 ++++++----- helm/{ => coder}/tests/testdata/command.yaml | 0 .../tests/testdata/command_args.golden | 143 ++++++------ .../tests/testdata/command_args.yaml | 0 .../tests/testdata/default_values.golden | 141 ++++++----- .../tests/testdata/default_values.yaml | 0 .../tests/testdata/labels_annotations.golden | 147 ++++++------ .../tests/testdata/labels_annotations.yaml | 0 .../tests/testdata/missing_values.yaml | 0 .../tests/testdata/provisionerd_psk.golden | 194 +++++++++++++++ .../tests/testdata/provisionerd_psk.yaml | 5 + helm/{ => coder}/tests/testdata/sa.golden | 144 ++++++------ helm/{ => coder}/tests/testdata/sa.yaml | 0 helm/coder/tests/testdata/tls.golden | 210 +++++++++++++++++ helm/{ => coder}/tests/testdata/tls.yaml | 0 .../tests/testdata/workspace_proxy.golden | 197 ++++++++++++++++ .../tests/testdata/workspace_proxy.yaml | 0 helm/{ => coder}/values.yaml | 10 + helm/libcoder/Chart.yaml | 13 ++ helm/libcoder/templates/_coder.yaml | 85 +++++++ helm/{ => libcoder}/templates/_helpers.tpl | 20 +- .../templates/_rbac.yaml} | 6 +- helm/libcoder/templates/_util.yaml | 13 ++ helm/provisioner/Chart.lock | 6 + helm/provisioner/Chart.yaml | 34 +++ helm/provisioner/charts/libcoder-0.1.0.tgz | Bin 0 -> 2994 bytes helm/provisioner/templates/_coder.tpl | 85 +++++++ helm/provisioner/templates/coder.yaml | 5 + helm/provisioner/templates/rbac.yaml | 1 + helm/provisioner/tests/chart_test.go | 172 ++++++++++++++ .../provisioner/tests/testdata/command.golden | 135 +++++++++++ helm/provisioner/tests/testdata/command.yaml | 5 + .../tests/testdata/command_args.golden | 135 +++++++++++ .../tests/testdata/command_args.yaml | 6 + .../tests/testdata/default_values.golden | 135 +++++++++++ .../tests/testdata/default_values.yaml | 3 + .../tests/testdata/labels_annotations.golden | 143 ++++++++++++ .../tests/testdata/labels_annotations.yaml | 15 ++ .../tests/testdata/missing_values.yaml | 0 .../tests/testdata/provisionerd_psk.golden | 137 +++++++++++ .../tests/testdata/provisionerd_psk.yaml | 8 + helm/provisioner/tests/testdata/sa.golden | 136 +++++++++++ helm/provisioner/tests/testdata/sa.yaml | 8 + helm/provisioner/values.yaml | 204 ++++++++++++++++ helm/templates/coder.yaml | 143 ------------ helm/tests/testdata/tls.golden | 220 ------------------ helm/tests/testdata/workspace_proxy.golden | 206 ---------------- provisionerd/provisionerd.go | 2 +- scripts/helm.sh | 4 +- site/.eslintignore | 2 +- site/.prettierignore | 2 +- 71 files changed, 2616 insertions(+), 981 deletions(-) rename helm/{ => coder}/.helmignore (100%) create mode 100644 helm/coder/Chart.lock rename helm/{ => coder}/Chart.yaml (85%) rename helm/{ => coder}/README.md (100%) create mode 100644 helm/coder/charts/libcoder-0.1.0.tgz rename helm/{ => coder}/templates/NOTES.txt (100%) create mode 100644 helm/coder/templates/_coder.tpl create mode 100644 helm/coder/templates/coder.yaml rename helm/{ => coder}/templates/extra-templates.yaml (100%) rename helm/{ => coder}/templates/ingress.yaml (100%) create mode 100644 helm/coder/templates/rbac.yaml rename helm/{ => coder}/templates/service.yaml (100%) rename helm/{ => coder}/tests/chart_test.go (91%) rename helm/{ => coder}/tests/testdata/command.golden (61%) rename helm/{ => coder}/tests/testdata/command.yaml (100%) rename helm/{ => coder}/tests/testdata/command_args.golden (61%) rename helm/{ => coder}/tests/testdata/command_args.yaml (100%) rename helm/{ => coder}/tests/testdata/default_values.golden (61%) rename helm/{ => coder}/tests/testdata/default_values.yaml (100%) rename helm/{ => coder}/tests/testdata/labels_annotations.golden (64%) rename helm/{ => coder}/tests/testdata/labels_annotations.yaml (100%) rename helm/{ => coder}/tests/testdata/missing_values.yaml (100%) create mode 100644 helm/coder/tests/testdata/provisionerd_psk.golden create mode 100644 helm/coder/tests/testdata/provisionerd_psk.yaml rename helm/{ => coder}/tests/testdata/sa.golden (60%) rename helm/{ => coder}/tests/testdata/sa.yaml (100%) create mode 100644 helm/coder/tests/testdata/tls.golden rename helm/{ => coder}/tests/testdata/tls.yaml (100%) create mode 100644 helm/coder/tests/testdata/workspace_proxy.golden rename helm/{ => coder}/tests/testdata/workspace_proxy.yaml (100%) rename helm/{ => coder}/values.yaml (96%) create mode 100644 helm/libcoder/Chart.yaml create mode 100644 helm/libcoder/templates/_coder.yaml rename helm/{ => libcoder}/templates/_helpers.tpl (93%) rename helm/{templates/rbac.yaml => libcoder/templates/_rbac.yaml} (85%) create mode 100644 helm/libcoder/templates/_util.yaml create mode 100644 helm/provisioner/Chart.lock create mode 100644 helm/provisioner/Chart.yaml create mode 100644 helm/provisioner/charts/libcoder-0.1.0.tgz create mode 100644 helm/provisioner/templates/_coder.tpl create mode 100644 helm/provisioner/templates/coder.yaml create mode 100644 helm/provisioner/templates/rbac.yaml create mode 100644 helm/provisioner/tests/chart_test.go create mode 100644 helm/provisioner/tests/testdata/command.golden create mode 100644 helm/provisioner/tests/testdata/command.yaml create mode 100644 helm/provisioner/tests/testdata/command_args.golden create mode 100644 helm/provisioner/tests/testdata/command_args.yaml create mode 100644 helm/provisioner/tests/testdata/default_values.golden create mode 100644 helm/provisioner/tests/testdata/default_values.yaml create mode 100644 helm/provisioner/tests/testdata/labels_annotations.golden create mode 100644 helm/provisioner/tests/testdata/labels_annotations.yaml create mode 100644 helm/provisioner/tests/testdata/missing_values.yaml create mode 100644 helm/provisioner/tests/testdata/provisionerd_psk.golden create mode 100644 helm/provisioner/tests/testdata/provisionerd_psk.yaml create mode 100644 helm/provisioner/tests/testdata/sa.golden create mode 100644 helm/provisioner/tests/testdata/sa.yaml create mode 100644 helm/provisioner/values.yaml delete mode 100644 helm/templates/coder.yaml delete mode 100644 helm/tests/testdata/tls.golden delete mode 100644 helm/tests/testdata/workspace_proxy.golden diff --git a/.prettierignore b/.prettierignore index 9296d15d8802e..d68357703d7ce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -67,7 +67,7 @@ scaletest/terraform/secrets.tfvars # .prettierignore.include: # Helm templates contain variables that are invalid YAML and can't be formatted # by Prettier. -helm/templates/*.yaml +helm/**/templates/*.yaml # Terraform state files used in tests, these are automatically generated. # Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json diff --git a/.prettierignore.include b/.prettierignore.include index 1f60eda9c54a7..975c00ca21b84 100644 --- a/.prettierignore.include +++ b/.prettierignore.include @@ -1,6 +1,6 @@ # Helm templates contain variables that are invalid YAML and can't be formatted # by Prettier. -helm/templates/*.yaml +helm/**/templates/*.yaml # Terraform state files used in tests, these are automatically generated. # Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json diff --git a/Makefile b/Makefile index c9089a9d4e452..8bb681c9d4020 100644 --- a/Makefile +++ b/Makefile @@ -553,7 +553,7 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) ./scripts/apidocgen/generate.sh pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json -update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden +update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden .PHONY: update-golden-files cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go) @@ -564,8 +564,12 @@ enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update touch "$@" -helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.yaml) $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/tests/*_test.go) - go test ./helm/tests -run=TestUpdateGoldenFiles -update +helm/coder/tests/testdata/.gen-golden: $(wildcard helm/coder/tests/testdata/*.yaml) $(wildcard helm/coder/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/coder/tests/*_test.go) + go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update + touch "$@" + +helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/testdata/*.yaml) $(wildcard helm/provisioner/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/provisioner/tests/*_test.go) + go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update touch "$@" scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go) diff --git a/docs/admin/configure.md b/docs/admin/configure.md index 25f6d87583763..2240ef4ed5d62 100644 --- a/docs/admin/configure.md +++ b/docs/admin/configure.md @@ -42,7 +42,7 @@ If you are providing TLS certificates directly to the Coder server, either 1. Use a single certificate and key for both the root and wildcard domains. 2. Configure multiple certificates and keys via - [`coder.tls.secretNames`](https://github.com/coder/coder/blob/main/helm/values.yaml) in the Helm Chart, or + [`coder.tls.secretNames`](https://github.com/coder/coder/blob/main/helm/coder/values.yaml) in the Helm Chart, or [`--tls-cert-file`](../cli/server.md#--tls-cert-file) and [`--tls-key-file`](../cli/server.md#--tls-key-file) command line options (these both take a comma separated list of files; list certificates and their respective keys in the same order). diff --git a/docs/admin/scale.md b/docs/admin/scale.md index 5b5e2369f54fd..998314061fd52 100644 --- a/docs/admin/scale.md +++ b/docs/admin/scale.md @@ -42,7 +42,7 @@ Users accessing workspaces via SSH will consume fewer resources, as SSH connecti Workspace builds are CPU-intensive, as it relies on Terraform. Various [Terraform providers](https://registry.terraform.io/browse/providers) have different resource requirements. When tested with our [kubernetes](https://github.com/coder/coder/tree/main/examples/templates/kubernetes) template, `coderd` will consume roughly 0.25 cores per concurrent workspace build. -For effective provisioning, our helm chart prefers to schedule [one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/values.yaml#L188-L202). +For effective provisioning, our helm chart prefers to schedule [one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/coder/values.yaml#L188-L202). We recommend: diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md index ade4b058c2dcf..11e8138d7c6d6 100644 --- a/docs/install/kubernetes.md +++ b/docs/install/kubernetes.md @@ -105,7 +105,7 @@ to log in and manage templates. > You can view our > [Helm README](https://github.com/coder/coder/blob/main/helm#readme) for > details on the values that are available, or you can view the - > [values.yaml](https://github.com/coder/coder/blob/main/helm/values.yaml) + > [values.yaml](https://github.com/coder/coder/blob/main/helm/coder/values.yaml) > file directly. 1. Run the following command to install the chart in your cluster. diff --git a/helm/Makefile b/helm/Makefile index a3f689b1637af..4010cf42d64fb 100644 --- a/helm/Makefile +++ b/helm/Makefile @@ -13,6 +13,13 @@ all: lint lint: lint/helm .PHONY: lint -lint/helm: - helm lint --strict --set coder.image.tag=v0.0.1 . +lint/helm: lint/helm/coder lint/helm/provisioner .PHONY: lint/helm + +lint/helm/coder: + helm lint --strict --set coder.image.tag=v0.0.1 coder/ +.PHONY: lint/helm/coder + +lint/helm/provisioner: + helm lint --strict --set coder.image.tag=v0.0.1 provisioner/ +.PHONY: lint/helm/provisioner diff --git a/helm/.helmignore b/helm/coder/.helmignore similarity index 100% rename from helm/.helmignore rename to helm/coder/.helmignore diff --git a/helm/coder/Chart.lock b/helm/coder/Chart.lock new file mode 100644 index 0000000000000..9692722e192f1 --- /dev/null +++ b/helm/coder/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: libcoder + repository: file://../libcoder + version: 0.1.0 +digest: sha256:5c9a99109258073b590a9f98268490ef387fde24c0c7c7ade9c1a8c7ef5e6e10 +generated: "2023-08-08T07:27:19.677972411Z" diff --git a/helm/Chart.yaml b/helm/coder/Chart.yaml similarity index 85% rename from helm/Chart.yaml rename to helm/coder/Chart.yaml index a68aa330d8d49..99f6b710474c3 100644 --- a/helm/Chart.yaml +++ b/helm/coder/Chart.yaml @@ -21,9 +21,14 @@ keywords: - coder - terraform sources: - - https://github.com/coder/coder/tree/main/helm + - https://github.com/coder/coder/tree/main/helm/coder icon: https://helm.coder.com/coder_logo_black.png maintainers: - name: Coder Technologies, Inc. email: support@coder.com url: https://coder.com/contact + +dependencies: + - name: libcoder + version: 0.1.0 + repository: file://../libcoder diff --git a/helm/README.md b/helm/coder/README.md similarity index 100% rename from helm/README.md rename to helm/coder/README.md diff --git a/helm/coder/charts/libcoder-0.1.0.tgz b/helm/coder/charts/libcoder-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b799de7d008196ae4c4a948df1db26d2c5cd4cf5 GIT binary patch literal 2999 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 1.19.0-0" + +keywords: + - coder + - terraform +sources: + - https://github.com/coder/coder/tree/main/helm/provisioner +icon: https://helm.coder.com/coder_logo_black.png +maintainers: + - name: Coder Technologies, Inc. + email: support@coder.com + url: https://coder.com/contact + +dependencies: + - name: libcoder + version: 0.1.0 + repository: file://../libcoder diff --git a/helm/provisioner/charts/libcoder-0.1.0.tgz b/helm/provisioner/charts/libcoder-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..638d50f976a7eb18335f26a15c12769a83e9f412 GIT binary patch literal 2994 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 Date: Thu, 10 Aug 2023 12:08:00 +0200 Subject: [PATCH 076/156] refactor(cli): adjust parameter resolver (#9019) --- cli/parameterresolver.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/parameterresolver.go b/cli/parameterresolver.go index 9e803356b45bf..d2f551a17ff4f 100644 --- a/cli/parameterresolver.go +++ b/cli/parameterresolver.go @@ -77,11 +77,12 @@ func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCL } func (pr *ParameterResolver) resolveWithParametersMapFile(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { +next: for name, value := range pr.richParametersFile { for i, r := range resolved { if r.Name == name { resolved[i].Value = value - goto done + continue next } } @@ -89,34 +90,33 @@ func (pr *ParameterResolver) resolveWithParametersMapFile(resolved []codersdk.Wo Name: name, Value: value, }) - done: } return resolved } func (pr *ParameterResolver) resolveWithCommandLineOrEnv(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter { +nextRichParameter: for _, richParameter := range pr.richParameters { for i, r := range resolved { if r.Name == richParameter.Name { resolved[i].Value = richParameter.Value - goto richParameterDone + continue nextRichParameter } } resolved = append(resolved, richParameter) - richParameterDone: } +nextBuildOption: for _, buildOption := range pr.buildOptions { for i, r := range resolved { if r.Name == buildOption.Name { resolved[i].Value = buildOption.Value - goto buildOptionDone + continue nextBuildOption } } resolved = append(resolved, buildOption) - buildOptionDone: } return resolved } @@ -126,6 +126,7 @@ func (pr *ParameterResolver) resolveWithLastBuildParameters(resolved []codersdk. return resolved // don't pull parameters from last build } +next: for _, buildParameter := range pr.lastBuildParameters { tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters) if tvp == nil { @@ -143,12 +144,11 @@ func (pr *ParameterResolver) resolveWithLastBuildParameters(resolved []codersdk. for i, r := range resolved { if r.Name == buildParameter.Name { resolved[i].Value = buildParameter.Value - goto done + continue next } } resolved = append(resolved, buildParameter) - done: } return resolved } @@ -160,7 +160,7 @@ func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuil return xerrors.Errorf("parameter %q is not present in the template", r.Name) } - if tvp.Ephemeral && !pr.promptBuildOptions && len(pr.buildOptions) == 0 { + if tvp.Ephemeral && !pr.promptBuildOptions && findWorkspaceBuildParameter(tvp.Name, pr.buildOptions) == nil { return xerrors.Errorf("ephemeral parameter %q can be used only with --build-options or --build-option flag", r.Name) } @@ -173,12 +173,12 @@ func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuil func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) { for _, tvp := range templateVersionParameters { - p := findWorkspaceBuildParameter(tvp, resolved) + p := findWorkspaceBuildParameter(tvp.Name, resolved) if p != nil { continue } - firstTimeUse := pr.isFirstTimeUse(tvp) + firstTimeUse := pr.isFirstTimeUse(tvp.Name) if (tvp.Ephemeral && pr.promptBuildOptions) || tvp.Required || @@ -201,8 +201,8 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild return resolved, nil } -func (pr *ParameterResolver) isFirstTimeUse(tvp codersdk.TemplateVersionParameter) bool { - return findWorkspaceBuildParameter(tvp, pr.lastBuildParameters) == nil +func (pr *ParameterResolver) isFirstTimeUse(parameterName string) bool { + return findWorkspaceBuildParameter(parameterName, pr.lastBuildParameters) == nil } func findTemplateVersionParameter(workspaceBuildParameter codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) *codersdk.TemplateVersionParameter { @@ -214,9 +214,9 @@ func findTemplateVersionParameter(workspaceBuildParameter codersdk.WorkspaceBuil return nil } -func findWorkspaceBuildParameter(tvp codersdk.TemplateVersionParameter, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter { +func findWorkspaceBuildParameter(parameterName string, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter { for _, p := range params { - if p.Name == tvp.Name { + if p.Name == parameterName { return &p } } From 834ce41013d2c26e7ee51479abc3a7236dbe6d69 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 10 Aug 2023 09:41:35 -0300 Subject: [PATCH 077/156] refactor(site): add default background color to html and body (#9009) --- site/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/site/index.html b/site/index.html index fb62af8b53a2d..29cafbb453a46 100644 --- a/site/index.html +++ b/site/index.html @@ -36,6 +36,12 @@ href="/favicons/favicon.svg" data-react-helmet="true" /> + From 091c00bd702156bc8a00ab88c177762e53bf8d49 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Thu, 10 Aug 2023 15:59:39 +0300 Subject: [PATCH 078/156] fix: make preferred region the first in list (#9014) --- site/src/components/Resources/AgentLatency.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/site/src/components/Resources/AgentLatency.tsx b/site/src/components/Resources/AgentLatency.tsx index 04f790980c977..9be53f106cc2f 100644 --- a/site/src/components/Resources/AgentLatency.tsx +++ b/site/src/components/Resources/AgentLatency.tsx @@ -69,14 +69,9 @@ export const AgentLatency: FC<{ agent: WorkspaceAgent }> = ({ agent }) => { - {Object.keys(agent.latency).map((regionName) => { - if (!agent.latency) { - throw new Error("No latency found on agent") - } - - const region = agent.latency[regionName] - - return ( + {Object.entries(agent.latency) + .sort(([, a], [, b]) => (a.preferred ? -1 : b.preferred ? 1 : 0)) + .map(([regionName, region]) => ( = ({ agent }) => { {regionName} {Math.round(region.latency_ms)}ms - ) - })} + ))} From 967a4b0c7ce88cff5eef34b2ac61572762d71d3e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Aug 2023 16:36:18 +0200 Subject: [PATCH 079/156] feat: add example template using rich parameters (#9020) --- examples/parameters/README.md | 19 ++ examples/parameters/build/Dockerfile | 18 ++ examples/parameters/main.tf | 281 +++++++++++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 examples/parameters/README.md create mode 100644 examples/parameters/build/Dockerfile create mode 100644 examples/parameters/main.tf diff --git a/examples/parameters/README.md b/examples/parameters/README.md new file mode 100644 index 0000000000000..8ebd3ee3c8b50 --- /dev/null +++ b/examples/parameters/README.md @@ -0,0 +1,19 @@ +--- +name: Sample Template with Parameters +description: Review the sample template and introduce parameters to your template +tags: [local, docker, parameters] +icon: /icon/docker.png +--- + +# Overview + +This Coder template presents various features of [rich parameters](https://coder.com/docs/v2/latest/templates/parameters), including types, validation constraints, +mutability, ephemeral (one-time) parameters, etc. + +## Development + +Update the template and push it using the following command: + +```bash +./scripts/coder-dev.sh templates push examples-parameters -d examples/parameters --create +``` diff --git a/examples/parameters/build/Dockerfile b/examples/parameters/build/Dockerfile new file mode 100644 index 0000000000000..a443b5d07100e --- /dev/null +++ b/examples/parameters/build/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu + +RUN apt-get update \ + && apt-get install -y \ + curl \ + git \ + golang \ + sudo \ + vim \ + wget \ + && rm -rf /var/lib/apt/lists/* + +ARG USER=coder +RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \ + && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \ + && chmod 0440 /etc/sudoers.d/${USER} +USER ${USER} +WORKDIR /home/${USER} diff --git a/examples/parameters/main.tf b/examples/parameters/main.tf new file mode 100644 index 0000000000000..0903a2d2e6475 --- /dev/null +++ b/examples/parameters/main.tf @@ -0,0 +1,281 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 0.11.1" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.1" + } + } +} + +locals { + username = data.coder_workspace.me.owner +} + +data "coder_provisioner" "me" { +} + +provider "docker" { +} + +data "coder_workspace" "me" { +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script_timeout = 180 + startup_script = <<-EOT + set -e + + # install and start code-server + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0 + /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & + EOT +} + +resource "coder_app" "code-server" { + agent_id = coder_agent.main.id + slug = "code-server" + display_name = "code-server" + url = "http://localhost:13337/?folder=/home/${local.username}" + icon = "/icon/code.svg" + subdomain = false + share = "owner" + + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace.me.id}-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace.me.owner + } + labels { + label = "coder.owner_id" + value = data.coder_workspace.me.owner_id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +resource "docker_image" "main" { + name = "coder-${data.coder_workspace.me.id}" + build { + context = "./build" + build_args = { + USER = local.username + } + } + triggers = { + dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)])) + } +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.main.name + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] + env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/${local.username}" + volume_name = docker_volume.home_volume.name + read_only = false + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace.me.owner + } + labels { + label = "coder.owner_id" + value = data.coder_workspace.me.owner_id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} + +// Rich parameters +// See: https://coder.com/docs/v2/latest/templates/parameters + +data "coder_parameter" "project_id" { + name = "project_id" + display_name = "My Project ID" + icon = "/emojis/1fab5.png" + description = "Specify the project ID to deploy in workspace." + default = "A1B2C3" + mutable = true + validation { + regex = "^[A-Z0-9]+$" + error = "Project ID is incorrect" + } + + order = 1 +} + +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + icon = "/emojis/1f30e.png" + description = "Select the region in which you would like to deploy your workspace." + default = "eu-helsinki" + option { + icon = "/emojis/1f1fa-1f1f8.png" + name = "Pittsburgh" + description = "Pittsburgh is a city in the Commonwealth of Pennsylvania and the county seat of Allegheny County." + value = "us-pittsburgh" + } + option { + icon = "/emojis/1f1eb-1f1ee.png" + name = "Helsinki" + description = "Helsinki, the capital city of Finland, is renowned for its vibrant cultural scene, stunning waterfront architecture, and a harmonious blend of modernity and natural beauty." + value = "eu-helsinki" + } + option { + icon = "/emojis/1f1e6-1f1fa.png" + name = "Sydney" + description = "Sydney, the largest city in Australia, captivates with its iconic Sydney Opera House, picturesque harbor, and diverse neighborhoods, making it a captivating blend of urban sophistication and coastal charm." + value = "ap-sydney" + } + + order = 1 +} + +data "coder_parameter" "apps_dir" { + name = "apps_dir" + display_name = "Apps Directory" + icon = "/emojis/1f9ba.png" + type = "string" + description = "Specify the directory to install project applications and tools." + default = "/var/apps" + + order = 2 +} + +data "coder_parameter" "worker_instances" { + name = "worker_instances" + display_name = "Worker Instances" + icon = "/emojis/2697.png" + type = "number" + description = "Specify the number of worker instances to spawn." + default = "3" + mutable = true + validation { + min = 3 + max = 12 + monotonic = "increasing" + } + order = 2 +} + +data "coder_parameter" "security_groups" { + name = "security_groups" + display_name = "Security Groups" + icon = "/emojis/26f4.png" + type = "list(string)" + description = "Select relevant security groups." + mutable = true + default = jsonencode([ + "Web Server Security Group", + "Database Security Group", + "Backend Security Group" + ]) + order = 2 +} + +data "coder_parameter" "docker_image" { + name = "docker_image" + display_name = "Docker Image" + mutable = true + type = "string" + description = "Docker image for the development container" + default = "ghcr.io/coder/coder-preview:main" + + order = 3 +} + +data "coder_parameter" "command_line_args" { + name = "command_line_args" + display_name = "Extra command line args" + type = "string" + default = "" + description = "Provide extra command line args for the startup script." + mutable = true + order = 80 +} + +data "coder_parameter" "enable_monitoring" { + name = "enable_monitoring" + display_name = "Enable Workspace Monitoring" + type = "bool" + description = "This monitoring functionality empowers you to closely track the health and resource utilization of your instance in real-time." + mutable = true + order = 90 +} + +// Build options (ephemeral parameters) +// See: https://coder.com/docs/v2/latest/templates/parameters#ephemeral-parameters + +data "coder_parameter" "pause-startup" { + name = "pause-startup" + display_name = "Pause startup script" + type = "number" + description = "Pause the startup script (seconds)" + default = "1" + mutable = true + ephemeral = true + validation { + min = 0 + max = 300 + } + + order = 4 +} + +data "coder_parameter" "force-rebuild" { + name = "force-rebuild" + display_name = "Force rebuild project" + type = "bool" + description = "Rebuild the workspace project" + default = "false" + mutable = true + ephemeral = true + + order = 4 +} From 76ad116e12552ed76dcef9239347fabc37b99e81 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Thu, 10 Aug 2023 10:02:02 -0700 Subject: [PATCH 080/156] docs: write 2.0.2 changelog (#9025) * mention provisioner authentication * add changelog for 2.1.0 * rename to 2.0.2 --- docs/admin/provisioners.md | 10 +++++++--- docs/changelogs/v2.0.2.md | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 docs/changelogs/v2.0.2.md diff --git a/docs/admin/provisioners.md b/docs/admin/provisioners.md index 8f733fdccaa7d..6843a4e3efaad 100644 --- a/docs/admin/provisioners.md +++ b/docs/admin/provisioners.md @@ -16,10 +16,14 @@ By default, the Coder server runs [built-in provisioner daemons](../cli/server.m Each provisioner can run a single [concurrent workspace build](./scale.md#concurrent-workspace-builds). For example, running 30 provisioner containers will allow 30 users to start workspaces at the same time. -### Requirements +Provisioners are started with the [coder provisionerd start](../cli/provisionerd_start.md) command. -- The [Coder CLI](../cli.md) must installed on and authenticated as a user with the Owner or Template Admin role. -- Your environment must be [authenticated](../templates/authentication.md) against the cloud environments templates need to provision against. +### Authentication + +The provisioner server must authenticate with your Coder deployment. There are two authentication methods: + +- PSK: Set a [provisioner daemon PSK](../cli/server#--provisioner-daemon-psk) on the Coder server and start the provisioner with `coder provisionerd start --psk ` +- User token: [Authenticate](../cli.md#--token) the Coder CLI as a user with the Template Admin or Owner role. ### Types of provisioners diff --git a/docs/changelogs/v2.0.2.md b/docs/changelogs/v2.0.2.md new file mode 100644 index 0000000000000..01377772e2d48 --- /dev/null +++ b/docs/changelogs/v2.0.2.md @@ -0,0 +1,39 @@ +## Changelog + +### Features + +- [External provisioners](https://coder.com/docs/v2/latest/admin/provisioners) updates + - Added [PSK authentication](https://coder.com/docs/v2/latest/admin/provisioners#authentication) method (#8877) (@spikecurtis) + - Provisioner daemons can be deployed [via Helm](https://github.com/coder/coder/tree/main/helm/provisioner) (#8939) (@spikecurtis) +- Added login type (OIDC, GitHub, or built-in, or none) to users page (#8912) (@Emyrk) +- Groups can be [automatically created](https://coder.com/docs/v2/latest/admin/auth#user-not-being-assigned--group-does-not-exist) from OIDC group sync (#8884) (@Emyrk) +- Parameter values can be specified via the [command line](https://coder.com/docs/v2/latest/cli/create#--parameter) during workspace creation/updates (#8898) (@mtojek) +- Added date range picker for the template insights page (#8976) (@BrunoQuaresma) +- We now publish preview [container images](https://github.com/coder/coder/pkgs/container/coder-preview) on every commit to `main`. Only use these images for testing. They are automatically deleted after 7 days. +- Coder is [officially listed JetBrains Gateway](https://coder.com/blog/self-hosted-remote-development-in-jetbrains-ides-now-available-to-coder-users). + +### Bug fixes + +- Don't close other web terminal or `coder_app` sessions during a terminal close (#8917) +- Properly refresh OIDC tokens (#8950) (@Emyrk) +- Added backoff to validate fresh git auth tokens (#8956) (@kylecarbs) +- Make preferred region the first in list (#9014) (@matifali) +- `coder stat`: clistat: accept positional arg for stat disk cmd (#8911) +- Prompt for confirmation during `coder delete ` (#8579) +- Ensure SCIM create user can unsuspend (#8916) +- Set correct Prometheus port in Helm notes (#8888) +- Show user avatar on group page (#8997) (@BrunoQuaresma) +- Make deployment stats bar scrollable on smaller viewports (#8996) (@BrunoQuaresma) +- Add horizontal scroll to template viewer (#8998) (@BrunoQuaresma) +- Persist search parameters when user has to authenticate (#9005) (@BrunoQuaresma) +- Set default color and display error on appearance form (#9004) (@BrunoQuaresma) + +Compare: [`v2.0.1...v2.0.2`](https://github.com/coder/coder/compare/v2.0.1...v2.0.2) + +## Container image + +- `docker pull ghcr.io/coder/coder:v2.0.2` + +## Install/upgrade + +Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below. From 83061bef7e8ad2ff15fc85272e804e3f250b223d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 10 Aug 2023 14:47:56 -0300 Subject: [PATCH 081/156] refactor(site): add minor improvements to the port button (#9028) --- .../components/Resources/PortForwardButton.tsx | 15 ++++++++------- site/src/theme/theme.ts | 4 ++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx index 2e9089649e808..0209f4bd9a9c8 100644 --- a/site/src/components/Resources/PortForwardButton.tsx +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -58,9 +58,10 @@ export const PortForwardButton: React.FC = (props) => { sx={{ fontSize: 12, fontWeight: 500, - height: 16, - padding: (theme) => theme.spacing(0, 1), - borderRadius: 7, + height: 20, + minWidth: 20, + padding: (theme) => theme.spacing(0, 0.5), + borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", @@ -82,11 +83,11 @@ export const PortForwardButton: React.FC = (props) => { onClose={onClose} anchorOrigin={{ vertical: "bottom", - horizontal: "left", + horizontal: "right", }} transformOrigin={{ vertical: "top", - horizontal: "left", + horizontal: "right", }} > @@ -210,7 +211,7 @@ export const PortForwardPopoverView: React.FC< max={65535} required sx={{ - fontSize: 12, + fontSize: 14, height: 34, p: (theme) => theme.spacing(0, 1.5), background: "none", @@ -248,7 +249,7 @@ const useStyles = makeStyles((theme) => ({ padding: 0, width: theme.spacing(38), color: theme.palette.text.secondary, - marginTop: theme.spacing(0.25), + marginTop: theme.spacing(0.5), }, openUrlButton: { diff --git a/site/src/theme/theme.ts b/site/src/theme/theme.ts index 48adafe05408d..354ade445fee9 100644 --- a/site/src/theme/theme.ts +++ b/site/src/theme/theme.ts @@ -92,6 +92,10 @@ dark = createTheme(dark, { input:-webkit-autofill:active { -webkit-box-shadow: 0 0 0 100px ${dark.palette.background.default} inset !important; } + + ::placeholder { + color: ${dark.palette.text.disabled}; + } `, }, MuiAvatar: { From 175aed16858548287c3f965f3653f2bf457242b5 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 10 Aug 2023 15:23:31 -0300 Subject: [PATCH 082/156] feat(site): add tooltip showing the error in the failure badge (#9029) --- .../WorkspaceStatusBadge.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 2ace2d0b903b8..75a35f6839657 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -9,6 +9,10 @@ import { ImpendingDeletionText, } from "components/WorkspaceDeletion" import { getDisplayWorkspaceStatus } from "utils/workspace" +import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip" +import { styled } from "@mui/material/styles" +import Box from "@mui/material/Box" +import ErrorOutline from "@mui/icons-material/ErrorOutline" export type WorkspaceStatusBadgeProps = { workspace: Workspace @@ -28,6 +32,27 @@ export const WorkspaceStatusBadge: FC< + + + theme.palette.error.light, + }} + /> + {workspace.latest_build.job.error} +

2#;s?62+-~X4|4QVN5zT4=msBKq9bH} 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 O=OXap9NF;NAO8f#png^W diff --git a/docs/images/gateway/plugin-select-ide.png b/docs/images/gateway/plugin-select-ide.png index b138b7b2a41d5baf82459d662f3e62dc64a9b251..bc4e6cb1ee03be03762de09317e3777f01487152 100644 GIT binary patch literal 87207 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 diff --git a/docs/images/gateway/plugin-session-token.png b/docs/images/gateway/plugin-session-token.png index c233fd0c277598e31237caee0e85131691b55107..b13d2fe12828e6ae74d7e91dbf2f48509df0ef70 100644 GIT binary patch literal 135600 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ⅈ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�!sMLs|vCZJpNe`O4l zF^6OgMpjy<5ElPHm5(hem%7=uiglta90Wr;_OR}Vs;$=_pnN0wqG{71*^V>)I&H** zN(@*tHEq@&c^NjlX?msRUvqQ)_F)gx@Dixu!&@#;;VwmMmd>%${-z!NP~O

+ } + placement="top" + > +
+ +
+ +
@@ -66,6 +91,17 @@ export const WorkspaceStatusText: FC< ) } +const FailureTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.background.paperLight, + border: `1px solid ${theme.palette.divider}`, + fontSize: 12, + padding: theme.spacing(1, 1.25), + }, +})) + const useStyles = makeStyles((theme) => ({ root: { fontWeight: 600 }, "type-error": { From 25c6832772142e1f440a98b2faf6bb02cd3d4f12 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 10 Aug 2023 11:26:28 -0700 Subject: [PATCH 083/156] chore: update tailscale (#9027) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4ac487adf127c..be7e6ee88f9d9 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0 // There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here: // https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main -replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c +replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230810172417-9321478a5fbe // This is replaced to include a fix that causes a deadlock when closing the // wireguard network. diff --git a/go.sum b/go.sum index 3fc732489b5e3..46703cf471c41 100644 --- a/go.sum +++ b/go.sum @@ -220,8 +220,8 @@ github.com/coder/retry v1.4.0 h1:g0fojHFxcdgM3sBULqgjFDxw1UIvaCqk4ngUDu0EWag= github.com/coder/retry v1.4.0/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY= github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c h1:TI7TzdFI0UvQmwgyQhtI1HeyYNRxAQpr8Tw/rjT8VSA= github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ= -github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c h1:4NR1TCdxl6Dw2iQ37KDyvsLZgYYWvBSKCnsotofZrFg= -github.com/coder/tailscale v1.1.1-0.20230809183309-7a9c6c71e16c/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4= +github.com/coder/tailscale v1.1.1-0.20230810172417-9321478a5fbe h1:1Sbdy2U+ruuiL9hYQcJwhDlgjRKBBJbkabQ6r+BNXd4= +github.com/coder/tailscale v1.1.1-0.20230810172417-9321478a5fbe/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4= github.com/coder/terraform-provider-coder v0.11.1 h1:1sXcHfQrX8XhmLbtKxBED2lZ5jk3/ezBtaw6uVhpJZ4= github.com/coder/terraform-provider-coder v0.11.1/go.mod h1:UIfU3bYNeSzJJvHyJ30tEKjD6Z9utloI+HUM/7n94CY= github.com/coder/wgtunnel v0.1.5 h1:WP3sCj/3iJ34eKvpMQEp1oJHvm24RYh0NHbj1kfUKfs= From d2f22b063a3cf260e4d6a60870d57a97dbd4a673 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 10 Aug 2023 12:04:17 -0700 Subject: [PATCH 084/156] fix: move STUN servers into their own regions (#9030) --- cli/netcheck_test.go | 2 +- cli/testdata/coder_server_--help.golden | 15 +- cli/testdata/server-config.yaml.golden | 15 +- codersdk/deployment.go | 6 +- docs/cli/server.md | 36 +---- .../cli/testdata/coder_server_--help.golden | 15 +- enterprise/coderd/coderd.go | 48 ++---- enterprise/wsproxy/wsproxy_test.go | 146 ++---------------- tailnet/derpmap.go | 57 ++++--- tailnet/derpmap_test.go | 4 +- 10 files changed, 101 insertions(+), 243 deletions(-) diff --git a/cli/netcheck_test.go b/cli/netcheck_test.go index 890260c1a704e..be0b50e1a5bac 100644 --- a/cli/netcheck_test.go +++ b/cli/netcheck_test.go @@ -31,7 +31,7 @@ func TestNetcheck(t *testing.T) { require.NoError(t, json.Unmarshal(b, &report)) assert.True(t, report.Healthy) - require.Len(t, report.Regions, 1) + require.Len(t, report.Regions, 1+5) // 1 built-in region + 5 STUN regions by default for _, v := range report.Regions { require.Len(t, v.NodeReports, len(v.Region.Nodes)) } diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 4e1274bb7ad20..cd8fd309f1154 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -175,18 +175,15 @@ backed by Tailscale and WireGuard. --derp-server-enable bool, $CODER_DERP_SERVER_ENABLE (default: true) Whether to enable or disable the embedded DERP relay server. - --derp-server-region-code string, $CODER_DERP_SERVER_REGION_CODE (default: coder) - Region code to use for the embedded DERP server. - - --derp-server-region-id int, $CODER_DERP_SERVER_REGION_ID (default: 999) - Region ID to use for the embedded DERP server. - --derp-server-region-name string, $CODER_DERP_SERVER_REGION_NAME (default: Coder Embedded Relay) Region name that for the embedded DERP server. - --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302) - Addresses for STUN servers to establish P2P connections. Use special - value 'disable' to turn off STUN. + --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302) + Addresses for STUN servers to establish P2P connections. It's + recommended to have at least two STUN servers to give users the best + chance of connecting P2P to workspaces. Each STUN server will get it's + own DERP region, with region IDs starting at `--derp-server-region-id + + 1`. Use special value 'disable' to turn off STUN completely. Networking / HTTP Options --disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index f3149b31eec7f..27aeb41a61b6a 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -111,11 +111,20 @@ networking: # Region name that for the embedded DERP server. # (default: Coder Embedded Relay, type: string) regionName: Coder Embedded Relay - # Addresses for STUN servers to establish P2P connections. Use special value - # 'disable' to turn off STUN. - # (default: stun.l.google.com:19302, type: string-array) + # Addresses for STUN servers to establish P2P connections. It's recommended to + # have at least two STUN servers to give users the best chance of connecting P2P + # to workspaces. Each STUN server will get it's own DERP region, with region IDs + # starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn + # off STUN completely. + # (default: + # stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302, + # type: string-array) stunAddresses: - stun.l.google.com:19302 + - stun1.l.google.com:19302 + - stun2.l.google.com:19302 + - stun3.l.google.com:19302 + - stun4.l.google.com:19302 # An HTTP URL that is accessible by other replicas to relay DERP traffic. Required # for high availability. # (default: , type: url) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 96cb9d19516f3..a7dd2a5af50a0 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -733,6 +733,7 @@ when required by your organization's security policy.`, Value: &c.DERP.Server.RegionID, Group: &deploymentGroupNetworkingDERP, YAML: "regionID", + Hidden: true, // Does not apply to external proxies as this value is generated. }, { @@ -744,6 +745,7 @@ when required by your organization's security policy.`, Value: &c.DERP.Server.RegionCode, Group: &deploymentGroupNetworkingDERP, YAML: "regionCode", + Hidden: true, // Does not apply to external proxies as we use the proxy name. }, { @@ -759,10 +761,10 @@ when required by your organization's security policy.`, }, { Name: "DERP Server STUN Addresses", - Description: "Addresses for STUN servers to establish P2P connections. Use special value 'disable' to turn off STUN.", + Description: "Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get it's own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely.", Flag: "derp-server-stun-addresses", Env: "CODER_DERP_SERVER_STUN_ADDRESSES", - Default: "stun.l.google.com:19302", + Default: "stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302", Value: &c.DERP.Server.STUNAddresses, Group: &deploymentGroupNetworkingDERP, YAML: "stunAddresses", diff --git a/docs/cli/server.md b/docs/cli/server.md index 27658ee0d16e8..9c7a90893ac9e 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -129,28 +129,6 @@ URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custo Whether to enable or disable the embedded DERP relay server. -### --derp-server-region-code - -| | | -| ----------- | ------------------------------------------- | -| Type | string | -| Environment | $CODER_DERP_SERVER_REGION_CODE | -| YAML | networking.derp.regionCode | -| Default | coder | - -Region code to use for the embedded DERP server. - -### --derp-server-region-id - -| | | -| ----------- | ----------------------------------------- | -| Type | int | -| Environment | $CODER_DERP_SERVER_REGION_ID | -| YAML | networking.derp.regionID | -| Default | 999 | - -Region ID to use for the embedded DERP server. - ### --derp-server-region-name | | | @@ -174,14 +152,14 @@ An HTTP URL that is accessible by other replicas to relay DERP traffic. Required ### --derp-server-stun-addresses -| | | -| ----------- | ---------------------------------------------- | -| Type | string-array | -| Environment | $CODER_DERP_SERVER_STUN_ADDRESSES | -| YAML | networking.derp.stunAddresses | -| Default | stun.l.google.com:19302 | +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| Type | string-array | +| Environment | $CODER_DERP_SERVER_STUN_ADDRESSES | +| YAML | networking.derp.stunAddresses | +| Default | stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302 | -Addresses for STUN servers to establish P2P connections. Use special value 'disable' to turn off STUN. +Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get it's own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely. ### --default-quiet-hours-schedule diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 4e1274bb7ad20..cd8fd309f1154 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -175,18 +175,15 @@ backed by Tailscale and WireGuard. --derp-server-enable bool, $CODER_DERP_SERVER_ENABLE (default: true) Whether to enable or disable the embedded DERP relay server. - --derp-server-region-code string, $CODER_DERP_SERVER_REGION_CODE (default: coder) - Region code to use for the embedded DERP server. - - --derp-server-region-id int, $CODER_DERP_SERVER_REGION_ID (default: 999) - Region ID to use for the embedded DERP server. - --derp-server-region-name string, $CODER_DERP_SERVER_REGION_NAME (default: Coder Embedded Relay) Region name that for the embedded DERP server. - --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302) - Addresses for STUN servers to establish P2P connections. Use special - value 'disable' to turn off STUN. + --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302) + Addresses for STUN servers to establish P2P connections. It's + recommended to have at least two STUN servers to give users the best + chance of connecting P2P to workspaces. Each STUN server will get it's + own DERP region, with region IDs starting at `--derp-server-region-id + + 1`. Use special value 'disable' to turn off STUN completely. Networking / HTTP Options --disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 1679165750b16..de385209e44f4 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -594,7 +594,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { if initial, changed, enabled := featureChanged(codersdk.FeatureWorkspaceProxy); shouldUpdate(initial, changed, enabled) { if enabled { - fn := derpMapper(api.Logger, api.DeploymentValues, api.ProxyHealth) + fn := derpMapper(api.Logger, api.ProxyHealth) api.AGPL.DERPMapper.Store(&fn) } else { api.AGPL.DERPMapper.Store(nil) @@ -659,7 +659,7 @@ var ( lastDerpConflictLog time.Time ) -func derpMapper(logger slog.Logger, _ *codersdk.DeploymentValues, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap { +func derpMapper(logger slog.Logger, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap { return func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap { derpMap = derpMap.Clone() @@ -753,46 +753,22 @@ func derpMapper(logger slog.Logger, _ *codersdk.DeploymentValues, proxyHealth *p } } - var stunNodes []*tailcfg.DERPNode - // TODO(@dean): potentially re-enable this depending on impact - /* - if !cfg.DERP.Config.BlockDirect.Value() { - stunNodes, err = agpltailnet.STUNNodes(regionID, cfg.DERP.Server.STUNAddresses) - if err != nil { - // Log a warning if we haven't logged one in the last - // minute. - lastDerpConflictMutex.Lock() - shouldLog := lastDerpConflictLog.IsZero() || time.Since(lastDerpConflictLog) > time.Minute - if shouldLog { - lastDerpConflictLog = time.Now() - } - lastDerpConflictMutex.Unlock() - if shouldLog { - logger.Error(context.Background(), "failed to calculate STUN nodes", slog.Error(err)) - } - - // No continue because we can keep going. - stunNodes = []*tailcfg.DERPNode{} - } - } - */ - - nodes := append(stunNodes, &tailcfg.DERPNode{ - Name: fmt.Sprintf("%da", regionID), - RegionID: regionID, - HostName: u.Hostname(), - DERPPort: portInt, - STUNPort: -1, - ForceHTTP: u.Scheme == "http", - }) - derpMap.Regions[regionID] = &tailcfg.DERPRegion{ // EmbeddedRelay ONLY applies to the primary. EmbeddedRelay: false, RegionID: regionID, RegionCode: regionCode, RegionName: regionName, - Nodes: nodes, + Nodes: []*tailcfg.DERPNode{ + { + Name: fmt.Sprintf("%da", regionID), + RegionID: regionID, + HostName: u.Hostname(), + DERPPort: portInt, + STUNPort: -1, + ForceHTTP: u.Scheme == "http", + }, + }, } } diff --git a/enterprise/wsproxy/wsproxy_test.go b/enterprise/wsproxy/wsproxy_test.go index d9d4e1a96bc8d..3ba8463331d6a 100644 --- a/enterprise/wsproxy/wsproxy_test.go +++ b/enterprise/wsproxy/wsproxy_test.go @@ -27,7 +27,6 @@ import ( "github.com/coder/coder/enterprise/coderd/coderdenttest" "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" - "github.com/coder/coder/tailnet" "github.com/coder/coder/testutil" ) @@ -203,10 +202,10 @@ resourceLoop: connInfo, err := client.WorkspaceAgentConnectionInfo(ctx, agentID) require.NoError(t, err) - // There should be three DERP servers in the map: the primary, and each - // of the two running proxies. + // There should be three DERP regions in the map: the primary, and each + // of the two running proxies. Also the STUN-only regions. require.NotNil(t, connInfo.DERPMap) - require.Len(t, connInfo.DERPMap.Regions, 3) + require.Len(t, connInfo.DERPMap.Regions, 3+len(api.DeploymentValues.DERP.Server.STUNAddresses.Value())) var ( primaryRegion *tailcfg.DERPRegion @@ -230,6 +229,11 @@ resourceLoop: // The last region is never started, which means it's never healthy, // which means it's never added to the DERP map. + if len(r.Nodes) == 1 && r.Nodes[0].STUNOnly { + // Skip STUN-only regions. + continue + } + t.Fatalf("unexpected region: %+v", r) } @@ -270,11 +274,15 @@ resourceLoop: connInfo, err := client.WorkspaceAgentConnectionInfo(testutil.Context(t, testutil.WaitLong), agentID) require.NoError(t, err) require.NotNil(t, connInfo.DERPMap) - require.Len(t, connInfo.DERPMap.Regions, 3) + require.Len(t, connInfo.DERPMap.Regions, 3+len(api.DeploymentValues.DERP.Server.STUNAddresses.Value())) // Connect to each region. for _, r := range connInfo.DERPMap.Regions { r := r + if len(r.Nodes) == 1 && r.Nodes[0].STUNOnly { + // Skip STUN-only regions. + continue + } t.Run(r.RegionName, func(t *testing.T) { t.Parallel() @@ -311,134 +319,6 @@ resourceLoop: }) } -func TestDERPMapStunNodes(t *testing.T) { - t.Parallel() - // See: enterprise/coderd/coderd.go - t.Skip("STUN nodes are removed from proxy regions in the DERP map for now") - - deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{ - string(codersdk.ExperimentMoons), - "*", - } - stunAddresses := []string{ - "stun.l.google.com:19302", - "stun1.l.google.com:19302", - "stun2.l.google.com:19302", - "stun3.l.google.com:19302", - "stun4.l.google.com:19302", - } - deploymentValues.DERP.Server.STUNAddresses = stunAddresses - - client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - DeploymentValues: deploymentValues, - AppHostname: "*.primary.test.coder.com", - IncludeProvisionerDaemon: true, - RealIPConfig: &httpmw.RealIPConfig{ - TrustedOrigins: []*net.IPNet{{ - IP: net.ParseIP("127.0.0.1"), - Mask: net.CIDRMask(8, 32), - }}, - TrustedHeaders: []string{ - "CF-Connecting-IP", - }, - }, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureWorkspaceProxy: 1, - }, - }, - }) - t.Cleanup(func() { - _ = closer.Close() - }) - - // Create a running external proxy. - _ = coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{ - Name: "cool-proxy", - }) - - // Wait for both running proxies to become healthy. - require.Eventually(t, func() bool { - healthCtx := testutil.Context(t, testutil.WaitLong) - err := api.ProxyHealth.ForceUpdate(healthCtx) - if !assert.NoError(t, err) { - return false - } - - regions, err := client.Regions(healthCtx) - if !assert.NoError(t, err) { - return false - } - if !assert.Len(t, regions, 2) { - return false - } - - // All regions should be healthy. - for _, r := range regions { - if !r.Healthy { - return false - } - } - return true - }, testutil.WaitLong, testutil.IntervalMedium) - - // Get the DERP map and ensure that the built-in region and the proxy region - // both have the STUN nodes. - ctx := testutil.Context(t, testutil.WaitLong) - connInfo, err := client.WorkspaceAgentConnectionInfoGeneric(ctx) - require.NoError(t, err) - - // There should be two DERP servers in the map: the primary and the - // proxy. - require.NotNil(t, connInfo.DERPMap) - require.Len(t, connInfo.DERPMap.Regions, 2) - - var ( - primaryRegion *tailcfg.DERPRegion - proxyRegion *tailcfg.DERPRegion - ) - for _, r := range connInfo.DERPMap.Regions { - if r.EmbeddedRelay { - primaryRegion = r - continue - } - if r.RegionName == "cool-proxy" { - proxyRegion = r - continue - } - - t.Fatalf("unexpected region: %+v", r) - } - - // The primary region: - require.Equal(t, "Coder Embedded Relay", primaryRegion.RegionName) - require.Equal(t, "coder", primaryRegion.RegionCode) - require.Equal(t, 999, primaryRegion.RegionID) - require.True(t, primaryRegion.EmbeddedRelay) - require.Len(t, primaryRegion.Nodes, len(stunAddresses)+1) - - // The proxy region: - require.Equal(t, "cool-proxy", proxyRegion.RegionName) - require.Equal(t, "coder_cool-proxy", proxyRegion.RegionCode) - require.Equal(t, 10001, proxyRegion.RegionID) - require.False(t, proxyRegion.EmbeddedRelay) - require.Len(t, proxyRegion.Nodes, len(stunAddresses)+1) - - for _, region := range []*tailcfg.DERPRegion{primaryRegion, proxyRegion} { - stunNodes, err := tailnet.STUNNodes(region.RegionID, stunAddresses) - require.NoError(t, err) - require.Len(t, stunNodes, len(stunAddresses)) - - require.Equal(t, stunNodes, region.Nodes[:len(stunNodes)]) - - // The last node should be the Coder server. - require.NotZero(t, region.Nodes[len(region.Nodes)-1].DERPPort) - } -} - func TestDERPEndToEnd(t *testing.T) { t.Parallel() diff --git a/tailnet/derpmap.go b/tailnet/derpmap.go index aa79300801d71..f35e48156b768 100644 --- a/tailnet/derpmap.go +++ b/tailnet/derpmap.go @@ -13,11 +13,11 @@ import ( "tailscale.com/tailcfg" ) -func STUNNodes(regionID int, stunAddrs []string) ([]*tailcfg.DERPNode, error) { - nodes := []*tailcfg.DERPNode{} +func STUNRegions(baseRegionID int, stunAddrs []string) ([]*tailcfg.DERPRegion, error) { + regions := make([]*tailcfg.DERPRegion, 0, len(stunAddrs)) for index, stunAddr := range stunAddrs { if stunAddr == "disable" { - return []*tailcfg.DERPNode{}, nil + return []*tailcfg.DERPRegion{}, nil } host, rawPort, err := net.SplitHostPort(stunAddr) @@ -28,16 +28,24 @@ func STUNNodes(regionID int, stunAddrs []string) ([]*tailcfg.DERPNode, error) { if err != nil { return nil, xerrors.Errorf("parse port for %q: %w", stunAddr, err) } - nodes = append([]*tailcfg.DERPNode{{ - Name: fmt.Sprintf("%dstun%d", regionID, index), - RegionID: regionID, - HostName: host, - STUNOnly: true, - STUNPort: port, - }}, nodes...) + + regionID := baseRegionID + index + 1 + regions = append(regions, &tailcfg.DERPRegion{ + EmbeddedRelay: false, + RegionID: regionID, + RegionCode: fmt.Sprintf("coder_stun_%d", regionID), + RegionName: fmt.Sprintf("Coder STUN %d", regionID), + Nodes: []*tailcfg.DERPNode{{ + Name: fmt.Sprintf("%dstun0", regionID), + RegionID: regionID, + HostName: host, + STUNOnly: true, + STUNPort: port, + }}, + }) } - return nodes, nil + return regions, nil } // NewDERPMap constructs a DERPMap from a set of STUN addresses and optionally a remote @@ -52,13 +60,17 @@ func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []str stunAddrs = nil } + // stunAddrs only applies when a default region is set. Each STUN node gets + // it's own region ID because netcheck will only try a single STUN server in + // each region before canceling the region's STUN check. + addRegions := []*tailcfg.DERPRegion{} if region != nil { - stunNodes, err := STUNNodes(region.RegionID, stunAddrs) + addRegions = append(addRegions, region) + stunRegions, err := STUNRegions(region.RegionID, stunAddrs) if err != nil { - return nil, xerrors.Errorf("construct stun nodes: %w", err) + return nil, xerrors.Errorf("create stun regions: %w", err) } - - region.Nodes = append(stunNodes, region.Nodes...) + addRegions = append(addRegions, stunRegions...) } derpMap := &tailcfg.DERPMap{ @@ -89,13 +101,18 @@ func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []str return nil, xerrors.Errorf("unmarshal derpmap: %w", err) } } - if region != nil { - _, conflicts := derpMap.Regions[region.RegionID] - if conflicts { - return nil, xerrors.Errorf("the default region ID conflicts with a remote region from %q", remoteURL) + + // Add our custom regions to the DERP map. + if len(addRegions) > 0 { + for _, region := range addRegions { + _, conflicts := derpMap.Regions[region.RegionID] + if conflicts { + return nil, xerrors.Errorf("a default region ID %d (%s - %q) conflicts with a remote region from %q", region.RegionID, region.RegionCode, region.RegionName, remoteURL) + } + derpMap.Regions[region.RegionID] = region } - derpMap.Regions[region.RegionID] = region } + // Remove all STUNPorts from DERPy nodes, and fully remove all STUNOnly // nodes. if disableSTUN { diff --git a/tailnet/derpmap_test.go b/tailnet/derpmap_test.go index bc5205cc45cf4..07d1b11255c93 100644 --- a/tailnet/derpmap_test.go +++ b/tailnet/derpmap_test.go @@ -24,7 +24,9 @@ func TestNewDERPMap(t *testing.T) { Nodes: []*tailcfg.DERPNode{{}}, }, []string{"stun.google.com:2345"}, "", "", false) require.NoError(t, err) - require.Len(t, derpMap.Regions[1].Nodes, 2) + require.Len(t, derpMap.Regions, 2) + require.Len(t, derpMap.Regions[1].Nodes, 1) + require.Len(t, derpMap.Regions[2].Nodes, 1) }) t.Run("RemoteURL", func(t *testing.T) { t.Parallel() From a2d64c08c15d582a893772691fea554bf0c4649e Mon Sep 17 00:00:00 2001 From: timquinlan Date: Thu, 10 Aug 2023 17:06:10 -0400 Subject: [PATCH 085/156] docs: update helm values.yaml code snippet, put quote around boolean value (#9026) * updated helm values.yaml code snippet, put quote around boolean values and added comments showing that CODER_OAUTH2_GITHUB_ALLOW_EVERYONE and CODER_OAUTH2_GITHUB_ALLOW_EVERYONE are mutually exclusive * Update auth.md spotted and fixed minor typo --- docs/admin/auth.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/admin/auth.md b/docs/admin/auth.md index 794def7f67e9e..5d7b135f12a46 100644 --- a/docs/admin/auth.md +++ b/docs/admin/auth.md @@ -59,15 +59,17 @@ If deploying Coder via Helm, you can set the above environment variables in the coder: env: - name: CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS - value: true - - name: CODER_OAUTH2_GITHUB_ALLOWED_ORGS - value: "your-org" + value: "true" - name: CODER_OAUTH2_GITHUB_CLIENT_ID value: "533...des" - name: CODER_OAUTH2_GITHUB_CLIENT_SECRET value: "G0CSP...7qSM" - - name: CODER_OAUTH2_GITHUB_ALLOW_EVERYONE - value: true + # If setting allowed orgs, comment out CODER_OAUTH2_GITHUB_ALLOW_EVERYONE and its value + - name: CODER_OAUTH2_GITHUB_ALLOWED_ORGS + value: "your-org" + # If allowing everyone, comment out CODER_OAUTH2_GITHUB_ALLOWED_ORGS and it's value + #- name: CODER_OAUTH2_GITHUB_ALLOW_EVERYONE + # value: "true" ``` To upgrade Coder, run: From 6fd5344d0add5148396726b1a83abfa7828f38d3 Mon Sep 17 00:00:00 2001 From: ffais <42377700+ffais@users.noreply.github.com> Date: Fri, 11 Aug 2023 02:57:59 +0200 Subject: [PATCH 086/156] feat: add support for NodePort service type in Helm chart (#8993) * add support for NodePort service type in Helm chart * fix nodeport values * formatting & make update-golden-files * update-golden-files --------- Co-authored-by: Eric --- helm/coder/templates/service.yaml | 6 ++++++ helm/coder/tests/testdata/command.golden | 1 + helm/coder/tests/testdata/command_args.golden | 1 + helm/coder/tests/testdata/default_values.golden | 1 + helm/coder/tests/testdata/labels_annotations.golden | 1 + helm/coder/tests/testdata/provisionerd_psk.golden | 1 + helm/coder/tests/testdata/sa.golden | 1 + helm/coder/tests/testdata/tls.golden | 2 ++ helm/coder/tests/testdata/workspace_proxy.golden | 1 + helm/coder/values.yaml | 6 ++++++ 10 files changed, 21 insertions(+) diff --git a/helm/coder/templates/service.yaml b/helm/coder/templates/service.yaml index 60dd5fe931dfa..1881f992a695e 100644 --- a/helm/coder/templates/service.yaml +++ b/helm/coder/templates/service.yaml @@ -16,11 +16,17 @@ spec: port: 80 targetPort: "http" protocol: TCP + {{ if eq .Values.coder.service.type "NodePort" }} + nodePort: {{ .Values.coder.service.httpNodePort }} + {{ end }} {{- if eq (include "coder.tlsEnabled" .) "true" }} - name: "https" port: 443 targetPort: "https" protocol: TCP + {{ if eq .Values.coder.service.type "NodePort" }} + nodePort: {{ .Values.coder.service.httpsNodePort }} + {{ end }} {{- end }} {{- if eq "LoadBalancer" .Values.coder.service.type }} {{- with .Values.coder.service.loadBalancerIP }} diff --git a/helm/coder/tests/testdata/command.golden b/helm/coder/tests/testdata/command.golden index 852ee36330ed2..4e88c36d4641d 100644 --- a/helm/coder/tests/testdata/command.golden +++ b/helm/coder/tests/testdata/command.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/command_args.golden b/helm/coder/tests/testdata/command_args.golden index 98bce5214c48e..9e7a9a01ee27a 100644 --- a/helm/coder/tests/testdata/command_args.golden +++ b/helm/coder/tests/testdata/command_args.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/default_values.golden b/helm/coder/tests/testdata/default_values.golden index 36d9fa171b63e..ed02773c6f7bb 100644 --- a/helm/coder/tests/testdata/default_values.golden +++ b/helm/coder/tests/testdata/default_values.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/labels_annotations.golden b/helm/coder/tests/testdata/labels_annotations.golden index b0edb4346f191..38812ffeab832 100644 --- a/helm/coder/tests/testdata/labels_annotations.golden +++ b/helm/coder/tests/testdata/labels_annotations.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/provisionerd_psk.golden b/helm/coder/tests/testdata/provisionerd_psk.golden index f8cfe550eefff..4dcde1eabe0fc 100644 --- a/helm/coder/tests/testdata/provisionerd_psk.golden +++ b/helm/coder/tests/testdata/provisionerd_psk.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/sa.golden b/helm/coder/tests/testdata/sa.golden index 940b761dd3f79..cf3b2df693835 100644 --- a/helm/coder/tests/testdata/sa.golden +++ b/helm/coder/tests/testdata/sa.golden @@ -91,6 +91,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/tls.golden b/helm/coder/tests/testdata/tls.golden index 75f0794a7945d..fccbbec0a2aa2 100644 --- a/helm/coder/tests/testdata/tls.golden +++ b/helm/coder/tests/testdata/tls.golden @@ -90,10 +90,12 @@ spec: port: 80 targetPort: "http" protocol: TCP + - name: "https" port: 443 targetPort: "https" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/tests/testdata/workspace_proxy.golden b/helm/coder/tests/testdata/workspace_proxy.golden index 6d03e49ff794e..096b40978aac0 100644 --- a/helm/coder/tests/testdata/workspace_proxy.golden +++ b/helm/coder/tests/testdata/workspace_proxy.golden @@ -90,6 +90,7 @@ spec: port: 80 targetPort: "http" protocol: TCP + externalTrafficPolicy: "Cluster" selector: app.kubernetes.io/name: coder diff --git a/helm/coder/values.yaml b/helm/coder/values.yaml index f6b43e4ee4dd0..2b85b54e67127 100644 --- a/helm/coder/values.yaml +++ b/helm/coder/values.yaml @@ -241,6 +241,12 @@ coder: # coder.service.annotations -- The service annotations. See: # https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer annotations: {} + # coder.service.httpNodePort -- Enabled if coder.service.type is set to NodePort. + # If not set, Kubernetes will allocate a port from the default range, 30000-32767. + httpNodePort: "" + # coder.service.httpsNodePort -- Enabled if coder.service.type is set to NodePort. + # If not set, Kubernetes will allocate a port from the default range, 30000-32767. + httpsNodePort: "" # coder.ingress -- The Ingress object to expose for Coder. ingress: From 40f3fc3a1c7ef613537be2b4064d513c4cc28925 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 10 Aug 2023 20:04:35 -0500 Subject: [PATCH 087/156] feat: allow creating manual oidc/github based users (#9000) * feat: allow creating manual oidc/github based users * Add unit test for oidc and no login type create --- cli/testdata/coder_users_create_--help.golden | 12 +-- cli/usercreate.go | 44 +++++++-- coderd/apidoc/docs.go | 12 ++- coderd/apidoc/swagger.json | 13 ++- coderd/coderdtest/coderdtest.go | 16 ++-- coderd/userauth_test.go | 18 +++- coderd/users.go | 40 ++++++--- coderd/users_test.go | 66 ++++++++++++++ codersdk/apikey.go | 1 + codersdk/users.go | 5 +- docs/api/audit.md | 2 +- docs/api/authorization.md | 4 +- docs/api/enterprise.md | 23 ++--- docs/api/schemas.md | 41 +++++---- docs/api/users.md | 21 ++--- docs/cli/users_create.md | 14 +-- scripts/develop.sh | 2 +- site/src/api/typesGenerated.ts | 4 +- .../CreateUserForm/CreateUserForm.tsx | 89 ++++++++++++++++++- .../CreateUserPage/CreateUserPage.test.tsx | 11 ++- .../CreateUserPage/CreateUserPage.tsx | 10 +++ 21 files changed, 355 insertions(+), 93 deletions(-) diff --git a/cli/testdata/coder_users_create_--help.golden b/cli/testdata/coder_users_create_--help.golden index bb94cac633bc0..275e89803d4c6 100644 --- a/cli/testdata/coder_users_create_--help.golden +++ b/cli/testdata/coder_users_create_--help.golden @@ -1,15 +1,15 @@ Usage: coder users create [flags] Options - --disable-login bool - Disabling login for a user prevents the user from authenticating via - password or IdP login. Authentication requires an API key/token - generated by an admin. Be careful when using this flag as it can lock - the user out of their account. - -e, --email string Specifies an email address for the new user. + --login-type string + Optionally specify the login type for the user. Valid values are: + password, none, github, oidc. Using 'none' prevents the user from + authenticating and requires an API key/token to be generated by an + admin. + -p, --password string Specifies a password for the new user. diff --git a/cli/usercreate.go b/cli/usercreate.go index b38bbb2d6401f..80118d7fced0e 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/go-playground/validator/v10" "golang.org/x/xerrors" @@ -18,6 +19,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd { username string password string disableLogin bool + loginType string ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -54,7 +56,18 @@ func (r *RootCmd) userCreate() *clibase.Cmd { return err } } - if password == "" && !disableLogin { + userLoginType := codersdk.LoginTypePassword + if disableLogin && loginType != "" { + return xerrors.New("You cannot specify both --disable-login and --login-type") + } + if disableLogin { + userLoginType = codersdk.LoginTypeNone + } else if loginType != "" { + userLoginType = codersdk.LoginType(loginType) + } + + if password == "" && userLoginType == codersdk.LoginTypePassword { + // Generate a random password password, err = cryptorand.StringCharset(cryptorand.Human, 20) if err != nil { return err @@ -66,14 +79,22 @@ func (r *RootCmd) userCreate() *clibase.Cmd { Username: username, Password: password, OrganizationID: organization.ID, - DisableLogin: disableLogin, + UserLoginType: userLoginType, }) if err != nil { return err } - authenticationMethod := `Your password is: ` + cliui.DefaultStyles.Field.Render(password) - if disableLogin { + + authenticationMethod := "" + switch codersdk.LoginType(strings.ToLower(string(userLoginType))) { + case codersdk.LoginTypePassword: + authenticationMethod = `Your password is: ` + cliui.DefaultStyles.Field.Render(password) + case codersdk.LoginTypeNone: authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate." + case codersdk.LoginTypeGithub: + authenticationMethod = `Login is authenticated through GitHub.` + case codersdk.LoginTypeOIDC: + authenticationMethod = `Login is authenticated through the configured OIDC provider.` } _, _ = fmt.Fprintln(inv.Stderr, `A new user has been created! @@ -111,11 +132,22 @@ Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`) Value: clibase.StringOf(&password), }, { - Flag: "disable-login", - Description: "Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " + + Flag: "disable-login", + Hidden: true, + Description: "Deprecated: Use '--login-type=none'. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " + "Be careful when using this flag as it can lock the user out of their account.", Value: clibase.BoolOf(&disableLogin), }, + { + Flag: "login-type", + Description: fmt.Sprintf("Optionally specify the login type for the user. Valid values are: %s. "+ + "Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.", + strings.Join([]string{ + string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC), + }, ", ", + )), + Value: clibase.StringOf(&loginType), + }, } return cmd } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6d590b02d4904..2f04b8be2a3d2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7536,13 +7536,21 @@ const docTemplate = `{ ], "properties": { "disable_login": { - "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.", + "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.", "type": "boolean" }, "email": { "type": "string", "format": "email" }, + "login_type": { + "description": "UserLoginType defaults to LoginTypePassword.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.LoginType" + } + ] + }, "organization_id": { "type": "string", "format": "uuid" @@ -8449,6 +8457,7 @@ const docTemplate = `{ "codersdk.LoginType": { "type": "string", "enum": [ + "", "password", "github", "oidc", @@ -8456,6 +8465,7 @@ const docTemplate = `{ "none" ], "x-enum-varnames": [ + "LoginTypeUnknown", "LoginTypePassword", "LoginTypeGithub", "LoginTypeOIDC", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 09aef2e0872b4..997bdca3ade64 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6715,13 +6715,21 @@ "required": ["email", "username"], "properties": { "disable_login": { - "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.", + "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.", "type": "boolean" }, "email": { "type": "string", "format": "email" }, + "login_type": { + "description": "UserLoginType defaults to LoginTypePassword.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.LoginType" + } + ] + }, "organization_id": { "type": "string", "format": "uuid" @@ -7576,8 +7584,9 @@ }, "codersdk.LoginType": { "type": "string", - "enum": ["password", "github", "oidc", "token", "none"], + "enum": ["", "password", "github", "oidc", "token", "none"], "x-enum-varnames": [ + "LoginTypeUnknown", "LoginTypePassword", "LoginTypeGithub", "LoginTypeOIDC", diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 5e2e55d5c032f..04470509682e2 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -588,14 +588,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI require.NoError(t, err) var sessionToken string - if !req.DisableLogin { - login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ - Email: req.Email, - Password: req.Password, - }) - require.NoError(t, err) - sessionToken = login.SessionToken - } else { + if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone { // Cannot log in with a disabled login user. So make it an api key from // the client making this user. token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{ @@ -605,6 +598,13 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI }) require.NoError(t, err) sessionToken = token.Key + } else { + login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ + Email: req.Email, + Password: req.Password, + }) + require.NoError(t, err) + sessionToken = login.SessionToken } if user.Status == codersdk.UserStatusDormant { diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index efa7673890863..8910bce286818 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -145,7 +145,7 @@ func TestUserLogin(t *testing.T) { require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) // Password auth should fail if the user is made without password login. - t.Run("LoginTypeNone", func(t *testing.T) { + t.Run("DisableLoginDeprecatedField", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) @@ -160,6 +160,22 @@ func TestUserLogin(t *testing.T) { }) require.Error(t, err) }) + + t.Run("LoginTypeNone", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) { + r.Password = "" + r.UserLoginType = codersdk.LoginTypeNone + }) + + _, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ + Email: anotherUser.Email, + Password: "SomeSecurePassword!", + }) + require.Error(t, err) + }) } func TestUserAuthMethods(t *testing.T) { diff --git a/coderd/users.go b/coderd/users.go index b34b447b8c456..29a6d56015369 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -287,11 +287,27 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { return } + if req.UserLoginType == "" && req.DisableLogin { + // Handle the deprecated field + req.UserLoginType = codersdk.LoginTypeNone + } + if req.UserLoginType == "" { + // Default to password auth + req.UserLoginType = codersdk.LoginTypePassword + } + + if req.UserLoginType != codersdk.LoginTypePassword && req.Password != "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Password cannot be set for non-password (%q) authentication.", req.UserLoginType), + }) + return + } + // If password auth is disabled, don't allow new users to be // created with a password! - if api.DeploymentValues.DisablePasswordAuth { + if api.DeploymentValues.DisablePasswordAuth && req.UserLoginType == codersdk.LoginTypePassword { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "You cannot manually provision new users with password authentication disabled!", + Message: "Password based authentication is disabled! Unable to provision new users with password authentication.", }) return } @@ -353,17 +369,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { } } - if req.DisableLogin && req.Password != "" { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Cannot set password when disabling login.", - }) - return - } - var loginType database.LoginType - if req.DisableLogin { + switch req.UserLoginType { + case codersdk.LoginTypeNone: loginType = database.LoginTypeNone - } else { + case codersdk.LoginTypePassword: err = userpassword.Validate(req.Password) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -376,6 +386,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { return } loginType = database.LoginTypePassword + case codersdk.LoginTypeOIDC: + loginType = database.LoginTypeOIDC + case codersdk.LoginTypeGithub: + loginType = database.LoginTypeGithub + default: + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Unsupported login type %q for manually creating new users.", req.UserLoginType), + }) } user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{ diff --git a/coderd/users_test.go b/coderd/users_test.go index 9b130133cd58a..c2564e2ff8c29 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/golang-jwt/jwt" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -566,6 +567,71 @@ func TestPostUsers(t *testing.T) { } } }) + + t.Run("CreateNoneLoginType", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + OrganizationID: first.OrganizationID, + Email: "another@user.org", + Username: "someone-else", + Password: "", + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + + found, err := client.User(ctx, user.ID.String()) + require.NoError(t, err) + require.Equal(t, found.LoginType, codersdk.LoginTypeNone) + }) + + t.Run("CreateOIDCLoginType", func(t *testing.T) { + t.Parallel() + email := "another@user.org" + conf := coderdtest.NewOIDCConfig(t, "") + config := conf.OIDCConfig(t, jwt.MapClaims{ + "email": email, + }) + config.AllowSignups = false + config.IgnoreUserInfo = true + + client := coderdtest.New(t, &coderdtest.Options{ + OIDCConfig: config, + }) + first := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + OrganizationID: first.OrganizationID, + Email: email, + Username: "someone-else", + Password: "", + UserLoginType: codersdk.LoginTypeOIDC, + }) + require.NoError(t, err) + + // Try to log in with OIDC. + userClient := codersdk.New(client.URL) + resp := oidcCallback(t, userClient, conf.EncodeClaims(t, jwt.MapClaims{ + "email": email, + })) + require.Equal(t, resp.StatusCode, http.StatusTemporaryRedirect) + // Set the client to use this OIDC context + authCookie := authCookieValue(resp.Cookies()) + userClient.SetSessionToken(authCookie) + _ = resp.Body.Close() + + found, err := userClient.User(ctx, "me") + require.NoError(t, err) + require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC) + }) } func TestUpdateUserProfile(t *testing.T) { diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 514b519f5ffda..32c97cf538417 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -28,6 +28,7 @@ type APIKey struct { type LoginType string const ( + LoginTypeUnknown LoginType = "" LoginTypePassword LoginType = "password" LoginTypeGithub LoginType = "github" LoginTypeOIDC LoginType = "oidc" diff --git a/codersdk/users.go b/codersdk/users.go index daeefee5f12bf..c11846ebdac2b 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -78,9 +78,12 @@ type CreateFirstUserResponse struct { type CreateUserRequest struct { Email string `json:"email" validate:"required,email" format:"email"` Username string `json:"username" validate:"required,username"` - Password string `json:"password" validate:"required_if=DisableLogin false"` + Password string `json:"password"` + // UserLoginType defaults to LoginTypePassword. + UserLoginType LoginType `json:"login_type"` // DisableLogin sets the user's login type to 'none'. This prevents the user // from being able to use a password or any other authentication method to login. + // Deprecated: Set UserLoginType=LoginTypeDisabled instead. DisableLogin bool `json:"disable_login"` OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"` } diff --git a/docs/api/audit.md b/docs/api/audit.md index d5aeb78665d31..5efe1f3410809 100644 --- a/docs/api/audit.md +++ b/docs/api/audit.md @@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?q=string \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { diff --git a/docs/api/authorization.md b/docs/api/authorization.md index d57a5e7542c35..17fc2e81d2299 100644 --- a/docs/api/authorization.md +++ b/docs/api/authorization.md @@ -129,7 +129,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \ ```json { "password": "string", - "to_type": "password" + "to_type": "" } ``` @@ -148,7 +148,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \ { "expires_at": "2019-08-24T14:15:22Z", "state_string": "string", - "to_type": "password", + "to_type": "", "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" } ``` diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index fc887cd12b6e3..15ba8c12b4ea3 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -183,7 +183,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -245,7 +245,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -307,7 +307,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -444,7 +444,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -502,6 +502,7 @@ Status Code **200** | Property | Value | | ------------ | ----------- | +| `login_type` | `` | | `login_type` | `password` | | `login_type` | `github` | | `login_type` | `oidc` | @@ -562,7 +563,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -625,7 +626,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -988,7 +989,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1040,7 +1041,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "role": "admin", "roles": [ @@ -1086,6 +1087,7 @@ Status Code **200** | Property | Value | | ------------ | ----------- | +| `login_type` | `` | | `login_type` | `password` | | `login_type` | `github` | | `login_type` | `oidc` | @@ -1197,7 +1199,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1222,7 +1224,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1278,6 +1280,7 @@ Status Code **200** | Property | Value | | ------------ | ----------- | +| `login_type` | `` | | `login_type` | `password` | | `login_type` | `github` | | `login_type` | `oidc` | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index cfcf28701ac1f..11aad4ba5f814 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -792,7 +792,7 @@ _None_ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -817,7 +817,7 @@ _None_ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1086,7 +1086,7 @@ _None_ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1163,7 +1163,7 @@ _None_ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1388,7 +1388,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { "password": "string", - "to_type": "password" + "to_type": "" } ``` @@ -1665,6 +1665,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "disable_login": true, "email": "user@example.com", + "login_type": "", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "password": "string", "username": "string" @@ -1673,13 +1674,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. | -| `email` | string | true | | | -| `organization_id` | string | false | | | -| `password` | string | false | | | -| `username` | string | true | | | +| Name | Type | Required | Restrictions | Description | +| ----------------- | ---------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. Deprecated: Set UserLoginType=LoginTypeDisabled instead. | +| `email` | string | true | | | +| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. | +| `organization_id` | string | false | | | +| `password` | string | false | | | +| `username` | string | true | | | ## codersdk.CreateWorkspaceBuildRequest @@ -2752,7 +2754,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -2970,7 +2972,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -3188,7 +3190,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ## codersdk.LoginType ```json -"password" +"" ``` ### Properties @@ -3197,6 +3199,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | Value | | ---------- | +| `` | | `password` | | `github` | | `oidc` | @@ -3305,7 +3308,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "expires_at": "2019-08-24T14:15:22Z", "state_string": "string", - "to_type": "password", + "to_type": "", "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" } ``` @@ -4563,7 +4566,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "role": "admin", "roles": [ @@ -5071,7 +5074,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -5196,7 +5199,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { - "login_type": "password" + "login_type": "" } ``` diff --git a/docs/api/users.md b/docs/api/users.md index 3c583e15787db..fdeed691da48f 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -36,7 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/users \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -79,6 +79,7 @@ curl -X POST http://coder-server:8080/api/v2/users \ { "disable_login": true, "email": "user@example.com", + "login_type": "", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "password": "string", "username": "string" @@ -102,7 +103,7 @@ curl -X POST http://coder-server:8080/api/v2/users \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -360,7 +361,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -411,7 +412,7 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -821,7 +822,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/login-type \ ```json { - "login_type": "password" + "login_type": "" } ``` @@ -1005,7 +1006,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1056,7 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1117,7 +1118,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1168,7 +1169,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { @@ -1219,7 +1220,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \ "email": "user@example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_seen_at": "2019-08-24T14:15:22Z", - "login_type": "password", + "login_type": "", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "roles": [ { diff --git a/docs/cli/users_create.md b/docs/cli/users_create.md index 2eb78318ffa0a..b89ff2aeb6d45 100644 --- a/docs/cli/users_create.md +++ b/docs/cli/users_create.md @@ -10,21 +10,21 @@ coder users create [flags] ## Options -### --disable-login +### -e, --email -| | | -| ---- | ----------------- | -| Type | bool | +| | | +| ---- | ------------------- | +| Type | string | -Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. Be careful when using this flag as it can lock the user out of their account. +Specifies an email address for the new user. -### -e, --email +### --login-type | | | | ---- | ------------------- | | Type | string | -Specifies an email address for the new user. +Optionally specify the login type for the user. Valid values are: password, none, github, oidc. Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin. ### -p, --password diff --git a/scripts/develop.sh b/scripts/develop.sh index 671c46a0bd5cc..cc1ab23f0554e 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -131,7 +131,7 @@ fatal() { trap 'fatal "Script encountered an error"' ERR cdroot - start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true --experiments "*,moons" "$@" + start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true "$@" echo '== Waiting for Coder to become ready' # Start the timeout in the background so interrupting this script diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c1aa96d872994..019fc0f60beb0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -247,6 +247,7 @@ export interface CreateUserRequest { readonly email: string readonly username: string readonly password: string + readonly login_type: LoginType readonly disable_login: boolean readonly organization_id: string } @@ -1674,8 +1675,9 @@ export type LogSource = "provisioner" | "provisioner_daemon" export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] // From codersdk/apikey.go -export type LoginType = "github" | "none" | "oidc" | "password" | "token" +export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token" export const LoginTypes: LoginType[] = [ + "", "github", "none", "oidc", diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index c2f03155e7c62..6270f0ca88799 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -13,6 +13,7 @@ import { FullPageForm } from "../FullPageForm/FullPageForm" import { Stack } from "../Stack/Stack" import { ErrorAlert } from "components/Alert/ErrorAlert" import { hasApiFieldErrors, isApiError } from "api/errors" +import MenuItem from "@mui/material/MenuItem" export const Language = { emailLabel: "Email", @@ -31,6 +32,7 @@ export interface CreateUserFormProps { error?: unknown isLoading: boolean myOrgId: string + authMethods?: TypesGen.AuthMethods } const validationSchema = Yup.object({ @@ -38,13 +40,31 @@ const validationSchema = Yup.object({ .trim() .email(Language.emailInvalid) .required(Language.emailRequired), - password: Yup.string().required(Language.passwordRequired), + password: Yup.string().when("login_type", { + is: "password", + then: (schema) => schema.required(Language.passwordRequired), + otherwise: (schema) => schema, + }), username: nameValidator(Language.usernameLabel), }) +const authMethodSelect = ( + title: string, + value: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- future will use this + description: string, +) => { + return ( + + {title} + {/* TODO: Add description */} + + ) +} + export const CreateUserForm: FC< React.PropsWithChildren -> = ({ onSubmit, onCancel, error, isLoading, myOrgId }) => { +> = ({ onSubmit, onCancel, error, isLoading, myOrgId, authMethods }) => { const form: FormikContextType = useFormik({ initialValues: { @@ -53,6 +73,7 @@ export const CreateUserForm: FC< username: "", organization_id: myOrgId, disable_login: false, + login_type: "password", }, validationSchema, onSubmit, @@ -62,6 +83,42 @@ export const CreateUserForm: FC< error, ) + const methods = [] + if (authMethods?.password.enabled) { + methods.push( + authMethodSelect( + "Password", + "password", + "User can provide their email and password to login.", + ), + ) + } + if (authMethods?.oidc.enabled) { + methods.push( + authMethodSelect( + "OpenID Connect", + "oidc", + "Uses an OpenID connect provider to authenticate the user.", + ), + ) + } + if (authMethods?.github.enabled) { + methods.push( + authMethodSelect( + "Github", + "github", + "Uses github oauth to authenticate the user.", + ), + ) + } + methods.push( + authMethodSelect( + "None", + "none", + "User authentication is disabled. This user an only be used if an api token is created for them.", + ), + ) + return ( {isApiError(error) && !hasApiFieldErrors(error) && ( @@ -85,13 +142,39 @@ export const CreateUserForm: FC< label={Language.emailLabel} /> + { + if (e.target.value !== "password") { + await form.setFieldValue("password", "") + } + await form.setFieldValue("login_type", e.target.value) + }} + > + {methods} + diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index d342ace9bdf75..ceeb30528d4f3 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -21,7 +21,7 @@ const renderCreateUserPage = async () => { const fillForm = async ({ username = "someuser", email = "someone@coder.com", - password = "password", + password = "SomeSecurePassword!", }: { username?: string email?: string @@ -29,10 +29,15 @@ const fillForm = async ({ }) => { const usernameField = screen.getByLabelText(FormLanguage.usernameLabel) const emailField = screen.getByLabelText(FormLanguage.emailLabel) - const passwordField = screen.getByLabelText(FormLanguage.passwordLabel) + const passwordField = screen + .getByTestId("password-input") + .querySelector("input") + + const loginTypeField = screen.getByTestId("login-type-input") await userEvent.type(usernameField, username) await userEvent.type(emailField, email) - await userEvent.type(passwordField, password) + await userEvent.type(loginTypeField, "password") + await userEvent.type(passwordField as HTMLElement, password) const submitButton = await screen.findByText( FooterLanguage.defaultSubmitLabel, ) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index cd905c39d88ae..cd92b6bc8141e 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -8,6 +8,8 @@ import * as TypesGen from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { Margins } from "../../../components/Margins/Margins" import { pageTitle } from "../../../utils/page" +import { getAuthMethods } from "api/api" +import { useQuery } from "@tanstack/react-query" export const Language = { unknownError: "Oops, an unknown error occurred.", @@ -25,6 +27,13 @@ export const CreateUserPage: FC = () => { }) const { error } = createUserState.context + // TODO: We should probably place this somewhere else to reduce the number of calls. + // This would be called each time this page is loaded. + const { data: authMethods } = useQuery({ + queryKey: ["authMethods"], + queryFn: getAuthMethods, + }) + return ( @@ -33,6 +42,7 @@ export const CreateUserPage: FC = () => { createUserSend({ type: "CREATE", user }) } From 4d8152d54324ef5374cfd2737723bb04e47d6876 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 11 Aug 2023 13:48:24 +0300 Subject: [PATCH 088/156] ci: run tests intelligently based on changes between consecutive commits (#9017) --- .github/workflows/ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8220492528b9a..8c3a3d441e2f1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,10 +2,6 @@ name: ci on: push: - branches: - - main - - pull_request: workflow_dispatch: permissions: @@ -47,6 +43,7 @@ jobs: uses: dorny/paths-filter@v2 id: filter with: + base: ${{ github.ref }} filters: | all: - "**" From 114ad4624e2408c19f87bdb7afd4210a785b966a Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 11 Aug 2023 13:49:23 +0300 Subject: [PATCH 089/156] ci: upgrade pr deployments workflow (#8924) --- .github/pr-deployments/certificate.yaml | 13 + .github/pr-deployments/rbac.yaml | 31 +++ .github/pr-deployments/template/main.tf | 313 +++++++++++++++++++++ .github/pr-deployments/values.yaml | 38 +++ .github/workflows/pr-deploy.yaml | 345 +++++++++++------------- docs/CONTRIBUTING.md | 5 +- docs/images/deploy-pr-manually.png | Bin 0 -> 13583 bytes docs/images/pr-deploy-manual.png | Bin 27155 -> 0 bytes scripts/deploy-pr.sh | 35 ++- 9 files changed, 574 insertions(+), 206 deletions(-) create mode 100644 .github/pr-deployments/certificate.yaml create mode 100644 .github/pr-deployments/rbac.yaml create mode 100644 .github/pr-deployments/template/main.tf create mode 100644 .github/pr-deployments/values.yaml create mode 100644 docs/images/deploy-pr-manually.png delete mode 100644 docs/images/pr-deploy-manual.png diff --git a/.github/pr-deployments/certificate.yaml b/.github/pr-deployments/certificate.yaml new file mode 100644 index 0000000000000..cf441a98bbc88 --- /dev/null +++ b/.github/pr-deployments/certificate.yaml @@ -0,0 +1,13 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: pr${PR_NUMBER}-tls + namespace: pr-deployment-certs +spec: + secretName: pr${PR_NUMBER}-tls + issuerRef: + name: letsencrypt + kind: ClusterIssuer + dnsNames: + - "${PR_HOSTNAME}" + - "*.${PR_HOSTNAME}" diff --git a/.github/pr-deployments/rbac.yaml b/.github/pr-deployments/rbac.yaml new file mode 100644 index 0000000000000..0d37cae7daebe --- /dev/null +++ b/.github/pr-deployments/rbac.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: coder-workspace-pr${PR_NUMBER} + namespace: pr${PR_NUMBER} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-pr${PR_NUMBER} + namespace: pr${PR_NUMBER} +rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: coder-workspace-pr${PR_NUMBER} + namespace: pr${PR_NUMBER} +subjects: + - kind: ServiceAccount + name: coder-workspace-pr${PR_NUMBER} + namespace: pr${PR_NUMBER} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-pr${PR_NUMBER} diff --git a/.github/pr-deployments/template/main.tf b/.github/pr-deployments/template/main.tf new file mode 100644 index 0000000000000..bef767547b2a0 --- /dev/null +++ b/.github/pr-deployments/template/main.tf @@ -0,0 +1,313 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 0.11.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.22" + } + } +} + +provider "coder" { +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces)" +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +provider "kubernetes" { + config_path = null +} + +data "coder_workspace" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + startup_script_timeout = 180 + startup_script = <<-EOT + set -e + + # install and start code-server + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server + /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & + + EOT + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = <> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + get_info: - if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' + needs: check_pr + if: ${{ needs.check_pr.outputs.PR_OPEN == 'true' }} outputs: PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }} PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }} PR_URL: ${{ steps.pr_info.outputs.PR_URL }} - PR_BRANCH: ${{ steps.pr_info.outputs.PR_BRANCH }} CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }} CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }} - NEW: ${{ steps.check_deployment.outputs.new }} - BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.new }} + NEW: ${{ steps.check_deployment.outputs.NEW }} + BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW }} runs-on: "ubuntu-latest" steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get PR number, title, and branch name id: pr_info run: | - set -euxo pipefail - PR_NUMBER=${{ github.event.inputs.pr_number || github.event.pull_request.number }} - PR_TITLE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.title') - PR_BRANCH=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.head.ref') - echo "PR_URL=https://github.com/coder/coder/pull/$PR_NUMBER" >> $GITHUB_OUTPUT + set -euo pipefail + PR_NUMBER=$(gh pr view --json number | jq -r '.number') + PR_TITLE=$(gh pr view --json title | jq -r '.title') + PR_URL=$(gh pr view --json url | jq -r '.url') + echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT - echo "PR_BRANCH=$PR_BRANCH" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set required tags id: set_tags run: | - set -euxo pipefail + set -euo pipefail echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT env: @@ -74,7 +105,7 @@ jobs: - name: Set up kubeconfig run: | - set -euxo pipefail + set -euo pipefail mkdir -p ~/.kube echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config export KUBECONFIG=~/.kube/config @@ -82,53 +113,21 @@ jobs: - name: Check if the helm deployment already exists id: check_deployment run: | - set -euxo pipefail + set -euo pipefail if helm status "pr${{ steps.pr_info.outputs.PR_NUMBER }}" --namespace "pr${{ steps.pr_info.outputs.PR_NUMBER }}" > /dev/null 2>&1; then echo "Deployment already exists. Skipping deployment." - new=false + NEW=false else echo "Deployment doesn't exist." - new=true + NEW=true fi - echo "new=$new" >> $GITHUB_OUTPUT - - - name: Find Comment - uses: peter-evans/find-comment@v2 - if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false' - id: fc - with: - issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }} - comment-author: "github-actions[bot]" - body-includes: ":rocket:" - direction: last - - - name: Comment on PR - id: comment_id - if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false' - uses: peter-evans/create-or-update-comment@v3 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }} - edit-mode: replace - body: | - --- - :rocket: Deploying PR ${{ steps.pr_info.outputs.PR_NUMBER }} ... - --- - reactions: eyes - reactions-edit-mode: replace - - - name: Checkout - if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false' - uses: actions/checkout@v3 - with: - ref: ${{ steps.pr_info.outputs.PR_BRANCH }} - fetch-depth: 0 + echo "NEW=$NEW" >> $GITHUB_OUTPUT - name: Check changed files - if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false' uses: dorny/paths-filter@v2 id: filter with: + base: ${{ github.ref }} filters: | all: - "**" @@ -149,47 +148,76 @@ jobs: - "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*" - name: Print number of changed files - if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false' run: | - set -euxo pipefail + set -euo pipefail echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}" echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}" + - name: Print job outputs + run: | + set -euo pipefail + # Print all outputs of this job + echo "PR_NUMBER=${{ steps.pr_info.outputs.PR_NUMBER }}" + echo "PR_TITLE=${{ steps.pr_info.outputs.PR_TITLE }}" + echo "PR_URL=${{ steps.pr_info.outputs.PR_URL }}" + echo "CODER_BASE_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}" + echo "CODER_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}" + echo "NEW=${{ steps.check_deployment.outputs.NEW }}" + echo "BUILD=${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW || github.event.inputs.build == 'true' }}" + echo "GITHUB_REF=${{ github.ref }}" + + comment-pr: + needs: [check_pr, get_info] + if: ${{ needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true' || github.event.inputs.build == 'true' }} + runs-on: "ubuntu-latest" + steps: + - name: Find Comment + uses: peter-evans/find-comment@v2 + id: fc + with: + issue-number: ${{ needs.get_info.outputs.PR_NUMBER }} + comment-author: "github-actions[bot]" + body-includes: ":rocket:" + direction: last + + - name: Comment on PR + id: comment_id + uses: peter-evans/create-or-update-comment@v3 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ needs.get_info.outputs.PR_NUMBER }} + edit-mode: replace + body: | + --- + :rocket: Deploying PR ${{ needs.get_info.outputs.PR_NUMBER }} ... + --- + reactions: eyes + reactions-edit-mode: replace + build: needs: get_info - # Skips the build job if the workflow was triggered by a workflow_dispatch event and the skip_build input is set to true - # or if the workflow was triggered by an issue_comment event and the comment body contains --skip-build - # always run the build job if a pull_request event triggered the workflow - if: | - (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') || - (github.event_name == 'pull_request' && needs.get_info.result == 'success' && needs.get_info.outputs.NEW == 'false') + # Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag + if: needs.get_info.outputs.BUILD == 'true' runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} env: DOCKER_CLI_EXPERIMENTAL: "enabled" CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} - PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }} - PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }} steps: - name: Checkout uses: actions/checkout@v3 with: - ref: ${{ env.PR_BRANCH }} fetch-depth: 0 - name: Setup Node - if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-node - name: Setup Go - if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-go - name: Setup sqlc - if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-sqlc - name: GHCR Login - if: needs.get_info.outputs.BUILD == 'true' uses: docker/login-action@v2 with: registry: ghcr.io @@ -197,9 +225,8 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Linux amd64 Docker image - if: needs.get_info.outputs.BUILD == 'true' run: | - set -euxo pipefail + set -euo pipefail go mod download make gen/mark-fresh export DOCKER_IMAGE_NO_PREREQUISITES=true @@ -217,35 +244,42 @@ jobs: needs: [build, get_info] # Run deploy job only if build job was successful or skipped if: | - always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && - (github.event_name == 'workflow_dispatch' || needs.get_info.outputs.NEW == 'false') + always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && + (needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true') runs-on: "ubuntu-latest" env: CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }} PR_TITLE: ${{ needs.get_info.outputs.PR_TITLE }} PR_URL: ${{ needs.get_info.outputs.PR_URL }} - PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }} - PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" + PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: - name: Set up kubeconfig run: | - set -euxo pipefail + set -euo pipefail mkdir -p ~/.kube echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config export KUBECONFIG=~/.kube/config - name: Check if image exists - if: needs.get_info.outputs.NEW == 'true' run: | - set -euxo pipefail - foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o ${{ env.CODER_IMAGE_TAG }} | head -n 1) + set -euo pipefail + foundTag=$( + gh api /orgs/coder/packages/container/coder-preview/versions | + jq -r --arg tag "pr${{ env.PR_NUMBER }}" '.[] | + select(.metadata.container.tags == [$tag]) | + .metadata.container.tags[0]' + ) if [ -z "$foundTag" ]; then echo "Image not found" echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview" - echo "Please remove --skip-build from the comment and try again" exit 1 + else + echo "Image found" + echo "$foundTag tag found in ghcr.io/coder/coder-preview" fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Add DNS record to Cloudflare if: needs.get_info.outputs.NEW == 'true' @@ -253,43 +287,27 @@ jobs: curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \ -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \ -H "Content-Type:application/json" \ - --data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}' - - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ env.PR_BRANCH }} + --data '{"type":"CNAME","name":"*.${{ env.PR_HOSTNAME }}","content":"${{ env.PR_HOSTNAME }}","ttl":1,"proxied":false}' - name: Create PR namespace - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' run: | - set -euxo pipefail + set -euo pipefail # try to delete the namespace, but don't fail if it doesn't exist kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true kubectl create namespace "pr${{ env.PR_NUMBER }}" + - name: Checkout + uses: actions/checkout@v3 + - name: Check and Create Certificate - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' run: | # Using kubectl to check if a Certificate resource already exists # we are doing this to avoid letsenrypt rate limits if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then echo "Certificate doesn't exist. Creating a new one." - cat < pr-deploy-values.yaml - coder: - image: - repo: ${{ env.REPO }} - tag: pr${{ env.PR_NUMBER }} - pullPolicy: Always - service: - type: ClusterIP - ingress: - enable: true - className: traefik - host: ${{ env.PR_DEPLOYMENT_ACCESS_URL }} - wildcardHost: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}" - tls: - enable: true - secretName: pr${{ env.PR_NUMBER }}-tls - wildcardSecretName: pr${{ env.PR_NUMBER }}-tls - env: - - name: "CODER_ACCESS_URL" - value: "https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}" - - name: "CODER_WILDCARD_ACCESS_URL" - value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}" - - name: "CODER_EXPERIMENTS" - value: "${{ github.event.inputs.experiments }}" - - name: CODER_PG_CONNECTION_URL - valueFrom: - secretKeyRef: - name: coder-db-url - key: url - - name: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS" - value: "true" - - name: "CODER_OAUTH2_GITHUB_CLIENT_ID" - value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}" - - name: "CODER_OAUTH2_GITHUB_CLIENT_SECRET" - value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}" - - name: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS" - value: "coder" - EOF + set -euo pipefail + envsubst < ./.github/pr-deployments/values.yaml > ./pr-deploy-values.yaml - name: Install/Upgrade Helm chart run: | - set -euxo pipefail - if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then - helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ - --namespace "pr${{ env.PR_NUMBER }}" \ - --values ./pr-deploy-values.yaml \ - --force - else - if [[ ${{ needs.get_info.outputs.BUILD }} == "true" ]]; then - helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ - --namespace "pr${{ env.PR_NUMBER }}" \ - --reuse-values \ - --force - else - echo "Skipping helm upgrade, as there is no new image to deploy" - fi - fi + set -euo pipefail + helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm/coder \ + --namespace "pr${{ env.PR_NUMBER }}" \ + --values ./pr-deploy-values.yaml \ + --force - name: Install coder-logstream-kube - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' run: | helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube helm upgrade --install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \ --namespace "pr${{ env.PR_NUMBER }}" \ - --set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" + --set url="https://${{ env.PR_HOSTNAME }}" - name: Get Coder binary - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' run: | - set -euxo pipefail + set -euo pipefail DEST="${HOME}/coder" - URL="https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64" + URL="https://${{ env.PR_HOSTNAME }}/bin/coder-linux-amd64" mkdir -p "$(dirname ${DEST})" @@ -414,10 +395,10 @@ jobs: mv "${DEST}" /usr/local/bin/coder - name: Create first user, template and workspace - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' id: setup_deployment run: | - set -euxo pipefail + set -euo pipefail # Create first user @@ -429,28 +410,24 @@ jobs: echo "password=$password" >> $GITHUB_OUTPUT coder login \ - --first-user-username test \ + --first-user-username coder \ --first-user-email pr${{ env.PR_NUMBER }}@coder.com \ --first-user-password $password \ --first-user-trial \ --use-token-as-session \ - https://${{ env.PR_DEPLOYMENT_ACCESS_URL }} + https://${{ env.PR_HOSTNAME }} # Create template - coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} + cd ./.github/pr-deployments/template + terraform init + coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes # Create workspace - cat < workspace.yaml - cpu: "2" - memory: "4" - home_disk_size: "2" - EOF - - coder create --template="kubernetes" test --rich-parameter-file ./workspace.yaml -y - coder stop test -y + coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y + coder stop kube -y - name: Send Slack notification - if: needs.get_info.outputs.NEW == 'true' + if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' run: | curl -s -o /dev/null -X POST -H 'Content-type: application/json' \ -d \ @@ -458,7 +435,7 @@ jobs: "pr_number": "'"${{ env.PR_NUMBER }}"'", "pr_url": "'"${{ env.PR_URL }}"'", "pr_title": "'"${{ env.PR_TITLE }}"'", - "pr_access_url": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'", + "pr_access_url": "'"https://${{ env.PR_HOSTNAME }}"'", "pr_username": "'"test"'", "pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'", "pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'", diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 611faf48400c9..291f4e1444e4b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -84,11 +84,12 @@ You can test your changes by creating a PR deployment. There are two ways to do 1. By running `./scripts/deploy-pr.sh` 2. By manually triggering the [`pr-deploy.yaml`](https://github.com/coder/coder/actions/workflows/pr-deploy.yaml) GitHub Action workflow - ![Deploy PR manually](./images/pr-deploy-manual.png) + ![Deploy PR manually](./images/deploy-pr-manually.png) #### Available options -- `-s` or `--skip-build`, force prevents the build of the Docker image.(generally not needed as we are intelligently checking if the image needs to be built) +- `-d` or `--deploy`, force deploys the PR by deleting the existing deployment. +- `-b` or `--build`, force builds the Docker image. (generally not needed as we are intelligently checking if the image needs to be built) - `-e EXPERIMENT1,EXPERIMENT2` or `--experiments EXPERIMENT1,EXPERIMENT2`, will enable the specified experiments. (defaults to `*`) - `-n` or `--dry-run` will display the context without deployment. e.g., branch name and PR number, etc. - `-y` or `--yes`, will skip the CLI confirmation prompt. diff --git a/docs/images/deploy-pr-manually.png b/docs/images/deploy-pr-manually.png new file mode 100644 index 0000000000000000000000000000000000000000..718d00c65d1cced5f5defe15e4d07aefc4b6f16c GIT binary patch literal 13583 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 literal 0 HcmV?d00001 diff --git a/docs/images/pr-deploy-manual.png b/docs/images/pr-deploy-manual.png deleted file mode 100644 index eab92cc2249e75e08119bc1d1e4897545f6f75e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27155 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] [--build -b] [--deploy -d] # deploys the current branch to a PR environment and posts login credentials to # [#pr-deployments](https://codercom.slack.com/archives/C05DNE982E8) Slack channel set -euo pipefail # default settings -skipBuild=false dryRun=false confirm=true +build=false +deploy=false experiments="" # parse arguments while (("$#")); do case "$1" in - -s | --skip-build) - skipBuild=true + -b | --build) + build=true + shift + ;; + -d | --deploy) + deploy=true shift ;; -n | --dry-run) @@ -63,30 +68,20 @@ fi branchName=$(gh pr view --json headRefName | jq -r .headRefName) prNumber=$(gh pr view --json number | jq -r .number) -if $skipBuild; then - #check if the image exists - foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o "$prNumber" | head -n 1) || true - echo "foundTag is: '${foundTag}'" - if [[ -z "${foundTag}" ]]; then - echo "Image not found" - echo "${prNumber} tag not found in ghcr.io/coder/coder-preview" - echo "Please remove --skip-build and try again" - exit 1 - fi -fi - -if $dryRun; then +if [[ "$dryRun" = true ]]; then echo "dry run" echo "branchName: ${branchName}" echo "prNumber: ${prNumber}" - echo "skipBuild: ${skipBuild}" echo "experiments: ${experiments}" + echo "build: ${build}" + echo "deploy: ${deploy}" exit 0 fi echo "branchName: ${branchName}" echo "prNumber: ${prNumber}" -echo "skipBuild: ${skipBuild}" echo "experiments: ${experiments}" +echo "build: ${build}" +echo "deploy: ${deploy}" -gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "skip_build=${skipBuild}" -f "experiments=${experiments}" +gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "experiments=${experiments}" -f "build=${build}" -f "deploy=${deploy}" From 7fb9197860b72aaa4004ef6a06ef8c4d51499e94 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 11 Aug 2023 13:54:55 +0300 Subject: [PATCH 090/156] ci: do not run deploy-pr on main (#9046) --- .github/workflows/pr-deploy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 9cae6f7f3520e..42146da3a21b9 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -5,6 +5,8 @@ name: Deploy PR on: push: + branches-ignore: + - main workflow_dispatch: inputs: pr_number: From a13c8c88d5276dc582dc752f1f9500e1a4c05d0a Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 11 Aug 2023 15:32:42 +0300 Subject: [PATCH 091/156] fix: prevent unrequested PR deployments (#9049) --- .github/workflows/pr-deploy.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 42146da3a21b9..8b830ee73e361 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -73,7 +73,14 @@ jobs: CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }} CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }} NEW: ${{ steps.check_deployment.outputs.NEW }} - BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW }} + BUILD: | + ${{ + (steps.filter.outputs.all_count > steps.filter.outputs.ignored_count) && + ( + (github.event_name == 'push' && steps.check_deployment.outputs.NEW == 'false') || + github.event.inputs.build == 'true' + ) + }} runs-on: "ubuntu-latest" steps: @@ -165,12 +172,12 @@ jobs: echo "CODER_BASE_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}" echo "CODER_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}" echo "NEW=${{ steps.check_deployment.outputs.NEW }}" - echo "BUILD=${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW || github.event.inputs.build == 'true' }}" + echo "BUILD=${{ (steps.filter.outputs.all_count > steps.filter.outputs.ignored_count) && ((github.event_name == 'push' && steps.check_deployment.outputs.NEW == 'false') || github.event.inputs.build == 'true') }}" echo "GITHUB_REF=${{ github.ref }}" comment-pr: needs: [check_pr, get_info] - if: ${{ needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true' || github.event.inputs.build == 'true' }} + if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true' runs-on: "ubuntu-latest" steps: - name: Find Comment From 59fd4e86c9f2ddee4a313aad176c85b0e8f1babb Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 11 Aug 2023 15:43:37 +0300 Subject: [PATCH 092/156] ci: remove deleting comments section from pr-cleanup.yaml (#9047) --- .github/workflows/pr-cleanup.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index bb4f7146c7025..510c8f4299361 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -71,10 +71,3 @@ jobs: run: | set -euxo pipefail kubectl delete certificate "pr${{ steps.pr_number.outputs.PR_NUMBER }}-tls" -n pr-deployment-certs || echo "certificate not found" - - - name: Delete PR Comments - uses: izhangzhihao/delete-comment@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - delete_user_name: github-actions[bot] - issue_number: ${{ github.event.number }} From 1c7bd57da8f592a25dbd76dc5eef984a5e7d568a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Aug 2023 08:26:01 -0500 Subject: [PATCH 093/156] chore: clarify region selection behavior (#9021) * chore: clarify region selection behavior * Update site/src/components/Navbar/NavbarView.tsx Co-authored-by: Kyle Carberry --------- Co-authored-by: Kyle Carberry --- site/src/components/Navbar/NavbarView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx index 6bc5777b95064..e90c23e7654e0 100644 --- a/site/src/components/Navbar/NavbarView.tsx +++ b/site/src/components/Navbar/NavbarView.tsx @@ -307,7 +307,9 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ }} > Workspace proxies improve terminal and web app connections to - workspaces. This does not apply to SSH connections. + workspaces. This does not apply to CLI connections. A region must be + manually selected, otherwise the default primary region will be + used. theme.palette.divider }} /> From 47ca84be47f13a86abeca1f220895c467d969a6b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Aug 2023 09:02:19 -0500 Subject: [PATCH 094/156] chore: return queried user on failure to help debug (#9051) --- coderd/httpmw/userparam.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index f565687e00bdd..b895d17aafb6c 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -2,6 +2,7 @@ package httpmw import ( "context" + "fmt" "net/http" "github.com/go-chi/chi/v5" @@ -85,6 +86,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: userErrorMessage, + Detail: fmt.Sprintf("queried user=%q", userQuery), }) return } @@ -96,6 +98,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: userErrorMessage, + Detail: fmt.Sprintf("queried user=%q", userQuery), }) return } From 320de18be7116cccafe1036e485df9b25a24439a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Aug 2023 09:25:05 -0500 Subject: [PATCH 095/156] fix: correct github oauth2 callback url (#9052) * fix: correct github oauth2 callback url --- .../SecurityPage/SecurityPage.test.tsx | 1 + .../SecurityPage/SingleSignOnSection.tsx | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index d5ef50fad2ba5..5b5fdd49ba4f2 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -136,6 +136,7 @@ test("change login type to OIDC", async () => { jest.spyOn(SSO, "redirectToOIDCAuth").mockImplementation(() => { // Does a noop + return "" }) const ssoSection = screen.getByTestId("sso-section") diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index 6d793d2e2546d..66830b793e38d 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -30,7 +30,16 @@ export const redirectToOIDCAuth = ( stateString: string, redirectTo: string, ) => { - window.location.href = `/api/v2/users/${toType}/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}` + switch (toType) { + case "github": + window.location.href = `/api/v2/users/oauth2/github/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}` + break + case "oidc": + window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}` + break + default: + throw new Error(`Unknown login type ${toType}`) + } } export const useSingleSignOnSection = () => { From d2a9049fd71afd874ec96333fe8e599227961a65 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Fri, 11 Aug 2023 15:56:35 -0400 Subject: [PATCH 096/156] docs: add offline docs for JetBrains Gateway (#9039) --- docs/ides/gateway.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/ides/gateway.md b/docs/ides/gateway.md index b0550c378b668..45c9539c80456 100644 --- a/docs/ides/gateway.md +++ b/docs/ides/gateway.md @@ -179,3 +179,9 @@ cd /opt/idea/bin [Here is the JetBrains article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F) explaining this IDE specification. + +## JetBrains Gateway in an offline environment + +In networks that restrict access to the internet, you will need to leverage the +JetBrains Client Installer to download and save the IDE clients locally. Please +see the [JetBrains documentation for more information](https://www.jetbrains.com/help/idea/fully-offline-mode.html). From 6af6e85fe39f0f5b3e4214fd3479fa0482d76b2f Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Fri, 11 Aug 2023 16:55:55 -0400 Subject: [PATCH 097/156] docs: add coder login to CI docs (#9038) * docs: add coder login to CI docs * add CODER_URL * add --url flag to login cmd --- docs/templates/change-management.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/templates/change-management.md b/docs/templates/change-management.md index a1add78ec64f8..f2781d9ee0711 100644 --- a/docs/templates/change-management.md +++ b/docs/templates/change-management.md @@ -20,6 +20,7 @@ export CODER_TEMPLATE_DIR=.coder/templates/kubernetes export CODER_TEMPLATE_VERSION=$(git rev-parse --short HEAD) # Push the new template version to Coder +coder login --url $CODER_URL --token $CODER_SESSION_TOKEN coder templates push --yes $CODER_TEMPLATE_NAME \ --directory $CODER_TEMPLATE_DIR \ --name=$CODER_TEMPLATE_VERSION # Version name is optional From 984f7ce045b4f2160690b9acf5c5a175a71cc292 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 13 Aug 2023 11:18:17 +0300 Subject: [PATCH 098/156] fix: update BUILD condition in pr-deploy.yaml (#9064) This makes the build condition more understandable and fixes an issue where we could not deploy a new PR as the build condition was constantly evaluating false. --- .github/workflows/pr-deploy.yaml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 8b830ee73e361..4dd2446dcc9aa 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -73,14 +73,7 @@ jobs: CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }} CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }} NEW: ${{ steps.check_deployment.outputs.NEW }} - BUILD: | - ${{ - (steps.filter.outputs.all_count > steps.filter.outputs.ignored_count) && - ( - (github.event_name == 'push' && steps.check_deployment.outputs.NEW == 'false') || - github.event.inputs.build == 'true' - ) - }} + BUILD: ${{ steps.build_conditionals.outputs.first_or_force_build || steps.build_conditionals.outputs.automatic_rebuild }} runs-on: "ubuntu-latest" steps: @@ -162,18 +155,14 @@ jobs: echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}" echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}" - - name: Print job outputs + - name: Build conditionals + id: build_conditionals run: | set -euo pipefail - # Print all outputs of this job - echo "PR_NUMBER=${{ steps.pr_info.outputs.PR_NUMBER }}" - echo "PR_TITLE=${{ steps.pr_info.outputs.PR_TITLE }}" - echo "PR_URL=${{ steps.pr_info.outputs.PR_URL }}" - echo "CODER_BASE_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}" - echo "CODER_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}" - echo "NEW=${{ steps.check_deployment.outputs.NEW }}" - echo "BUILD=${{ (steps.filter.outputs.all_count > steps.filter.outputs.ignored_count) && ((github.event_name == 'push' && steps.check_deployment.outputs.NEW == 'false') || github.event.inputs.build == 'true') }}" - echo "GITHUB_REF=${{ github.ref }}" + # build if the workflow is manually triggered and the deployment doesn't exist (first build or force rebuild) + echo "first_or_force_build=${{ (github.event_name == 'workflow_dispatch' && steps.check_deployment.outputs.NEW == 'true') || github.event.inputs.build == 'true' }}" >> $GITHUB_OUTPUT + # build if the deployment alreday exist and there are changes in the files that we care about (automatic updates) + echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT comment-pr: needs: [check_pr, get_info] From abe17b1164fa76e45c813b58036f532aad126089 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 13 Aug 2023 10:10:58 -0500 Subject: [PATCH 099/156] chore: update speakeasy to fix stty path bug on nixos (#9022) Prompts failed on NixOS due to /bin/stty being hardcoded for turning off echo in the terminal prompt. See: https://github.com/bgentry/speakeasy/commit/760eaf8b681647364e7a400b856e0921248728a5 --- go.mod | 2 +- go.sum | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index be7e6ee88f9d9..f67e1b88d2771 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/awalterschulze/gographviz v2.0.3+incompatible github.com/bep/debounce v1.2.1 - github.com/bgentry/speakeasy v0.1.0 + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b github.com/briandowns/spinner v1.18.1 github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 diff --git a/go.sum b/go.sum index 46703cf471c41..b21e1008f30e7 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 h1:L6S7kR7SlhQKplIBpkra3s6yhcZV51lhRnXmYc4HohI= github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -152,6 +154,8 @@ github.com/bep/golibsass v1.1.1 h1:xkaet75ygImMYjM+FnHIT3xJn7H0xBA9UxSOJjk8Khw= github.com/bep/golibsass v1.1.1/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b h1:UJeNthMS3NHVtMFKMhzZNxdaXpYqQlbLrDRtVXorT7w= github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0= @@ -281,6 +285,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -323,6 +328,7 @@ github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -330,6 +336,7 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -365,6 +372,7 @@ github.com/go-playground/validator/v10 v10.15.0 h1:nDU5XeOKtB3GEa+uB7GNYwhVKsgjA github.com/go-playground/validator/v10 v10.15.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -504,6 +512,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= @@ -550,6 +559,7 @@ github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3s github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -559,6 +569,7 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E= github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI= github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -575,14 +586,17 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI= github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -658,10 +672,12 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -675,6 +691,7 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -694,8 +711,10 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= @@ -711,6 +730,7 @@ github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4Y github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= @@ -782,6 +802,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= @@ -789,6 +810,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= @@ -907,6 +929,7 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= @@ -1365,6 +1388,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 1629a2a4ee3df9d9a96b757c5c88336fc2e1db40 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 13 Aug 2023 18:52:14 +0300 Subject: [PATCH 100/156] chore: sort DERP regions by latencies on workspace page (#9063) --- site/src/components/Resources/AgentLatency.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/components/Resources/AgentLatency.tsx b/site/src/components/Resources/AgentLatency.tsx index 9be53f106cc2f..6f3ef29ad729b 100644 --- a/site/src/components/Resources/AgentLatency.tsx +++ b/site/src/components/Resources/AgentLatency.tsx @@ -66,11 +66,10 @@ export const AgentLatency: FC<{ agent: WorkspaceAgent }> = ({ agent }) => { This is the latency overhead on non peer to peer connections. The first row is the preferred relay. - {Object.entries(agent.latency) - .sort(([, a], [, b]) => (a.preferred ? -1 : b.preferred ? 1 : 0)) + .sort(([, a], [, b]) => a.latency_ms - b.latency_ms) .map(([regionName, region]) => ( Date: Sun, 13 Aug 2023 11:47:44 -0500 Subject: [PATCH 101/156] fix: remove duplication from language of query param error (#9069) --- coderd/httpapi/queryparams.go | 2 +- coderd/searchquery/search_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/httpapi/queryparams.go b/coderd/httpapi/queryparams.go index 3f16565e1dd20..b3ae12c7de11c 100644 --- a/coderd/httpapi/queryparams.go +++ b/coderd/httpapi/queryparams.go @@ -45,7 +45,7 @@ func (p *QueryParamParser) ErrorExcessParams(values url.Values) { if _, ok := p.Parsed[k]; !ok { p.Errors = append(p.Errors, codersdk.ValidationError{ Field: k, - Detail: fmt.Sprintf("Query param %q is not a valid query param", k), + Detail: fmt.Sprintf("%q is not a valid query param", k), }) } } diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 4a7f61331a5f2..f30cc44d9498a 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -142,7 +142,7 @@ func TestSearchWorkspace(t *testing.T) { { Name: "ExtraKeys", Query: `foo:bar`, - ExpectedErrorContains: `Query param "foo" is not a valid query param`, + ExpectedErrorContains: `"foo" is not a valid query param`, }, } @@ -239,7 +239,7 @@ func TestSearchAudit(t *testing.T) { { Name: "ExtraKeys", Query: `foo:bar`, - ExpectedErrorContains: `Query param "foo" is not a valid query param`, + ExpectedErrorContains: `"foo" is not a valid query param`, }, { Name: "Dates", @@ -370,7 +370,7 @@ func TestSearchUsers(t *testing.T) { { Name: "ExtraKeys", Query: `foo:bar`, - ExpectedErrorContains: `Query param "foo" is not a valid query param`, + ExpectedErrorContains: `"foo" is not a valid query param`, }, } From 0d01d022f7ae20910d726d8d895ad8f0a58f60c0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 13 Aug 2023 11:48:11 -0500 Subject: [PATCH 102/156] fix: remove unnecessary newlines from the end of cli output (#9068) `Infof` already adds a newline, so we don't need to as well! --- cli/exp_scaletest.go | 8 ++++---- cli/gitaskpass.go | 6 +++--- cli/publickey.go | 10 +++++----- cli/server.go | 4 ++-- cli/server_createadminuser.go | 2 +- cli/speedtest.go | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go index d2ee36c1819eb..e947aa1260100 100644 --- a/cli/exp_scaletest.go +++ b/cli/exp_scaletest.go @@ -427,7 +427,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd { cliui.Errorf(inv.Stderr, "Found %d scaletest workspaces\n", len(workspaces)) if len(workspaces) != 0 { - cliui.Infof(inv.Stdout, "Deleting scaletest workspaces..."+"\n") + cliui.Infof(inv.Stdout, "Deleting scaletest workspaces...") harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{}) for i, w := range workspaces { @@ -443,7 +443,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd { return xerrors.Errorf("run test harness to delete workspaces (harness failure, not a test failure): %w", err) } - cliui.Infof(inv.Stdout, "Done deleting scaletest workspaces:"+"\n") + cliui.Infof(inv.Stdout, "Done deleting scaletest workspaces:") res := harness.Results() res.PrintText(inv.Stderr) @@ -460,7 +460,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd { cliui.Errorf(inv.Stderr, "Found %d scaletest users\n", len(users)) if len(users) != 0 { - cliui.Infof(inv.Stdout, "Deleting scaletest users..."+"\n") + cliui.Infof(inv.Stdout, "Deleting scaletest users...") harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{}) for i, u := range users { @@ -479,7 +479,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd { return xerrors.Errorf("run test harness to delete users (harness failure, not a test failure): %w", err) } - cliui.Infof(inv.Stdout, "Done deleting scaletest users:"+"\n") + cliui.Infof(inv.Stdout, "Done deleting scaletest users:") res := harness.Results() res.PrintText(inv.Stderr) diff --git a/cli/gitaskpass.go b/cli/gitaskpass.go index 5bb67adf82416..3d2654507ce00 100644 --- a/cli/gitaskpass.go +++ b/cli/gitaskpass.go @@ -51,9 +51,9 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd { } if token.URL != "" { if err := openURL(inv, token.URL); err == nil { - cliui.Infof(inv.Stderr, "Your browser has been opened to authenticate with Git:\n\n%s\n", token.URL) + cliui.Infof(inv.Stderr, "Your browser has been opened to authenticate with Git:\n%s", token.URL) } else { - cliui.Infof(inv.Stderr, "Open the following URL to authenticate with Git:\n\n%s\n", token.URL) + cliui.Infof(inv.Stderr, "Open the following URL to authenticate with Git:\n%s", token.URL) } for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); { @@ -61,7 +61,7 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd { if err != nil { continue } - cliui.Infof(inv.Stderr, "You've been authenticated with Git!\n") + cliui.Infof(inv.Stderr, "You've been authenticated with Git!") break } } diff --git a/cli/publickey.go b/cli/publickey.go index 43537eec428a1..cbe67fca23c1a 100644 --- a/cli/publickey.go +++ b/cli/publickey.go @@ -45,12 +45,12 @@ func (r *RootCmd) publickey() *clibase.Cmd { cliui.Infof(inv.Stdout, "This is your public key for using "+cliui.DefaultStyles.Field.Render("git")+" in "+ - "Coder. All clones with SSH will be authenticated automatically 🪄.\n\n", + "Coder. All clones with SSH will be authenticated automatically 🪄.", ) - cliui.Infof(inv.Stdout, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n\n") - cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:"+"\n") - cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new"+"\n") - cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys"+"\n") + cliui.Infof(inv.Stdout, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n") + cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:") + cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new") + cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys") return nil }, diff --git a/cli/server.go b/cli/server.go index 55a4db844723f..e4557e7d218ee 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1030,7 +1030,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. defer wg.Done() if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok { - cliui.Infof(inv.Stdout, "Shutting down provisioner daemon %d...\n", id) + cliui.Infof(inv.Stdout, "Shutting down provisioner daemon %d...", id) } err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second) if err != nil { @@ -1043,7 +1043,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return } if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok { - cliui.Infof(inv.Stdout, "Gracefully shut down provisioner daemon %d\n", id) + cliui.Infof(inv.Stdout, "Gracefully shut down provisioner daemon %d", id) } }() } diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go index fbdfed6b8016e..4eb16343318e2 100644 --- a/cli/server_createadminuser.go +++ b/cli/server_createadminuser.go @@ -51,7 +51,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd { defer cancel() if newUserDBURL == "" { - cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)\n", cfg.PostgresPath()) + cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", cfg.PostgresPath()) url, closePg, err := startBuiltinPostgres(ctx, cfg, logger) if err != nil { return err diff --git a/cli/speedtest.go b/cli/speedtest.go index 150605b3330ce..e38581404ecf0 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -85,14 +85,14 @@ func (r *RootCmd) speedtest() *clibase.Cmd { } peer := status.Peer[status.Peers()[0]] if !p2p && direct { - cliui.Infof(inv.Stdout, "Waiting for a direct connection... (%dms via %s)\n", dur.Milliseconds(), peer.Relay) + cliui.Infof(inv.Stdout, "Waiting for a direct connection... (%dms via %s)", dur.Milliseconds(), peer.Relay) continue } via := peer.Relay if via == "" { via = "direct" } - cliui.Infof(inv.Stdout, "%dms via %s\n", dur.Milliseconds(), via) + cliui.Infof(inv.Stdout, "%dms via %s", dur.Milliseconds(), via) break } } else { @@ -107,7 +107,7 @@ func (r *RootCmd) speedtest() *clibase.Cmd { default: return xerrors.Errorf("invalid direction: %q", direction) } - cliui.Infof(inv.Stdout, "Starting a %ds %s test...\n", int(duration.Seconds()), tsDir) + cliui.Infof(inv.Stdout, "Starting a %ds %s test...", int(duration.Seconds()), tsDir) results, err := conn.Speedtest(ctx, tsDir, duration) if err != nil { return err From 594b9797dda57c8dfc5977e54267ba05403093a1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 13 Aug 2023 11:58:04 -0500 Subject: [PATCH 103/156] fix: change dashboard route `/settings/deployment` to `/deployment` (#9070) It felt unnecessary to nest this. --- site/src/AppRouter.tsx | 5 +---- site/src/components/Navbar/NavbarView.test.tsx | 2 +- site/src/components/Navbar/NavbarView.tsx | 2 +- .../LicensesSettingsPage/AddNewLicensePage.tsx | 4 ++-- .../LicensesSettingsPage/AddNewLicensePageView.tsx | 2 +- .../LicensesSettingsPage/LicensesSettingsPageView.tsx | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 449d4925febb3..c1f222d90c334 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -272,10 +272,7 @@ export const AppRouter: FC = () => { } /> - } - > + }> } /> } /> } /> diff --git a/site/src/components/Navbar/NavbarView.test.tsx b/site/src/components/Navbar/NavbarView.test.tsx index 307670708cc72..2ba0abc8e0ab4 100644 --- a/site/src/components/Navbar/NavbarView.test.tsx +++ b/site/src/components/Navbar/NavbarView.test.tsx @@ -154,7 +154,7 @@ describe("NavbarView", () => { ) const auditLink = await screen.findByText(navLanguage.deployment) expect((auditLink as HTMLAnchorElement).href).toContain( - "/settings/deployment/general", + "/deployment/general", ) }) diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx index e90c23e7654e0..780cbd98deb81 100644 --- a/site/src/components/Navbar/NavbarView.tsx +++ b/site/src/components/Navbar/NavbarView.tsx @@ -93,7 +93,7 @@ const NavItems: React.FC< )} {canViewDeployment && ( - + {Language.deployment} diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx index 7c75dca3713c0..3d60005a5e87f 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx @@ -17,7 +17,7 @@ const AddNewLicensePage: FC = () => { } = useMutation(createLicense, { onSuccess: () => { displaySuccess("You have successfully added a license") - navigate("/settings/deployment/licenses?success=true") + navigate("/deployment/licenses?success=true") }, onError: () => displayError("Failed to save license key"), }) @@ -28,7 +28,7 @@ const AddNewLicensePage: FC = () => { { onSuccess: () => { displaySuccess("You have successfully added a license") - navigate("/settings/deployment/licenses?success=true") + navigate("/deployment/licenses?success=true") }, onError: () => displayError("Failed to save license key"), }, diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx index 48d0bb992b71a..a7a2fbc346718 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx @@ -60,7 +60,7 @@ export const AddNewLicensePageView: FC = ({ diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index b1f5c678ac630..fe3286eab8c8c 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -57,7 +57,7 @@ const LicensesSettingsPageView: FC = ({