From 97e47001fdfac4c8394bebfade9c0d6b79097a98 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 11 Jun 2024 18:03:58 +0000 Subject: [PATCH 01/12] chore: accept payload on workspace usage route --- cli/portforward.go | 2 +- coderd/workspaces.go | 90 +++++++++++++++++++++++++++ coderd/workspacestats/tracker_test.go | 20 +++--- codersdk/deployment.go | 1 + codersdk/workspaces.go | 33 ++++++++-- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/cli/portforward.go b/cli/portforward.go index 4c0b1d772eecc..7ceedd2c88986 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -137,7 +137,7 @@ func (r *RootCmd) portForward() *serpent.Command { listeners[i] = l } - stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID) + stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID, nil) // Wait for the context to be canceled or for a signal and close // all listeners. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 1b3f076e8f4bf..a2a1a4b3ac4bd 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "slices" "strconv" "time" @@ -15,6 +16,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" @@ -1106,6 +1108,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Tags Workspaces // @Param workspace path string true "Workspace ID" format(uuid) +// @Param request body codersdk.PostWorkspaceUsageRequest false "Post workspace usage request" // @Success 204 // @Router /workspaces/{workspace}/usage [post] func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { @@ -1116,6 +1119,93 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { } api.statsReporter.TrackUsage(workspace.ID) + + if !api.Experiments.Enabled(codersdk.ExperimentWorkspaceUsage) { + // Continue previous behavior if the experiment is not enabled. + rw.WriteHeader(http.StatusNoContent) + return + } + + ctx := r.Context() + var req codersdk.PostWorkspaceUsageRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + if req.AgentID == uuid.Nil && req.AppName == "" { + // Continue previous behavior if body is empty. + rw.WriteHeader(http.StatusNoContent) + return + } + if req.AgentID == uuid.Nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request", + Validations: []codersdk.ValidationError{{ + Field: "agent_id", + Detail: "must be set when app_name is set", + }}, + }) + return + } + if req.AppName == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request", + Validations: []codersdk.ValidationError{{ + Field: "app_name", + Detail: "must be set when agent_id is set", + }}, + }) + return + } + if !slices.Contains(codersdk.AllowedAppNames, req.AppName) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request", + Validations: []codersdk.ValidationError{{ + Field: "app_name", + Detail: fmt.Sprintf("must be one of %v", codersdk.AllowedAppNames), + }}, + }) + return + } + + stat := &proto.Stats{} + switch req.AppName { + case codersdk.UsageAppNameVscode: + stat.SessionCountVscode = 1 + case codersdk.UsageAppNameJetbrains: + stat.SessionCountJetbrains = 1 + case codersdk.UsageAppNameReconnectingPty: + stat.SessionCountReconnectingPty = 1 + case codersdk.UsageAppNameSsh: + stat.SessionCountSsh = 1 + default: + // This means the app_name is not in the list of allowed app names. + httpapi.InternalServerError(rw, xerrors.Errorf("unknown app_name %q", req.AppName)) + return + } + + agent, err := api.Database.GetWorkspaceAgentByID(ctx, req.AgentID) + if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + httpapi.InternalServerError(rw, err) + return + } + + template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + err = api.statsReporter.ReportAgentStats(ctx, dbtime.Now(), workspace, agent, template.Name, stat) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + rw.WriteHeader(http.StatusNoContent) } diff --git a/coderd/workspacestats/tracker_test.go b/coderd/workspacestats/tracker_test.go index 99e9f9503b645..6a67e3da890a6 100644 --- a/coderd/workspacestats/tracker_test.go +++ b/coderd/workspacestats/tracker_test.go @@ -158,18 +158,18 @@ func TestTracker_MultipleInstances(t *testing.T) { } // Use client A to update LastUsedAt of the first three - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID, nil)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID, nil)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID, nil)) // Use client B to update LastUsedAt of the next three - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID, nil)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID, nil)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID, nil)) // The next two will have updated from both instances - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID, nil)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID, nil)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID, nil)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID, nil)) // The last two will not report any usage. // Tick both with different times and wait for both flushes to complete diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 21d33ebc81dc0..b67964d6a985c 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2233,6 +2233,7 @@ const ( ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature. ExperimentMultiOrganization Experiment = "multi-organization" // Requires organization context for interactions, default org is assumed. ExperimentCustomRoles Experiment = "custom-roles" // Allows creating runtime custom roles + ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking ) // ExperimentsAll should include all experiments that are safe for diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 0007e85de8ee4..7969f5b99ccee 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -316,10 +316,31 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx return nil } +type PostWorkspaceUsageRequest struct { + AgentID uuid.UUID `json:"agent_id"` + AppName UsageAppName `json:"app_name"` +} + +type UsageAppName string + +const ( + UsageAppNameVscode UsageAppName = "vscode" + UsageAppNameJetbrains UsageAppName = "jetbrains" + UsageAppNameReconnectingPty UsageAppName = "reconnecting-pty" + UsageAppNameSsh UsageAppName = "ssh" +) + +var AllowedAppNames = []UsageAppName{ + UsageAppNameVscode, + UsageAppNameJetbrains, + UsageAppNameReconnectingPty, + UsageAppNameSsh, +} + // PostWorkspaceUsage marks the workspace as having been used recently. -func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID) error { +func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req *PostWorkspaceUsageRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) - res, err := c.Request(ctx, http.MethodPost, path, nil) + res, err := c.Request(ctx, http.MethodPost, path, req) if err != nil { return xerrors.Errorf("post workspace usage: %w", err) } @@ -334,10 +355,11 @@ func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID) error { // with the given id in the background. // The caller is responsible for calling the returned function to stop the background // process. -func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, id uuid.UUID) func() { +func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID, req *PostWorkspaceUsageRequest) func() { hbCtx, hbCancel := context.WithCancel(ctx) // Perform one initial update - if err := c.PostWorkspaceUsage(hbCtx, id); err != nil { + err := c.PostWorkspaceUsage(hbCtx, workspaceID, req) + if err != nil { c.logger.Warn(ctx, "failed to post workspace usage", slog.Error(err)) } ticker := time.NewTicker(time.Minute) @@ -350,7 +372,8 @@ func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, id uuid.UUID) for { select { case <-ticker.C: - if err := c.PostWorkspaceUsage(hbCtx, id); err != nil { + err := c.PostWorkspaceUsage(hbCtx, workspaceID, req) + if err != nil { c.logger.Warn(ctx, "failed to post workspace usage in background", slog.Error(err)) } case <-hbCtx.Done(): From 61db1c2e0cce3c6be46c82b1a055d2fccdea1d91 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 11 Jun 2024 18:11:51 +0000 Subject: [PATCH 02/12] make gen --- coderd/apidoc/docs.go | 43 +++++++++++++++++++++++++++++++--- coderd/apidoc/swagger.json | 38 +++++++++++++++++++++++++++--- docs/api/schemas.md | 34 +++++++++++++++++++++++++++ docs/api/workspaces.md | 17 +++++++++++--- site/src/api/typesGenerated.ts | 19 ++++++++++++++- 5 files changed, 141 insertions(+), 10 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 81c16ba784798..865534de929a4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7503,6 +7503,14 @@ const docTemplate = `{ "name": "workspace", "in": "path", "required": true + }, + { + "description": "Post workspace usage request", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/codersdk.PostWorkspaceUsageRequest" + } } ], "responses": { @@ -9200,19 +9208,22 @@ const docTemplate = `{ "example", "auto-fill-parameters", "multi-organization", - "custom-roles" + "custom-roles", + "workspace-usage" ], "x-enum-comments": { "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentCustomRoles": "Allows creating runtime custom roles", "ExperimentExample": "This isn't used for anything.", - "ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed." + "ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed.", + "ExperimentWorkspaceUsage": "Enables the new workspace usage tracking" }, "x-enum-varnames": [ "ExperimentExample", "ExperimentAutoFillParameters", "ExperimentMultiOrganization", - "ExperimentCustomRoles" + "ExperimentCustomRoles", + "ExperimentWorkspaceUsage" ] }, "codersdk.ExternalAuth": { @@ -10150,6 +10161,17 @@ const docTemplate = `{ } } }, + "codersdk.PostWorkspaceUsageRequest": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "app_name": { + "$ref": "#/definitions/codersdk.UsageAppName" + } + } + }, "codersdk.PprofConfig": { "type": "object", "properties": { @@ -11902,6 +11924,21 @@ const docTemplate = `{ } } }, + "codersdk.UsageAppName": { + "type": "string", + "enum": [ + "vscode", + "jetbrains", + "reconnecting-pty", + "ssh" + ], + "x-enum-varnames": [ + "UsageAppNameVscode", + "UsageAppNameJetbrains", + "UsageAppNameReconnectingPty", + "UsageAppNameSsh" + ] + }, "codersdk.User": { "type": "object", "required": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7859bcb5ded02..1c40f26ce5ff9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6639,6 +6639,14 @@ "name": "workspace", "in": "path", "required": true + }, + { + "description": "Post workspace usage request", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/codersdk.PostWorkspaceUsageRequest" + } } ], "responses": { @@ -8236,19 +8244,22 @@ "example", "auto-fill-parameters", "multi-organization", - "custom-roles" + "custom-roles", + "workspace-usage" ], "x-enum-comments": { "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentCustomRoles": "Allows creating runtime custom roles", "ExperimentExample": "This isn't used for anything.", - "ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed." + "ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed.", + "ExperimentWorkspaceUsage": "Enables the new workspace usage tracking" }, "x-enum-varnames": [ "ExperimentExample", "ExperimentAutoFillParameters", "ExperimentMultiOrganization", - "ExperimentCustomRoles" + "ExperimentCustomRoles", + "ExperimentWorkspaceUsage" ] }, "codersdk.ExternalAuth": { @@ -9131,6 +9142,17 @@ } } }, + "codersdk.PostWorkspaceUsageRequest": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "app_name": { + "$ref": "#/definitions/codersdk.UsageAppName" + } + } + }, "codersdk.PprofConfig": { "type": "object", "properties": { @@ -10791,6 +10813,16 @@ } } }, + "codersdk.UsageAppName": { + "type": "string", + "enum": ["vscode", "jetbrains", "reconnecting-pty", "ssh"], + "x-enum-varnames": [ + "UsageAppNameVscode", + "UsageAppNameJetbrains", + "UsageAppNameReconnectingPty", + "UsageAppNameSsh" + ] + }, "codersdk.User": { "type": "object", "required": ["created_at", "email", "id", "username"], diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 348ce54e11ba3..5ef58f5e8b605 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2360,6 +2360,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `auto-fill-parameters` | | `multi-organization` | | `custom-roles` | +| `workspace-usage` | ## codersdk.ExternalAuth @@ -3359,6 +3360,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `icon` | string | false | | | | `name` | string | true | | | +## codersdk.PostWorkspaceUsageRequest + +```json +{ + "agent_id": "string", + "app_name": "vscode" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------- | ---------------------------------------------- | -------- | ------------ | ----------- | +| `agent_id` | string | false | | | +| `app_name` | [codersdk.UsageAppName](#codersdkusageappname) | false | | | + ## codersdk.PprofConfig ```json @@ -5227,6 +5244,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `share_level` | `authenticated` | | `share_level` | `public` | +## codersdk.UsageAppName + +```json +"vscode" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ------------------ | +| `vscode` | +| `jetbrains` | +| `reconnecting-pty` | +| `ssh` | + ## codersdk.User ```json diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 886f8401f7d7e..f48b83a5a4181 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -1397,16 +1397,27 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/usage \ + -H 'Content-Type: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` `POST /workspaces/{workspace}/usage` +> Body parameter + +```json +{ + "agent_id": "string", + "app_name": "vscode" +} +``` + ### Parameters -| Name | In | Type | Required | Description | -| ----------- | ---- | ------------ | -------- | ------------ | -| `workspace` | path | string(uuid) | true | Workspace ID | +| Name | In | Type | Required | Description | +| ----------- | ---- | ---------------------------------------------------------------------------------- | -------- | ---------------------------- | +| `workspace` | path | string(uuid) | true | Workspace ID | +| `body` | body | [codersdk.PostWorkspaceUsageRequest](schemas.md#codersdkpostworkspaceusagerequest) | false | Post workspace usage request | ### Responses diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a53717e3e0229..312f43dcd5bb4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -841,6 +841,12 @@ export interface PostOAuth2ProviderAppRequest { readonly icon: string; } +// From codersdk/workspaces.go +export interface PostWorkspaceUsageRequest { + readonly agent_id: string; + readonly app_name: UsageAppName; +} + // From codersdk/deployment.go export interface PprofConfig { readonly enable: boolean; @@ -1943,12 +1949,14 @@ export type Experiment = | "auto-fill-parameters" | "custom-roles" | "example" - | "multi-organization"; + | "multi-organization" + | "workspace-usage"; export const Experiments: Experiment[] = [ "auto-fill-parameters", "custom-roles", "example", "multi-organization", + "workspace-usage", ]; // From codersdk/deployment.go @@ -2243,6 +2251,15 @@ export const TemplateVersionWarnings: TemplateVersionWarning[] = [ "UNSUPPORTED_WORKSPACES", ]; +// From codersdk/workspaces.go +export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode"; +export const UsageAppNames: UsageAppName[] = [ + "jetbrains", + "reconnecting-pty", + "ssh", + "vscode", +]; + // From codersdk/users.go export type UserStatus = "active" | "dormant" | "suspended"; export const UserStatuses: UserStatus[] = ["active", "dormant", "suspended"]; From c2a9339d407b5a70a72258189f4890dce3bf8541 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 12 Jun 2024 15:21:56 +0000 Subject: [PATCH 03/12] lint and format --- coderd/workspaces.go | 2 +- codersdk/workspaces.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a2a1a4b3ac4bd..a0910ce0eccc3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1176,7 +1176,7 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { stat.SessionCountJetbrains = 1 case codersdk.UsageAppNameReconnectingPty: stat.SessionCountReconnectingPty = 1 - case codersdk.UsageAppNameSsh: + case codersdk.UsageAppNameSSH: stat.SessionCountSsh = 1 default: // This means the app_name is not in the list of allowed app names. diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 7969f5b99ccee..280ec41e3563b 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -317,7 +317,7 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx } type PostWorkspaceUsageRequest struct { - AgentID uuid.UUID `json:"agent_id"` + AgentID uuid.UUID `json:"agent_id" format:"uuid"` AppName UsageAppName `json:"app_name"` } @@ -327,14 +327,14 @@ const ( UsageAppNameVscode UsageAppName = "vscode" UsageAppNameJetbrains UsageAppName = "jetbrains" UsageAppNameReconnectingPty UsageAppName = "reconnecting-pty" - UsageAppNameSsh UsageAppName = "ssh" + UsageAppNameSSH UsageAppName = "ssh" ) var AllowedAppNames = []UsageAppName{ UsageAppNameVscode, UsageAppNameJetbrains, UsageAppNameReconnectingPty, - UsageAppNameSsh, + UsageAppNameSSH, } // PostWorkspaceUsage marks the workspace as having been used recently. From cc226eb2371e698c308939d4deddd708b3663ddf Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 12 Jun 2024 16:00:57 +0000 Subject: [PATCH 04/12] accepts --- coderd/apidoc/docs.go | 8 ++++++-- coderd/apidoc/swagger.json | 6 ++++-- coderd/workspaces.go | 1 + docs/api/schemas.md | 2 +- docs/api/workspaces.md | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 865534de929a4..23389633f0b87 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7490,6 +7490,9 @@ const docTemplate = `{ "CoderSessionToken": [] } ], + "consumes": [ + "application/json" + ], "tags": [ "Workspaces" ], @@ -10165,7 +10168,8 @@ const docTemplate = `{ "type": "object", "properties": { "agent_id": { - "type": "string" + "type": "string", + "format": "uuid" }, "app_name": { "$ref": "#/definitions/codersdk.UsageAppName" @@ -11936,7 +11940,7 @@ const docTemplate = `{ "UsageAppNameVscode", "UsageAppNameJetbrains", "UsageAppNameReconnectingPty", - "UsageAppNameSsh" + "UsageAppNameSSH" ] }, "codersdk.User": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1c40f26ce5ff9..d6233524067fd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6628,6 +6628,7 @@ "CoderSessionToken": [] } ], + "consumes": ["application/json"], "tags": ["Workspaces"], "summary": "Post Workspace Usage by ID", "operationId": "post-workspace-usage-by-id", @@ -9146,7 +9147,8 @@ "type": "object", "properties": { "agent_id": { - "type": "string" + "type": "string", + "format": "uuid" }, "app_name": { "$ref": "#/definitions/codersdk.UsageAppName" @@ -10820,7 +10822,7 @@ "UsageAppNameVscode", "UsageAppNameJetbrains", "UsageAppNameReconnectingPty", - "UsageAppNameSsh" + "UsageAppNameSSH" ] }, "codersdk.User": { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a0910ce0eccc3..4c22f679229d4 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1107,6 +1107,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { // @ID post-workspace-usage-by-id // @Security CoderSessionToken // @Tags Workspaces +// @Accept json // @Param workspace path string true "Workspace ID" format(uuid) // @Param request body codersdk.PostWorkspaceUsageRequest false "Post workspace usage request" // @Success 204 diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 5ef58f5e8b605..905cb45479b54 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3364,7 +3364,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ```json { - "agent_id": "string", + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", "app_name": "vscode" } ``` diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index f48b83a5a4181..f16d9be857fef 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -1407,7 +1407,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/usage \ ```json { - "agent_id": "string", + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", "app_name": "vscode" } ``` From f954f2b3315854dbbcfc053bf6026bb39a2c50ea Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 12 Jun 2024 17:57:09 +0000 Subject: [PATCH 05/12] tests --- cli/portforward.go | 2 +- coderd/workspaces_test.go | 93 +++++++++++++++++++++++++++ coderd/workspacestats/tracker_test.go | 20 +++--- codersdk/workspaces.go | 4 +- 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/cli/portforward.go b/cli/portforward.go index 7ceedd2c88986..aebad3a5b2da5 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -137,7 +137,7 @@ func (r *RootCmd) portForward() *serpent.Command { listeners[i] = l } - stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID, nil) + stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{}) // Wait for the context to be canceled or for a signal and close // all listeners. diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index a20a26d2ab161..0dbd3aa969239 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3371,3 +3371,96 @@ func TestWorkspaceFavoriteUnfavorite(t *testing.T) { require.ErrorAs(t, err, &sdkErr) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) } + +func TestWorkspaceUsageTracking(t *testing.T) { + t.Parallel() + t.Run("NoExperiment", func(t *testing.T) { + t.Parallel() + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + tmpDir := t.TempDir() + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Directory = tmpDir + return agents + }).Do() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + // continue legacy behavior + err := client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) + require.NoError(t, err) + }) + t.Run("Experiment", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)} + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + user := coderdtest.CreateFirstUser(t, client) + tmpDir := t.TempDir() + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Directory = tmpDir + return agents + }).Do() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + // continue legacy behavior + err := client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) + require.NoError(t, err) + + workspace, err := client.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + + // only agent id fails + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + }) + require.ErrorContains(t, err, "agent_id") + // only app name fails + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AppName: "ssh", + }) + require.ErrorContains(t, err, "app_name") + // unknown app name fails + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + AppName: "unknown", + }) + require.ErrorContains(t, err, "app_name") + + // vscode works + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + AppName: "vscode", + }) + require.NoError(t, err) + // jetbrains works + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + AppName: "jetbrains", + }) + require.NoError(t, err) + // reconnecting-pty works + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + AppName: "reconnecting-pty", + }) + require.NoError(t, err) + // ssh works + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + AppName: "ssh", + }) + require.NoError(t, err) + }) +} diff --git a/coderd/workspacestats/tracker_test.go b/coderd/workspacestats/tracker_test.go index 6a67e3da890a6..9be1b85781f95 100644 --- a/coderd/workspacestats/tracker_test.go +++ b/coderd/workspacestats/tracker_test.go @@ -158,18 +158,18 @@ func TestTracker_MultipleInstances(t *testing.T) { } // Use client A to update LastUsedAt of the first three - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID, nil)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID, nil)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID, nil)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) // Use client B to update LastUsedAt of the next three - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID, nil)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID, nil)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID, nil)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) // The next two will have updated from both instances - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID, nil)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID, nil)) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID, nil)) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID, nil)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) // The last two will not report any usage. // Tick both with different times and wait for both flushes to complete diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 280ec41e3563b..61651ed88ce14 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -338,7 +338,7 @@ var AllowedAppNames = []UsageAppName{ } // PostWorkspaceUsage marks the workspace as having been used recently. -func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req *PostWorkspaceUsageRequest) error { +func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req PostWorkspaceUsageRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) res, err := c.Request(ctx, http.MethodPost, path, req) if err != nil { @@ -355,7 +355,7 @@ func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req *Post // with the given id in the background. // The caller is responsible for calling the returned function to stop the background // process. -func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID, req *PostWorkspaceUsageRequest) func() { +func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID, req PostWorkspaceUsageRequest) func() { hbCtx, hbCancel := context.WithCancel(ctx) // Perform one initial update err := c.PostWorkspaceUsage(hbCtx, workspaceID, req) From 8623ae4028dd97c3e6a1511a19eea72a55cda675 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 12 Jun 2024 18:37:06 +0000 Subject: [PATCH 06/12] comment --- coderd/workspaces.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4c22f679229d4..1f598cbf4ce06 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1180,7 +1180,8 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { case codersdk.UsageAppNameSSH: stat.SessionCountSsh = 1 default: - // This means the app_name is not in the list of allowed app names. + // This means the app_name is in the codersdk.AllowedAppNames but not being + // handled by this switch statement. httpapi.InternalServerError(rw, xerrors.Errorf("unknown app_name %q", req.AppName)) return } From e4652121474c575497e34d00f06626c2eed57520 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 18:44:03 +0000 Subject: [PATCH 07/12] new client method --- cli/portforward.go | 2 +- coderd/workspaces_test.go | 22 ++++++---- coderd/workspacestats/tracker_test.go | 20 ++++----- codersdk/workspaces.go | 60 ++++++++++++++++++++++++--- 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/cli/portforward.go b/cli/portforward.go index aebad3a5b2da5..4c0b1d772eecc 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -137,7 +137,7 @@ func (r *RootCmd) portForward() *serpent.Command { listeners[i] = l } - stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{}) + stopUpdating := client.UpdateWorkspaceUsageContext(ctx, workspace.ID) // Wait for the context to be canceled or for a signal and close // all listeners. diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 0dbd3aa969239..61535d31147b4 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3391,7 +3391,9 @@ func TestWorkspaceUsageTracking(t *testing.T) { defer cancel() // continue legacy behavior - err := client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) + err := client.PostWorkspaceUsage(ctx, r.Workspace.ID) + require.NoError(t, err) + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) require.NoError(t, err) }) t.Run("Experiment", func(t *testing.T) { @@ -3415,49 +3417,51 @@ func TestWorkspaceUsageTracking(t *testing.T) { defer cancel() // continue legacy behavior - err := client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) + err := client.PostWorkspaceUsage(ctx, r.Workspace.ID) + require.NoError(t, err) + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) require.NoError(t, err) workspace, err := client.Workspace(ctx, r.Workspace.ID) require.NoError(t, err) // only agent id fails - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, }) require.ErrorContains(t, err, "agent_id") // only app name fails - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AppName: "ssh", }) require.ErrorContains(t, err, "app_name") // unknown app name fails - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, AppName: "unknown", }) require.ErrorContains(t, err, "app_name") // vscode works - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, AppName: "vscode", }) require.NoError(t, err) // jetbrains works - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, AppName: "jetbrains", }) require.NoError(t, err) // reconnecting-pty works - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, AppName: "reconnecting-pty", }) require.NoError(t, err) // ssh works - err = client.PostWorkspaceUsage(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, AppName: "ssh", }) diff --git a/coderd/workspacestats/tracker_test.go b/coderd/workspacestats/tracker_test.go index 9be1b85781f95..99e9f9503b645 100644 --- a/coderd/workspacestats/tracker_test.go +++ b/coderd/workspacestats/tracker_test.go @@ -158,18 +158,18 @@ func TestTracker_MultipleInstances(t *testing.T) { } // Use client A to update LastUsedAt of the first three - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[0].Workspace.ID)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[1].Workspace.ID)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[2].Workspace.ID)) // Use client B to update LastUsedAt of the next three - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[3].Workspace.ID)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[4].Workspace.ID)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[5].Workspace.ID)) // The next two will have updated from both instances - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) - require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID, codersdk.PostWorkspaceUsageRequest{})) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[6].Workspace.ID)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[6].Workspace.ID)) + require.NoError(t, clientA.PostWorkspaceUsage(ctx, w[7].Workspace.ID)) + require.NoError(t, clientB.PostWorkspaceUsage(ctx, w[7].Workspace.ID)) // The last two will not report any usage. // Tick both with different times and wait for both flushes to complete diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 61651ed88ce14..73c6827313485 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -337,8 +337,8 @@ var AllowedAppNames = []UsageAppName{ UsageAppNameSSH, } -// PostWorkspaceUsage marks the workspace as having been used recently. -func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req PostWorkspaceUsageRequest) error { +// PostWorkspaceUsage marks the workspace as having been used recently and records an app stat. +func (c *Client) PostWorkspaceUsageWithBody(ctx context.Context, id uuid.UUID, req PostWorkspaceUsageRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) res, err := c.Request(ctx, http.MethodPost, path, req) if err != nil { @@ -351,14 +351,64 @@ func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID, req PostW return nil } +// PostWorkspaceUsage marks the workspace as having been used recently. +func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID) error { + path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) + res, err := c.Request(ctx, http.MethodPost, path, nil) + if err != nil { + return xerrors.Errorf("post workspace usage: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + +// UpdateWorkspaceUsageWithBodyContext periodically posts workspace usage for the workspace +// with the given id and app name in the background. +// The caller is responsible for calling the returned function to stop the background +// process. +func (c *Client) UpdateWorkspaceUsageWithBodyContext(ctx context.Context, workspaceID uuid.UUID, req PostWorkspaceUsageRequest) func() { + hbCtx, hbCancel := context.WithCancel(ctx) + // Perform one initial update + err := c.PostWorkspaceUsageWithBody(hbCtx, workspaceID, req) + if err != nil { + c.logger.Warn(ctx, "failed to post workspace usage", slog.Error(err)) + } + ticker := time.NewTicker(time.Minute) + doneCh := make(chan struct{}) + go func() { + defer func() { + ticker.Stop() + close(doneCh) + }() + for { + select { + case <-ticker.C: + err := c.PostWorkspaceUsageWithBody(hbCtx, workspaceID, req) + if err != nil { + c.logger.Warn(ctx, "failed to post workspace usage in background", slog.Error(err)) + } + case <-hbCtx.Done(): + return + } + } + }() + return func() { + hbCancel() + <-doneCh + } +} + // UpdateWorkspaceUsageContext periodically posts workspace usage for the workspace // with the given id in the background. // The caller is responsible for calling the returned function to stop the background // process. -func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID, req PostWorkspaceUsageRequest) func() { +func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID) func() { hbCtx, hbCancel := context.WithCancel(ctx) // Perform one initial update - err := c.PostWorkspaceUsage(hbCtx, workspaceID, req) + err := c.PostWorkspaceUsage(hbCtx, workspaceID) if err != nil { c.logger.Warn(ctx, "failed to post workspace usage", slog.Error(err)) } @@ -372,7 +422,7 @@ func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uu for { select { case <-ticker.C: - err := c.PostWorkspaceUsage(hbCtx, workspaceID, req) + err := c.PostWorkspaceUsage(hbCtx, workspaceID) if err != nil { c.logger.Warn(ctx, "failed to post workspace usage in background", slog.Error(err)) } From 17b25b946a5400d2874d24e8c74e805fc7727f9d Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 20:05:26 +0000 Subject: [PATCH 08/12] test activity bump --- coderd/workspaces.go | 10 +++++++++- coderd/workspaces_test.go | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 1f598cbf4ce06..22a269fc5fb7f 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1127,6 +1127,12 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { return } + if r.Body == http.NoBody { + // Continue previous behavior if no body is present. + rw.WriteHeader(http.StatusNoContent) + return + } + ctx := r.Context() var req codersdk.PostWorkspaceUsageRequest if !httpapi.Read(ctx, rw, r, &req) { @@ -1169,7 +1175,9 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { return } - stat := &proto.Stats{} + stat := &proto.Stats{ + ConnectionCount: 1, + } switch req.AppName { case codersdk.UsageAppNameVscode: stat.SessionCountVscode = 1 diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 61535d31147b4..75c706f4c9bd3 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3398,6 +3398,8 @@ func TestWorkspaceUsageTracking(t *testing.T) { }) t.Run("Experiment", func(t *testing.T) { t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)} client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ @@ -3405,19 +3407,37 @@ func TestWorkspaceUsageTracking(t *testing.T) { }) user := coderdtest.CreateFirstUser(t, client) tmpDir := t.TempDir() + org := dbgen.Organization(t, db, database.Organization{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.UserID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: org.ID, + CreatedBy: user.UserID, + }) + template := dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + ActiveVersionID: templateVersion.ID, + CreatedBy: user.UserID, + DefaultTTL: int64(8 * time.Hour), + }) + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + ActivityBumpMillis: int64(1 * time.Hour), + }) + require.NoError(t, err) r := dbfake.WorkspaceBuild(t, db, database.Workspace{ OrganizationID: user.OrganizationID, OwnerID: user.UserID, + TemplateID: template.ID, + Ttl: sql.NullInt64{Valid: true, Int64: int64(8 * time.Hour)}, }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { agents[0].Directory = tmpDir return agents }).Do() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) - defer cancel() - // continue legacy behavior - err := client.PostWorkspaceUsage(ctx, r.Workspace.ID) + err = client.PostWorkspaceUsage(ctx, r.Workspace.ID) require.NoError(t, err) err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{}) require.NoError(t, err) @@ -3466,5 +3486,12 @@ func TestWorkspaceUsageTracking(t *testing.T) { AppName: "ssh", }) require.NoError(t, err) + + // ensure deadline has been bumped + newWorkspace, err := client.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + require.True(t, workspace.LatestBuild.Deadline.Valid) + require.True(t, newWorkspace.LatestBuild.Deadline.Valid) + require.Greater(t, newWorkspace.LatestBuild.Deadline.Time, workspace.LatestBuild.Deadline.Time) }) } From 0c7247ca5382254c6f257229d3ae174f0997aa1e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 20:09:32 +0000 Subject: [PATCH 09/12] deprecation notice --- codersdk/workspaces.go | 1 + 1 file changed, 1 insertion(+) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 73c6827313485..ab727408a502f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -352,6 +352,7 @@ func (c *Client) PostWorkspaceUsageWithBody(ctx context.Context, id uuid.UUID, r } // PostWorkspaceUsage marks the workspace as having been used recently. +// Deprecated: use PostWorkspaceUsageWithBody instead func (c *Client) PostWorkspaceUsage(ctx context.Context, id uuid.UUID) error { path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) res, err := c.Request(ctx, http.MethodPost, path, nil) From 01256ce0222582213256caa97c91e4eba79c3d8a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 20:10:11 +0000 Subject: [PATCH 10/12] another deprecation notice --- codersdk/workspaces.go | 1 + 1 file changed, 1 insertion(+) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index ab727408a502f..69472f8d4579d 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -406,6 +406,7 @@ func (c *Client) UpdateWorkspaceUsageWithBodyContext(ctx context.Context, worksp // with the given id in the background. // The caller is responsible for calling the returned function to stop the background // process. +// Deprecated: use UpdateWorkspaceUsageContextWithBody instead func (c *Client) UpdateWorkspaceUsageContext(ctx context.Context, workspaceID uuid.UUID) func() { hbCtx, hbCancel := context.WithCancel(ctx) // Perform one initial update From 11e633c8d21da568d55d3d80a633b9e3d9be4d3a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 20:26:48 +0000 Subject: [PATCH 11/12] fix pg test --- coderd/workspaces_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 75c706f4c9bd3..986c8ad1ebd9b 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3423,7 +3423,7 @@ func TestWorkspaceUsageTracking(t *testing.T) { DefaultTTL: int64(8 * time.Hour), }) _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - ActivityBumpMillis: int64(1 * time.Hour), + ActivityBumpMillis: int64(1 * time.Hour.Milliseconds()), }) require.NoError(t, err) r := dbfake.WorkspaceBuild(t, db, database.Workspace{ From 19584762d685d8a3066408791ad475ba9656d1d2 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 13 Jun 2024 20:47:33 +0000 Subject: [PATCH 12/12] try fix pg test --- coderd/workspaces_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 986c8ad1ebd9b..e5a01df9f8edc 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3423,7 +3423,7 @@ func TestWorkspaceUsageTracking(t *testing.T) { DefaultTTL: int64(8 * time.Hour), }) _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - ActivityBumpMillis: int64(1 * time.Hour.Milliseconds()), + ActivityBumpMillis: 8 * time.Hour.Milliseconds(), }) require.NoError(t, err) r := dbfake.WorkspaceBuild(t, db, database.Workspace{ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy