diff --git a/coderd/coderd.go b/coderd/coderd.go index d094c05757..2be576ea80 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1436,9 +1436,7 @@ func New(options *Options) *API { Optional: true, }), httpmw.RequireAPIKeyOrWorkspaceProxyAuth(), - - httpmw.ExtractWorkspaceAgentParam(options.Database), - httpmw.ExtractWorkspaceParam(options.Database), + httpmw.ExtractWorkspaceAgentAndWorkspaceParam(options.Database), ) r.Get("/", api.workspaceAgent) r.Get("/watch-metadata", api.watchWorkspaceAgentMetadataSSE) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index ef9180aa6f..001d725ecb 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -569,7 +569,7 @@ func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerNa }.String() } -func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) []codersdk.WorkspaceApp { +func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus, agent database.WorkspaceAgent, ownerName string, workspace database.WorkspaceTable) []codersdk.WorkspaceApp { sort.Slice(dbApps, func(i, j int) bool { if dbApps[i].DisplayOrder != dbApps[j].DisplayOrder { return dbApps[i].DisplayOrder < dbApps[j].DisplayOrder diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index f9921b44f3..a15ed466ee 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3614,6 +3614,10 @@ func (q *querier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context return q.db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, authToken) } +func (q *querier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentAndWorkspaceByIDRow, error) { + return fetch(q.log, q.auth, q.db.GetWorkspaceAgentAndWorkspaceByID)(ctx, id) +} + func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { // Fast path: Check if we have a workspace RBAC object in context. // In the agent API this is set at agent connection time to avoid the expensive diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 5c0bb1f3af..473b7e43bf 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1828,6 +1828,11 @@ func (s *MethodTestSuite) TestWorkspace() { dbm.EXPECT().GetWorkspaceAgentByID(gomock.Any(), agt.ID).Return(agt, nil).AnyTimes() check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(agt) })) + s.Run("GetWorkspaceAgentAndWorkspaceByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + aww := testutil.Fake(s.T(), faker, database.GetWorkspaceAgentAndWorkspaceByIDRow{}) + dbm.EXPECT().GetWorkspaceAgentAndWorkspaceByID(gomock.Any(), aww.WorkspaceAgent.ID).Return(aww, nil).AnyTimes() + check.Args(aww.WorkspaceAgent.ID).Asserts(aww.WorkspaceTable, policy.ActionRead).Returns(aww) + })) s.Run("GetWorkspaceAgentsByWorkspaceAndBuildNumber", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { w := testutil.Fake(s.T(), faker, database.Workspace{}) agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index ca62612eda..179e7bff3e 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2215,6 +2215,14 @@ func (m queryMetricsStore) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx contex return r0, r1 } +func (m queryMetricsStore) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentAndWorkspaceByIDRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentAndWorkspaceByID(ctx, id) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentByIDWithWorkspace").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetWorkspaceAgentByIDWithWorkspace").Inc() + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { start := time.Now() r0, r1 := m.s.GetWorkspaceAgentByID(ctx, id) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 87f533094a..aad6fc0746 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4137,6 +4137,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentAndLatestBuildByAuthToken", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentAndLatestBuildByAuthToken), ctx, authToken) } +// GetWorkspaceAgentAndWorkspaceByID mocks base method. +func (m *MockStore) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentAndWorkspaceByIDRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAgentAndWorkspaceByID", ctx, id) + ret0, _ := ret[0].(database.GetWorkspaceAgentAndWorkspaceByIDRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAgentAndWorkspaceByID indicates an expected call of GetWorkspaceAgentAndWorkspaceByID. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentAndWorkspaceByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentAndWorkspaceByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentAndWorkspaceByID), ctx, id) +} + // GetWorkspaceAgentByID mocks base method. func (m *MockStore) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 352d414a20..5008de03f3 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -884,3 +884,8 @@ func WorkspaceIdentityFromWorkspace(w Workspace) WorkspaceIdentity { AutostartSchedule: w.AutostartSchedule, } } + +// A workspace agent belongs to the owner of the associated workspace. +func (r GetWorkspaceAgentAndWorkspaceByIDRow) RBACObject() rbac.Object { + return r.WorkspaceTable.RBACObject() +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 571a3b3f6c..e864869b98 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -467,6 +467,7 @@ type sqlcQuerier interface { GetWebpushVAPIDKeys(ctx context.Context) (GetWebpushVAPIDKeysRow, error) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (GetWorkspaceACLByIDRow, error) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) + GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentAndWorkspaceByIDRow, error) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentDevcontainer, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b4bbc35942..4adba352db 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -18155,6 +18155,99 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont return i, err } +const getWorkspaceAgentAndWorkspaceByID = `-- name: GetWorkspaceAgentAndWorkspaceByID :one +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.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.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, + 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.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, + users.username as owner_username +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id +JOIN + workspace_builds ON provisioner_jobs.id = workspace_builds.job_id +JOIN + workspaces ON workspace_builds.workspace_id = workspaces.id +JOIN + users ON workspaces.owner_id = users.id +WHERE + workspace_agents.id = $1 + AND workspace_agents.deleted = FALSE + AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type + AND workspaces.deleted = FALSE + AND users.deleted = FALSE +LIMIT 1 +` + +type GetWorkspaceAgentAndWorkspaceByIDRow struct { + WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"` + WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"` + OwnerUsername string `db:"owner_username" json:"owner_username"` +} + +func (q *sqlQuerier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentAndWorkspaceByIDRow, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAgentAndWorkspaceByID, id) + var i GetWorkspaceAgentAndWorkspaceByIDRow + err := row.Scan( + &i.WorkspaceAgent.ID, + &i.WorkspaceAgent.CreatedAt, + &i.WorkspaceAgent.UpdatedAt, + &i.WorkspaceAgent.Name, + &i.WorkspaceAgent.FirstConnectedAt, + &i.WorkspaceAgent.LastConnectedAt, + &i.WorkspaceAgent.DisconnectedAt, + &i.WorkspaceAgent.ResourceID, + &i.WorkspaceAgent.AuthToken, + &i.WorkspaceAgent.AuthInstanceID, + &i.WorkspaceAgent.Architecture, + &i.WorkspaceAgent.EnvironmentVariables, + &i.WorkspaceAgent.OperatingSystem, + &i.WorkspaceAgent.InstanceMetadata, + &i.WorkspaceAgent.ResourceMetadata, + &i.WorkspaceAgent.Directory, + &i.WorkspaceAgent.Version, + &i.WorkspaceAgent.LastConnectedReplicaID, + &i.WorkspaceAgent.ConnectionTimeoutSeconds, + &i.WorkspaceAgent.TroubleshootingURL, + &i.WorkspaceAgent.MOTDFile, + &i.WorkspaceAgent.LifecycleState, + &i.WorkspaceAgent.ExpandedDirectory, + &i.WorkspaceAgent.LogsLength, + &i.WorkspaceAgent.LogsOverflowed, + &i.WorkspaceAgent.StartedAt, + &i.WorkspaceAgent.ReadyAt, + pq.Array(&i.WorkspaceAgent.Subsystems), + pq.Array(&i.WorkspaceAgent.DisplayApps), + &i.WorkspaceAgent.APIVersion, + &i.WorkspaceAgent.DisplayOrder, + &i.WorkspaceAgent.ParentID, + &i.WorkspaceAgent.APIKeyScope, + &i.WorkspaceAgent.Deleted, + &i.WorkspaceTable.ID, + &i.WorkspaceTable.CreatedAt, + &i.WorkspaceTable.UpdatedAt, + &i.WorkspaceTable.OwnerID, + &i.WorkspaceTable.OrganizationID, + &i.WorkspaceTable.TemplateID, + &i.WorkspaceTable.Deleted, + &i.WorkspaceTable.Name, + &i.WorkspaceTable.AutostartSchedule, + &i.WorkspaceTable.Ttl, + &i.WorkspaceTable.LastUsedAt, + &i.WorkspaceTable.DormantAt, + &i.WorkspaceTable.DeletingAt, + &i.WorkspaceTable.AutomaticUpdates, + &i.WorkspaceTable.Favorite, + &i.WorkspaceTable.NextStartAt, + &i.WorkspaceTable.GroupACL, + &i.WorkspaceTable.UserACL, + &i.OwnerUsername, + ) + 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, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index da6c34a761..0ac15d63a8 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -393,3 +393,28 @@ AND wb.build_number = ( WHERE wb2.workspace_id = w.id ) AND workspace_agents.deleted = FALSE; + +-- name: GetWorkspaceAgentAndWorkspaceByID :one +SELECT + sqlc.embed(workspace_agents), + sqlc.embed(workspaces), + users.username as owner_username +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id +JOIN + workspace_builds ON provisioner_jobs.id = workspace_builds.job_id +JOIN + workspaces ON workspace_builds.workspace_id = workspaces.id +JOIN + users ON workspaces.owner_id = users.id +WHERE + workspace_agents.id = @id + AND workspace_agents.deleted = FALSE + AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type + AND workspaces.deleted = FALSE + AND users.deleted = FALSE +LIMIT 1; diff --git a/coderd/httpmw/workspaceagentparam.go b/coderd/httpmw/workspaceagentparam.go index 38f572c434..ce2e675e55 100644 --- a/coderd/httpmw/workspaceagentparam.go +++ b/coderd/httpmw/workspaceagentparam.go @@ -13,19 +13,19 @@ import ( "github.com/coder/coder/v2/codersdk" ) -type workspaceAgentParamContextKey struct{} +type workspaceAgentAndWorkspaceParamContextKey struct{} -// WorkspaceAgentParam returns the workspace agent from the ExtractWorkspaceAgentParam handler. -func WorkspaceAgentParam(r *http.Request) database.WorkspaceAgent { - user, ok := r.Context().Value(workspaceAgentParamContextKey{}).(database.WorkspaceAgent) +// WorkspaceAgentAndWorkspaceParam returns the workspace agent and its associated workspace from the ExtractWorkspaceAgentParam handler. +func WorkspaceAgentAndWorkspaceParam(r *http.Request) database.GetWorkspaceAgentAndWorkspaceByIDRow { + aw, ok := r.Context().Value(workspaceAgentAndWorkspaceParamContextKey{}).(database.GetWorkspaceAgentAndWorkspaceByIDRow) if !ok { panic("developer error: agent middleware not provided") } - return user + return aw } -// ExtractWorkspaceAgentParam grabs a workspace agent from the "workspaceagent" URL parameter. -func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handler { +// ExtractWorkspaceAgentAndWorkspaceParam grabs a workspace agent and its associated workspace from the "workspaceagent" URL parameter. +func ExtractWorkspaceAgentAndWorkspaceParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -34,60 +34,21 @@ func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handl return } - agent, err := db.GetWorkspaceAgentByID(ctx, agentUUID) + agentWithWorkspace, err := db.GetWorkspaceAgentAndWorkspaceByID(ctx, agentUUID) if httpapi.Is404Error(err) { httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ Message: "Agent doesn't exist with that id, or you do not have access to it.", }) return } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agent.", - Detail: err.Error(), - }) - return - } - resource, err := db.GetWorkspaceResourceByID(ctx, agent.ResourceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource.", - Detail: err.Error(), - }) - return - } - - job, err := db.GetProvisionerJobByID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner job.", - Detail: err.Error(), - }) - return - } - if job.Type != database.ProvisionerJobTypeWorkspaceBuild { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Workspace agents can only be fetched for builds.", - }) - return - } - build, err := db.GetWorkspaceBuildByJobID(ctx, job.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build.", - Detail: err.Error(), - }) - return - } - - ctx = context.WithValue(ctx, workspaceAgentParamContextKey{}, agent) - chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String()) + ctx = context.WithValue(ctx, workspaceAgentAndWorkspaceParamContextKey{}, agentWithWorkspace) + chi.RouteContext(ctx).URLParams.Add("workspace", agentWithWorkspace.WorkspaceTable.ID.String()) if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { rlogger.WithFields( - slog.F("workspace_name", resource.Name), - slog.F("agent_name", agent.Name), + slog.F("workspace_name", agentWithWorkspace.WorkspaceTable.Name), + slog.F("agent_name", agentWithWorkspace.WorkspaceAgent.Name), ) } diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index 30f8fc46ef..7aedd7fabf 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -86,7 +86,7 @@ func TestWorkspaceAgentParam(t *testing.T) { db, _ := dbtestutil.NewDB(t) dbtestutil.DisableForeignKeysAndTriggers(t, db) rtr := chi.NewRouter() - rtr.Use(httpmw.ExtractWorkspaceAgentParam(db)) + rtr.Use(httpmw.ExtractWorkspaceAgentAndWorkspaceParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) @@ -113,10 +113,10 @@ func TestWorkspaceAgentParam(t *testing.T) { RedirectToLogin: false, }), // Only fail authz in this middleware - httpmw.ExtractWorkspaceAgentParam(dbFail), + httpmw.ExtractWorkspaceAgentAndWorkspaceParam(dbFail), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceAgentParam(r) + _ = httpmw.WorkspaceAgentAndWorkspaceParam(r) rw.WriteHeader(http.StatusOK) }) @@ -140,10 +140,10 @@ func TestWorkspaceAgentParam(t *testing.T) { DB: db, RedirectToLogin: false, }), - httpmw.ExtractWorkspaceAgentParam(db), + httpmw.ExtractWorkspaceAgentAndWorkspaceParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceAgentParam(r) + _ = httpmw.WorkspaceAgentAndWorkspaceParam(r) rw.WriteHeader(http.StatusOK) }) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a847519de5..f97cfde85f 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -60,10 +60,9 @@ import ( // @Success 200 {object} codersdk.WorkspaceAgent // @Router /workspaceagents/{workspaceagent} [get] func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgentParam(r) - var ( + ctx = r.Context() + waws = httpmw.WorkspaceAgentAndWorkspaceParam(r) dbApps []database.WorkspaceApp scripts []database.WorkspaceAgentScript logSources []database.WorkspaceAgentLogSource @@ -71,17 +70,17 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { var eg errgroup.Group eg.Go(func() (err error) { - dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID) + dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, waws.WorkspaceAgent.ID) return err }) eg.Go(func() (err error) { //nolint:gocritic // TODO: can we make this not require system restricted? - scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{workspaceAgent.ID}) + scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) return err }) eg.Go(func() (err error) { //nolint:gocritic // TODO: can we make this not require system restricted? - logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{workspaceAgent.ID}) + logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) return err }) err := eg.Wait() @@ -111,41 +110,8 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { return } - resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource.", - Detail: err.Error(), - }) - return - } - build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build.", - Detail: err.Error(), - }) - return - } - workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), - }) - return - } - owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace owner.", - Detail: err.Error(), - }) - return - } - apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, db2sdk.Apps(dbApps, statuses, workspaceAgent, owner.Username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), waws.WorkspaceAgent, db2sdk.Apps(dbApps, statuses, waws.WorkspaceAgent, waws.OwnerUsername, waws.WorkspaceTable), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -546,12 +512,12 @@ func (api *API) enqueueAITaskStateNotification( func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { // This mostly copies how provisioner job logs are streamed! var ( - ctx = r.Context() - workspaceAgent = httpmw.WorkspaceAgentParam(r) - logger = api.Logger.With(slog.F("workspace_agent_id", workspaceAgent.ID)) - follow = r.URL.Query().Has("follow") - afterRaw = r.URL.Query().Get("after") - noCompression = r.URL.Query().Has("no_compression") + ctx = r.Context() + waws = httpmw.WorkspaceAgentAndWorkspaceParam(r) + logger = api.Logger.With(slog.F("workspace_agent_id", waws.WorkspaceAgent.ID)) + follow = r.URL.Query().Has("follow") + afterRaw = r.URL.Query().Get("after") + noCompression = r.URL.Query().Has("no_compression") ) var after int64 @@ -571,7 +537,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { } logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{ - AgentID: workspaceAgent.ID, + AgentID: waws.WorkspaceAgent.ID, CreatedAfter: after, }) if errors.Is(err, sql.ErrNoRows) { @@ -593,15 +559,6 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { return } - workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace by agent id.", - Detail: err.Error(), - }) - return - } - api.WebsocketWaitMutex.Lock() api.WebsocketWaitGroup.Add(1) api.WebsocketWaitMutex.Unlock() @@ -650,13 +607,13 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { notifyCh <- struct{}{} // Subscribe to workspace to detect new builds. - closeSubscribeWorkspace, err := api.Pubsub.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + closeSubscribeWorkspace, err := api.Pubsub.SubscribeWithErr(wspubsub.WorkspaceEventChannel(waws.WorkspaceTable.OwnerID), wspubsub.HandleWorkspaceEvent( func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { if err != nil { return } - if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == waws.WorkspaceTable.ID { select { case workspaceNotifyCh <- struct{}{}: default: @@ -672,7 +629,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { } defer closeSubscribeWorkspace() // Subscribe early to prevent missing log events. - closeSubscribe, err := api.Pubsub.Subscribe(agentsdk.LogsNotifyChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) { + closeSubscribe, err := api.Pubsub.Subscribe(agentsdk.LogsNotifyChannel(waws.WorkspaceAgent.ID), func(_ context.Context, _ []byte) { // The message is not important, we're tracking lastSentLogID manually. select { case notifyCh <- struct{}{}: @@ -726,7 +683,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { t.Reset(recheckInterval) } - agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) + agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, waws.WorkspaceTable.ID) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { if xerrors.Is(err, context.Canceled) { return @@ -736,7 +693,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { } // If the agent is no longer in the latest build, we can stop after // checking once. - keepGoing = slices.ContainsFunc(agents, func(agent database.WorkspaceAgent) bool { return agent.ID == workspaceAgent.ID }) + keepGoing = slices.ContainsFunc(agents, func(agent database.WorkspaceAgent) bool { return agent.ID == waws.WorkspaceAgent.ID }) logger.Debug( ctx, @@ -753,7 +710,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { } logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{ - AgentID: workspaceAgent.ID, + AgentID: waws.WorkspaceAgent.ID, CreatedAfter: lastSentLogID, }) if err != nil { @@ -816,7 +773,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { // @Router /workspaceagents/{workspaceagent}/listening-ports [get] func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgentParam(r) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) // If the agent is unreachable, the request will hang. Assume that if we // don't get a response after 30s that the agent is unreachable. @@ -824,7 +781,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req defer cancel() apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), waws.WorkspaceAgent, nil, nil, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -841,7 +798,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req return } - agentConn, release, err := api.agentProvider.AgentConn(ctx, workspaceAgent.ID) + agentConn, release, err := api.agentProvider.AgentConn(ctx, waws.WorkspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error dialing workspace agent.", @@ -861,7 +818,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req } // Get a list of ports that are in-use by applications. - apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID) + apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, waws.WorkspaceAgent.ID) if xerrors.Is(err, sql.ErrNoRows) { apps = []database.WorkspaceApp{} err = nil @@ -926,9 +883,9 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req // @Router /workspaceagents/{workspaceagent}/containers/watch [get] func (api *API) watchWorkspaceAgentContainers(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - workspaceAgent = httpmw.WorkspaceAgentParam(r) - logger = api.Logger.Named("agent_container_watcher").With(slog.F("agent_id", workspaceAgent.ID)) + ctx = r.Context() + waws = httpmw.WorkspaceAgentAndWorkspaceParam(r) + logger = api.Logger.Named("agent_container_watcher").With(slog.F("agent_id", waws.WorkspaceAgent.ID)) ) // If the agent is unreachable, the request will hang. Assume that if we @@ -938,7 +895,7 @@ func (api *API) watchWorkspaceAgentContainers(rw http.ResponseWriter, r *http.Re apiAgent, err := db2sdk.WorkspaceAgent( api.DERPMap(), *api.TailnetCoordinator.Load(), - workspaceAgent, + waws.WorkspaceAgent, nil, nil, nil, @@ -959,7 +916,7 @@ func (api *API) watchWorkspaceAgentContainers(rw http.ResponseWriter, r *http.Re return } - agentConn, release, err := api.agentProvider.AgentConn(dialCtx, workspaceAgent.ID) + agentConn, release, err := api.agentProvider.AgentConn(dialCtx, waws.WorkspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error dialing workspace agent.", @@ -1034,7 +991,7 @@ func (api *API) watchWorkspaceAgentContainers(rw http.ResponseWriter, r *http.Re // @Router /workspaceagents/{workspaceagent}/containers [get] func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgentParam(r) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) labelParam, ok := r.URL.Query()["label"] if !ok { @@ -1060,7 +1017,7 @@ func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Req apiAgent, err := db2sdk.WorkspaceAgent( api.DERPMap(), *api.TailnetCoordinator.Load(), - workspaceAgent, + waws.WorkspaceAgent, nil, nil, nil, @@ -1081,7 +1038,7 @@ func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Req return } - agentConn, release, err := api.agentProvider.AgentConn(ctx, workspaceAgent.ID) + agentConn, release, err := api.agentProvider.AgentConn(ctx, waws.WorkspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error dialing workspace agent.", @@ -1131,10 +1088,9 @@ func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Req // @Router /workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer} [delete] func (api *API) workspaceAgentDeleteDevcontainer(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgentParam(r) - workspace := httpmw.WorkspaceParam(r) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) - if !api.Authorize(r, policy.ActionUpdate, workspace) { + if !api.Authorize(r, policy.ActionUpdate, waws.WorkspaceTable) { httpapi.Forbidden(rw) return } @@ -1153,7 +1109,7 @@ func (api *API) workspaceAgentDeleteDevcontainer(rw http.ResponseWriter, r *http apiAgent, err := db2sdk.WorkspaceAgent( api.DERPMap(), *api.TailnetCoordinator.Load(), - workspaceAgent, + waws.WorkspaceAgent, nil, nil, nil, @@ -1178,7 +1134,7 @@ func (api *API) workspaceAgentDeleteDevcontainer(rw http.ResponseWriter, r *http // don't get a response after 30s that the agent is unreachable. dialCtx, dialCancel := context.WithTimeout(ctx, 30*time.Second) defer dialCancel() - agentConn, release, err := api.agentProvider.AgentConn(dialCtx, workspaceAgent.ID) + agentConn, release, err := api.agentProvider.AgentConn(dialCtx, waws.WorkspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error dialing workspace agent.", @@ -1222,7 +1178,7 @@ func (api *API) workspaceAgentDeleteDevcontainer(rw http.ResponseWriter, r *http // @Router /workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate [post] func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - workspaceAgent := httpmw.WorkspaceAgentParam(r) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) devcontainer := chi.URLParam(r, "devcontainer") if devcontainer == "" { @@ -1238,7 +1194,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht apiAgent, err := db2sdk.WorkspaceAgent( api.DERPMap(), *api.TailnetCoordinator.Load(), - workspaceAgent, + waws.WorkspaceAgent, nil, nil, nil, @@ -1263,7 +1219,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht // don't get a response after 30s that the agent is unreachable. dialCtx, dialCancel := context.WithTimeout(ctx, 30*time.Second) defer dialCancel() - agentConn, release, err := api.agentProvider.AgentConn(dialCtx, workspaceAgent.ID) + agentConn, release, err := api.agentProvider.AgentConn(dialCtx, waws.WorkspaceAgent.ID) if err != nil { httpapi.Write(dialCtx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error dialing workspace agent.", @@ -1441,8 +1397,8 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R // This route accepts user API key auth and workspace proxy auth. The moon actor has // full permissions so should be able to pass this authz check. - workspace := httpmw.WorkspaceParam(r) - if !api.Authorize(r, policy.ActionSSH, workspace) { + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) + if !api.Authorize(r, policy.ActionSSH, waws) { httpapi.ResourceNotFound(rw) return } @@ -1482,7 +1438,6 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R api.WebsocketWaitGroup.Add(1) api.WebsocketWaitMutex.Unlock() defer api.WebsocketWaitGroup.Done() - workspaceAgent := httpmw.WorkspaceAgentParam(r) conn, err := websocket.Accept(rw, r, nil) if err != nil { @@ -1502,7 +1457,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R Name: "client", ID: peerID, Auth: tailnet.ClientCoordinateeAuth{ - AgentID: workspaceAgent.ID, + AgentID: waws.WorkspaceAgent.ID, }, }) if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) { @@ -1647,7 +1602,7 @@ func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { // convertProvisionedApps converts applications that are in the middle of provisioning process. // It means that they may not have an agent or workspace assigned (dry-run job). func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { - return db2sdk.Apps(dbApps, []database.WorkspaceAppStatus{}, database.WorkspaceAgent{}, "", database.Workspace{}) + return db2sdk.Apps(dbApps, []database.WorkspaceAppStatus{}, database.WorkspaceAgent{}, "", database.WorkspaceTable{}) } func convertLogSources(dbLogSources []database.WorkspaceAgentLogSource) []codersdk.WorkspaceAgentLogSource { @@ -1719,15 +1674,16 @@ func (api *API) watchWorkspaceAgentMetadata( defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - workspaceAgent := httpmw.WorkspaceAgentParam(r) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) log := api.Logger.Named("workspace_metadata_watcher").With( - slog.F("workspace_agent_id", workspaceAgent.ID), + slog.F("workspace_agent_id", waws.WorkspaceAgent.ID), + slog.F("workspace_id", waws.WorkspaceTable.ID), ) // Send metadata on updates, we must ensure subscription before sending // initial metadata to guarantee that events in-between are not missed. update := make(chan agentapi.WorkspaceAgentMetadataChannelPayload, 1) - cancelSub, err := api.Pubsub.Subscribe(agentapi.WatchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, byt []byte) { + cancelSub, err := api.Pubsub.Subscribe(agentapi.WatchWorkspaceAgentMetadataChannel(waws.WorkspaceAgent.ID), func(_ context.Context, byt []byte) { if ctx.Err() != nil { return } @@ -1758,7 +1714,7 @@ func (api *API) watchWorkspaceAgentMetadata( // We always use the original Request context because it contains // the RBAC actor. initialMD, err := api.Database.GetWorkspaceAgentMetadata(ctx, database.GetWorkspaceAgentMetadataParams{ - WorkspaceAgentID: workspaceAgent.ID, + WorkspaceAgentID: waws.WorkspaceAgent.ID, Keys: nil, }) if err != nil { @@ -1839,7 +1795,7 @@ func (api *API) watchWorkspaceAgentMetadata( return case payload := <-update: md, err := api.Database.GetWorkspaceAgentMetadata(ctx, database.GetWorkspaceAgentMetadataParams{ - WorkspaceAgentID: workspaceAgent.ID, + WorkspaceAgentID: waws.WorkspaceAgent.ID, Keys: payload.Keys, }) if err != nil { diff --git a/coderd/workspaceagents_internal_test.go b/coderd/workspaceagents_internal_test.go index c36fcf8698..29607731a0 100644 --- a/coderd/workspaceagents_internal_test.go +++ b/coderd/workspaceagents_internal_test.go @@ -96,8 +96,6 @@ func TestWatchAgentContainers(t *testing.T) { workspaceID = uuid.New() agentID = uuid.New() resourceID = uuid.New() - jobID = uuid.New() - buildID = uuid.New() containersCh = make(chan codersdk.WorkspaceAgentListContainersResponse) @@ -120,24 +118,17 @@ func TestWatchAgentContainers(t *testing.T) { api.agentProvider = fAgentProvider // Setup: Allow `ExtractWorkspaceAgentParams` to complete. - mDB.EXPECT().GetWorkspaceAgentByID(gomock.Any(), agentID).Return(database.WorkspaceAgent{ - ID: agentID, - ResourceID: resourceID, - LifecycleState: database.WorkspaceAgentLifecycleStateReady, - FirstConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - LastConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - }, nil) - mDB.EXPECT().GetWorkspaceResourceByID(gomock.Any(), resourceID).Return(database.WorkspaceResource{ - ID: resourceID, - JobID: jobID, - }, nil) - mDB.EXPECT().GetProvisionerJobByID(gomock.Any(), jobID).Return(database.ProvisionerJob{ - ID: jobID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - }, nil) - mDB.EXPECT().GetWorkspaceBuildByJobID(gomock.Any(), jobID).Return(database.WorkspaceBuild{ - WorkspaceID: workspaceID, - ID: buildID, + mDB.EXPECT().GetWorkspaceAgentAndWorkspaceByID(gomock.Any(), agentID).Return(database.GetWorkspaceAgentAndWorkspaceByIDRow{ + WorkspaceAgent: database.WorkspaceAgent{ + ID: agentID, + ResourceID: resourceID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + FirstConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, + LastConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, + }, + WorkspaceTable: database.WorkspaceTable{ + ID: workspaceID, + }, }, nil) // And: Allow `db2dsk.WorkspaceAgent` to complete. @@ -152,7 +143,7 @@ func TestWatchAgentContainers(t *testing.T) { }) // And: We mount the HTTP Handler - r.With(httpmw.ExtractWorkspaceAgentParam(mDB)). + r.With(httpmw.ExtractWorkspaceAgentAndWorkspaceParam(mDB)). Get("/workspaceagents/{workspaceagent}/containers/watch", api.watchWorkspaceAgentContainers) // Given: We create the HTTP server @@ -222,8 +213,6 @@ func TestWatchAgentContainers(t *testing.T) { workspaceID = uuid.New() agentID = uuid.New() resourceID = uuid.New() - jobID = uuid.New() - buildID = uuid.New() containersCh = make(chan codersdk.WorkspaceAgentListContainersResponse) @@ -246,24 +235,17 @@ func TestWatchAgentContainers(t *testing.T) { api.agentProvider = fAgentProvider // Setup: Allow `ExtractWorkspaceAgentParams` to complete. - mDB.EXPECT().GetWorkspaceAgentByID(gomock.Any(), agentID).Return(database.WorkspaceAgent{ - ID: agentID, - ResourceID: resourceID, - LifecycleState: database.WorkspaceAgentLifecycleStateReady, - FirstConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - LastConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - }, nil) - mDB.EXPECT().GetWorkspaceResourceByID(gomock.Any(), resourceID).Return(database.WorkspaceResource{ - ID: resourceID, - JobID: jobID, - }, nil) - mDB.EXPECT().GetProvisionerJobByID(gomock.Any(), jobID).Return(database.ProvisionerJob{ - ID: jobID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - }, nil) - mDB.EXPECT().GetWorkspaceBuildByJobID(gomock.Any(), jobID).Return(database.WorkspaceBuild{ - WorkspaceID: workspaceID, - ID: buildID, + mDB.EXPECT().GetWorkspaceAgentAndWorkspaceByID(gomock.Any(), agentID).Return(database.GetWorkspaceAgentAndWorkspaceByIDRow{ + WorkspaceAgent: database.WorkspaceAgent{ + ID: agentID, + ResourceID: resourceID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + FirstConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, + LastConnectedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, + }, + WorkspaceTable: database.WorkspaceTable{ + ID: workspaceID, + }, }, nil) // And: Allow `db2dsk.WorkspaceAgent` to complete. @@ -274,7 +256,7 @@ func TestWatchAgentContainers(t *testing.T) { Return(containersCh, io.NopCloser(&bytes.Buffer{}), nil) // And: We mount the HTTP Handler - r.With(httpmw.ExtractWorkspaceAgentParam(mDB)). + r.With(httpmw.ExtractWorkspaceAgentAndWorkspaceParam(mDB)). Get("/workspaceagents/{workspaceagent}/containers/watch", api.watchWorkspaceAgentContainers) // Given: We create the HTTP server diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 32c736f547..08b3cd8426 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -148,7 +148,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * aReq.dbReq = dbReq // Update audit request. - token.UserID = dbReq.User.ID + token.UserID = dbReq.UserID token.WorkspaceID = dbReq.Workspace.ID token.AgentID = dbReq.Agent.ID if dbReq.AppURL != nil { diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index ae421c9095..980ec7c3a6 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -188,10 +188,10 @@ func (r Request) Check() error { type databaseRequest struct { Request - // User is the user that owns the app. - User database.User + // UserID is the ID of the user that owns the app. + UserID uuid.UUID // Workspace is the workspace that the app is in. - Workspace database.Workspace + Workspace database.WorkspaceTable // Agent is the agent that the app is running on. Agent database.WorkspaceAgent // App is the app that the user is trying to access. @@ -420,8 +420,8 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR return &databaseRequest{ Request: r, - User: user, - Workspace: workspace, + UserID: user.ID, + Workspace: workspace.WorkspaceTable(), Agent: agent, App: app, AppURL: appURLParsed, @@ -443,40 +443,16 @@ func (r Request) getDatabaseTerminal(ctx context.Context, db database.Store) (*d } var err error - agent, err := db.GetWorkspaceAgentByID(ctx, agentID) + aw, err := db.GetWorkspaceAgentAndWorkspaceByID(ctx, agentID) if err != nil { - return nil, xerrors.Errorf("get workspace agent %q: %w", agentID, err) - } - - // Get the corresponding resource. - res, err := db.GetWorkspaceResourceByID(ctx, agent.ResourceID) - if err != nil { - return nil, xerrors.Errorf("get workspace agent resource %q: %w", agent.ResourceID, err) - } - - // Get the corresponding workspace build. - build, err := db.GetWorkspaceBuildByJobID(ctx, res.JobID) - if err != nil { - return nil, xerrors.Errorf("get workspace build by job ID %q: %w", res.JobID, err) - } - - // Get the corresponding workspace. - workspace, err := db.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - return nil, xerrors.Errorf("get workspace %q: %w", build.WorkspaceID, err) - } - - // Get the workspace's owner. - user, err := db.GetUserByID(ctx, workspace.OwnerID) - if err != nil { - return nil, xerrors.Errorf("get user %q: %w", workspace.OwnerID, err) + return nil, xerrors.Errorf("get workspace agent %q with workspace: %w", agentID, err) } return &databaseRequest{ Request: r, - User: user, - Workspace: workspace, - Agent: agent, + UserID: aw.WorkspaceTable.OwnerID, + Workspace: aw.WorkspaceTable, + Agent: aw.WorkspaceAgent, AppURL: nil, AppSharingLevel: database.AppSharingLevelOwner, }, nil diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 090d480cba..24f8224a36 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1252,7 +1252,7 @@ func (api *API) convertWorkspaceBuild( statuses := statusesByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID] apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, statuses, agent, workspace.OwnerUsername, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, statuses, agent, workspace.OwnerUsername, workspace.WorkspaceTable()), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil {