diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 55c4973cb74b2..49e6735c4cf06 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -607,12 +607,12 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } if arg.Limit > 0 { if int(arg.Limit) > len(workspaces) { - return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil } workspaces = workspaces[:arg.Limit] } - return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil } // mapAgentStatus determines the agent status based on different timestamps like created_at, last_connected_at, disconnected_at, etc. @@ -649,10 +649,10 @@ func mapAgentStatus(dbAgent database.WorkspaceAgent, agentInactiveDisconnectTime return status } -func convertToWorkspaceRows(workspaces []database.Workspace, count int64) []database.GetWorkspacesRow { - rows := make([]database.GetWorkspacesRow, len(workspaces)) - for i, w := range workspaces { - rows[i] = database.GetWorkspacesRow{ +func (q *fakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspaces []database.Workspace, count int64) []database.GetWorkspacesRow { + rows := make([]database.GetWorkspacesRow, 0, len(workspaces)) + for _, w := range workspaces { + wr := database.GetWorkspacesRow{ ID: w.ID, CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, @@ -666,6 +666,28 @@ func convertToWorkspaceRows(workspaces []database.Workspace, count int64) []data LastUsedAt: w.LastUsedAt, Count: count, } + + for _, t := range q.templates { + if t.ID == w.TemplateID { + wr.TemplateName = t.Name + break + } + } + + if build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, w.ID); err == nil { + for _, tv := range q.templateVersions { + if tv.ID == build.TemplateVersionID { + wr.TemplateVersionID = tv.ID + wr.TemplateVersionName = sql.NullString{ + Valid: true, + String: tv.Name, + } + break + } + } + } + + rows = append(rows, wr) } return rows } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 4ff6e6e2d154a..28a56b825f34e 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -236,6 +236,9 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.Ttl, &i.LastUsedAt, &i.LockedAt, + &i.TemplateName, + &i.TemplateVersionID, + &i.TemplateVersionName, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 94d643f25bb58..7bbb5dd2d3404 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8391,7 +8391,11 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace const getWorkspaces = `-- name: GetWorkspaces :many SELECT - 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, COUNT(*) OVER () as count + 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, + COALESCE(template_name.template_name, 'unknown') as template_name, + latest_build.template_version_id, + latest_build.template_version_name, + COUNT(*) OVER () as count FROM workspaces JOIN @@ -8401,6 +8405,8 @@ ON LEFT JOIN LATERAL ( SELECT workspace_builds.transition, + workspace_builds.template_version_id, + template_versions.name AS template_version_name, provisioner_jobs.id AS provisioner_job_id, provisioner_jobs.started_at, provisioner_jobs.updated_at, @@ -8413,6 +8419,10 @@ LEFT JOIN LATERAL ( provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id + LEFT JOIN + template_versions + ON + template_versions.id = workspace_builds.template_version_id WHERE workspace_builds.workspace_id = workspaces.id ORDER BY @@ -8420,6 +8430,14 @@ LEFT JOIN LATERAL ( LIMIT 1 ) latest_build ON TRUE +LEFT JOIN LATERAL ( + SELECT + templates.name AS template_name + FROM + templates + WHERE + templates.id = workspaces.template_id +) template_name ON true WHERE -- Optionally include deleted workspaces workspaces.deleted = $1 @@ -8491,13 +8509,13 @@ WHERE -- Filter by owner_id AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - owner_id = $3 + workspaces.owner_id = $3 ELSE true END -- Filter by owner_name AND CASE WHEN $4 :: text != '' THEN - owner_id = (SELECT id FROM users WHERE lower(username) = lower($4) AND deleted = false) + workspaces.owner_id = (SELECT id FROM users WHERE lower(username) = lower($4) AND deleted = false) ELSE true END -- Filter by template_name @@ -8505,19 +8523,19 @@ WHERE -- Use the organization filter to restrict to 1 org if needed. AND CASE WHEN $5 :: text != '' THEN - template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($5) AND deleted = false) + workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($5) AND deleted = false) ELSE true END -- Filter by template_ids AND CASE WHEN array_length($6 :: uuid[], 1) > 0 THEN - template_id = ANY($6) + workspaces.template_id = ANY($6) ELSE true END -- Filter by name, matching on substring AND CASE WHEN $7 :: text != '' THEN - name ILIKE '%' || $7 || '%' + workspaces.name ILIKE '%' || $7 || '%' ELSE true END -- Filter by agent status @@ -8565,7 +8583,7 @@ ORDER BY latest_build.error IS NULL AND latest_build.transition = 'start'::workspace_transition) DESC, LOWER(users.username) ASC, - LOWER(name) ASC + LOWER(workspaces.name) ASC LIMIT CASE WHEN $11 :: integer > 0 THEN @@ -8590,19 +8608,22 @@ type GetWorkspacesParams struct { } type GetWorkspacesRow struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - Deleted bool `db:"deleted" json:"deleted"` - Name string `db:"name" json:"name"` - AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` - Ttl sql.NullInt64 `db:"ttl" json:"ttl"` - LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` - LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` - Count int64 `db:"count" json:"count"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + Deleted bool `db:"deleted" json:"deleted"` + Name string `db:"name" json:"name"` + AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` + LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` + Count int64 `db:"count" json:"count"` } func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { @@ -8639,6 +8660,9 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.Ttl, &i.LastUsedAt, &i.LockedAt, + &i.TemplateName, + &i.TemplateVersionID, + &i.TemplateVersionName, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 5bcfa02bf17c9..ef3d84adebfec 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -75,7 +75,11 @@ WHERE -- name: GetWorkspaces :many SELECT - workspaces.*, COUNT(*) OVER () as count + workspaces.*, + COALESCE(template_name.template_name, 'unknown') as template_name, + latest_build.template_version_id, + latest_build.template_version_name, + COUNT(*) OVER () as count FROM workspaces JOIN @@ -85,6 +89,8 @@ ON LEFT JOIN LATERAL ( SELECT workspace_builds.transition, + workspace_builds.template_version_id, + template_versions.name AS template_version_name, provisioner_jobs.id AS provisioner_job_id, provisioner_jobs.started_at, provisioner_jobs.updated_at, @@ -97,6 +103,10 @@ LEFT JOIN LATERAL ( provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id + LEFT JOIN + template_versions + ON + template_versions.id = workspace_builds.template_version_id WHERE workspace_builds.workspace_id = workspaces.id ORDER BY @@ -104,6 +114,14 @@ LEFT JOIN LATERAL ( LIMIT 1 ) latest_build ON TRUE +LEFT JOIN LATERAL ( + SELECT + templates.name AS template_name + FROM + templates + WHERE + templates.id = workspaces.template_id +) template_name ON true WHERE -- Optionally include deleted workspaces workspaces.deleted = @deleted @@ -175,13 +193,13 @@ WHERE -- Filter by owner_id AND CASE WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - owner_id = @owner_id + workspaces.owner_id = @owner_id ELSE true END -- Filter by owner_name AND CASE WHEN @owner_username :: text != '' THEN - owner_id = (SELECT id FROM users WHERE lower(username) = lower(@owner_username) AND deleted = false) + workspaces.owner_id = (SELECT id FROM users WHERE lower(username) = lower(@owner_username) AND deleted = false) ELSE true END -- Filter by template_name @@ -189,19 +207,19 @@ WHERE -- Use the organization filter to restrict to 1 org if needed. AND CASE WHEN @template_name :: text != '' THEN - template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower(@template_name) AND deleted = false) + workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower(@template_name) AND deleted = false) ELSE true END -- Filter by template_ids AND CASE WHEN array_length(@template_ids :: uuid[], 1) > 0 THEN - template_id = ANY(@template_ids) + workspaces.template_id = ANY(@template_ids) ELSE true END -- Filter by name, matching on substring AND CASE WHEN @name :: text != '' THEN - name ILIKE '%' || @name || '%' + workspaces.name ILIKE '%' || @name || '%' ELSE true END -- Filter by agent status @@ -249,7 +267,7 @@ ORDER BY latest_build.error IS NULL AND latest_build.transition = 'start'::workspace_transition) DESC, LOWER(users.username) ASC, - LOWER(name) ASC + LOWER(workspaces.name) ASC LIMIT CASE WHEN @limit_ :: integer > 0 THEN diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index 6ad5f528db41a..454934f1dd7b4 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -15,7 +15,6 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/db2sdk" "github.com/coder/coder/coderd/database/dbauthz" @@ -153,7 +152,7 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis Subsystem: "agents", Name: "up", Help: "The number of active agents per workspace.", - }, []string{usernameLabel, workspaceNameLabel})) + }, []string{usernameLabel, workspaceNameLabel, "template_name", "template_version"})) err := registerer.Register(agentsGauge) if err != nil { return nil, err @@ -234,29 +233,35 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis } for _, workspace := range workspaceRows { + templateName := workspace.TemplateName + templateVersionName := workspace.TemplateVersionName.String + if !workspace.TemplateVersionName.Valid { + templateVersionName = "unknown" + } + user, err := db.GetUserByID(ctx, workspace.OwnerID) if err != nil { logger.Error(ctx, "can't get user from the database", slog.F("user_id", workspace.OwnerID), slog.Error(err)) - agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name) + agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name, templateName, templateVersionName) continue } agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) if err != nil { logger.Error(ctx, "can't get workspace agents", slog.F("workspace_id", workspace.ID), slog.Error(err)) - agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name) + agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name, templateName, templateVersionName) continue } if len(agents) == 0 { logger.Debug(ctx, "workspace agents are unavailable", slog.F("workspace_id", workspace.ID)) - agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name) + agentsGauge.WithLabelValues(VectorOperationAdd, 0, user.Username, workspace.Name, templateName, templateVersionName) continue } for _, agent := range agents { // Collect information about agents - agentsGauge.WithLabelValues(VectorOperationAdd, 1, user.Username, workspace.Name) + agentsGauge.WithLabelValues(VectorOperationAdd, 1, user.Username, workspace.Name, templateName, templateVersionName) connectionStatus := agent.Status(agentInactiveDisconnectTimeout) node := (*coordinator.Load()).Node(agent.ID) diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index 9101288cca570..5b53fcaa047e4 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -312,7 +312,7 @@ func TestAgents(t *testing.T) { // when closeFunc, err := prometheusmetrics.Agents(ctx, slogtest.Make(t, &slogtest.Options{ IgnoreErrors: true, - }), registry, db, &coordinatorPtr, derpMap, agentInactiveDisconnectTimeout, time.Millisecond) + }), registry, db, &coordinatorPtr, derpMap, agentInactiveDisconnectTimeout, 50*time.Millisecond) require.NoError(t, err) t.Cleanup(closeFunc) @@ -332,8 +332,10 @@ func TestAgents(t *testing.T) { for _, metric := range metrics { switch metric.GetName() { case "coderd_agents_up": - assert.Equal(t, "testuser", metric.Metric[0].Label[0].GetValue()) // Username - assert.Equal(t, workspace.Name, metric.Metric[0].Label[1].GetValue()) // Workspace name + assert.Equal(t, template.Name, metric.Metric[0].Label[0].GetValue()) // Template name + assert.Equal(t, version.Name, metric.Metric[0].Label[1].GetValue()) // Template version name + assert.Equal(t, "testuser", metric.Metric[0].Label[2].GetValue()) // Username + assert.Equal(t, workspace.Name, metric.Metric[0].Label[3].GetValue()) // Workspace name assert.Equal(t, 1, int(metric.Metric[0].Gauge.GetValue())) // Metric value agentsUp = true case "coderd_agents_connections":