From 9d00a26a907f19869aae995bd0bf2ec67d83de76 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 3 Jun 2024 12:29:50 -0500 Subject: [PATCH] fix: add missing route for `codersdk.PostLogSource` (#13421) --- coderd/apidoc/docs.go | 54 ++++++++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 48 ++++++++++++++++++++++++++++++ coderd/apikey/apikey_test.go | 2 +- coderd/coderd.go | 1 + coderd/workspaceagents.go | 50 +++++++++++++++++++++++++++++++ coderd/workspaceagents_test.go | 42 ++++++++++++++++++++++++++ coderd/workspaceapps/proxy.go | 2 +- codersdk/agentsdk/agentsdk.go | 4 +-- docs/api/agents.md | 52 ++++++++++++++++++++++++++++++++ docs/api/schemas.md | 18 ++++++++++++ 10 files changed, 269 insertions(+), 4 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9b73496b9c..c5e2a60415 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5871,6 +5871,45 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/log-source": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Post workspace agent log source", + "operationId": "post-workspace-agent-log-source", + "parameters": [ + { + "description": "Log source request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PostLogSourceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgentLogSource" + } + } + } + } + }, "/workspaceagents/me/logs": { "patch": { "security": [ @@ -8112,6 +8151,21 @@ const docTemplate = `{ } } }, + "agentsdk.PostLogSourceRequest": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "description": "ID is a unique identifier for the log source.\nIt is scoped to a workspace agent, and can be statically\ndefined inside code to prevent duplicate sources from being\ncreated for the same agent.", + "type": "string" + } + } + }, "agentsdk.PostMetadataRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index de2ae4ffc3..66afad1f04 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5179,6 +5179,39 @@ } } }, + "/workspaceagents/me/log-source": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Post workspace agent log source", + "operationId": "post-workspace-agent-log-source", + "parameters": [ + { + "description": "Log source request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PostLogSourceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgentLogSource" + } + } + } + } + }, "/workspaceagents/me/logs": { "patch": { "security": [ @@ -7184,6 +7217,21 @@ } } }, + "agentsdk.PostLogSourceRequest": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "description": "ID is a unique identifier for the log source.\nIt is scoped to a workspace agent, and can be statically\ndefined inside code to prevent duplicate sources from being\ncreated for the same agent.", + "type": "string" + } + } + }, "agentsdk.PostMetadataRequest": { "type": "object", "properties": { diff --git a/coderd/apikey/apikey_test.go b/coderd/apikey/apikey_test.go index 734a187219..41f64fe0d8 100644 --- a/coderd/apikey/apikey_test.go +++ b/coderd/apikey/apikey_test.go @@ -128,7 +128,7 @@ func TestGenerate(t *testing.T) { // Assert that the hashed secret is correct. hashed := sha256.Sum256([]byte(keytokens[1])) - assert.ElementsMatch(t, hashed, key.HashedSecret[:]) + assert.ElementsMatch(t, hashed, key.HashedSecret) assert.Equal(t, tc.params.UserID, key.UserID) assert.WithinDuration(t, dbtime.Now(), key.CreatedAt, time.Second*5) diff --git a/coderd/coderd.go b/coderd/coderd.go index 25763530db..98b1686171 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1021,6 +1021,7 @@ func New(options *Options) *API { r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle) r.Post("/metadata", api.workspaceAgentPostMetadata) r.Post("/metadata/{key}", api.workspaceAgentPostMetadataDeprecated) + r.Post("/log-source", api.workspaceAgentPostLogSource) }) r.Route("/{workspaceagent}", func(r chi.Router) { r.Use( diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 753e3aeaa0..c45fae8726 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1084,6 +1084,56 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R } } +// @Summary Post workspace agent log source +// @ID post-workspace-agent-log-source +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Agents +// @Param request body agentsdk.PostLogSourceRequest true "Log source request" +// @Success 200 {object} codersdk.WorkspaceAgentLogSource +// @Router /workspaceagents/me/log-source [post] +func (api *API) workspaceAgentPostLogSource(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req agentsdk.PostLogSourceRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + workspaceAgent := httpmw.WorkspaceAgent(r) + + sources, err := api.Database.InsertWorkspaceAgentLogSources(ctx, database.InsertWorkspaceAgentLogSourcesParams{ + WorkspaceAgentID: workspaceAgent.ID, + CreatedAt: dbtime.Now(), + ID: []uuid.UUID{req.ID}, + DisplayName: []string{req.DisplayName}, + Icon: []string{req.Icon}, + }) + if err != nil { + if database.IsUniqueViolation(err, "workspace_agent_log_sources_pkey") { + httpapi.Write(ctx, rw, http.StatusCreated, codersdk.WorkspaceAgentLogSource{ + WorkspaceAgentID: workspaceAgent.ID, + CreatedAt: dbtime.Now(), + ID: req.ID, + DisplayName: req.DisplayName, + Icon: req.Icon, + }) + return + } + httpapi.InternalServerError(rw, err) + return + } + + if len(sources) != 1 { + httpapi.InternalServerError(rw, xerrors.Errorf("database should've returned 1 row, got %d", len(sources))) + return + } + + apiSource := convertLogSources(sources)[0] + + httpapi.Write(ctx, rw, http.StatusCreated, apiSource) +} + // 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 { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index e99b6a297c..7052d59144 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -921,6 +921,48 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, manifest.Apps[1].Health) } +func TestWorkspaceAgentPostLogSource(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitShort) + + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + + req := agentsdk.PostLogSourceRequest{ + ID: uuid.New(), + DisplayName: "colin logs", + Icon: "/emojis/1f42e.png", + } + + res, err := agentClient.PostLogSource(ctx, req) + require.NoError(t, err) + assert.Equal(t, req.ID, res.ID) + assert.Equal(t, req.DisplayName, res.DisplayName) + assert.Equal(t, req.Icon, res.Icon) + assert.NotZero(t, res.WorkspaceAgentID) + assert.NotZero(t, res.CreatedAt) + + // should be idempotent + res, err = agentClient.PostLogSource(ctx, req) + require.NoError(t, err) + assert.Equal(t, req.ID, res.ID) + assert.Equal(t, req.DisplayName, res.DisplayName) + assert.Equal(t, req.Icon, res.Icon) + assert.NotZero(t, res.WorkspaceAgentID) + assert.NotZero(t, res.CreatedAt) + }) +} + // TestWorkspaceAgentReportStats tests the legacy (agent API v1) report stats endpoint. func TestWorkspaceAgentReportStats(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index 7bf470a3cc..69f1aadca4 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -573,7 +573,7 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT } // This strips the session token from a workspace app request. - cookieHeaders := r.Header.Values("Cookie")[:] + cookieHeaders := r.Header.Values("Cookie") r.Header.Del("Cookie") for _, cookieHeader := range cookieHeaders { r.Header.Add("Cookie", httpapi.StripCoderCookies(cookieHeader)) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 5dcccca09e..f3a09c5357 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -533,7 +533,7 @@ func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error { return nil } -type PostLogSource struct { +type PostLogSourceRequest struct { // ID is a unique identifier for the log source. // It is scoped to a workspace agent, and can be statically // defined inside code to prevent duplicate sources from being @@ -543,7 +543,7 @@ type PostLogSource struct { Icon string `json:"icon"` } -func (c *Client) PostLogSource(ctx context.Context, req PostLogSource) (codersdk.WorkspaceAgentLogSource, error) { +func (c *Client) PostLogSource(ctx context.Context, req PostLogSourceRequest) (codersdk.WorkspaceAgentLogSource, error) { res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/log-source", req) if err != nil { return codersdk.WorkspaceAgentLogSource{}, err diff --git a/docs/api/agents.md b/docs/api/agents.md index 0d73ca9262..13e5c38590 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -341,6 +341,58 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Post workspace agent log source + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/log-source \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /workspaceagents/me/log-source` + +> Body parameter + +```json +{ + "display_name": "string", + "icon": "string", + "id": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | ------------------------------------------------------------------------ | -------- | ------------------ | +| `body` | body | [agentsdk.PostLogSourceRequest](schemas.md#agentsdkpostlogsourcerequest) | true | Log source request | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "display_name": "string", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentLogSource](schemas.md#codersdkworkspaceagentlogsource) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Patch workspace agent logs ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 82804508b0..7770b09187 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -403,6 +403,24 @@ | `changed_at` | string | false | | | | `state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | +## agentsdk.PostLogSourceRequest + +```json +{ + "display_name": "string", + "icon": "string", + "id": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------ | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `display_name` | string | false | | | +| `icon` | string | false | | | +| `id` | string | false | | ID is a unique identifier for the log source. It is scoped to a workspace agent, and can be statically defined inside code to prevent duplicate sources from being created for the same agent. | + ## agentsdk.PostMetadataRequest ```json