Skip to content

Commit

Permalink
Use Axios client for EventSource
Browse files Browse the repository at this point in the history
This allows client TLS certificates to be used for event monitoring.
Upgrade to a newer `eventsource` package that supports a custom `fetch`
function, and provide a custom `fetch` function which wraps Axios.
  • Loading branch information
aaronlehmann committed Feb 28, 2025
1 parent d6b798e commit 56ce33c
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 30 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

## Unreleased

## [v1.4.1](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-02-19)
- Use Axios client to receive event stream so TLS settings are properly applied.

### Fixed

## [v1.4.1](https://github.com/coder/vscode-coder/releases/tag/v1.4.1) (2025-02-19)

- Recreate REST client in spots where confirmStart may have waited indefinitely.

## [v1.4.0](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-02-04)
## [v1.4.0](https://github.com/coder/vscode-coder/releases/tag/v1.4.0) (2025-02-04)

- Recreate REST client after starting a workspace to ensure fresh TLS certificates.
- Use `coder ssh` subcommand in place of `coder vscodessh`.

## [v1.3.10](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-01-17)
## [v1.3.10](https://github.com/coder/vscode-coder/releases/tag/v1.3.10) (2025-01-17)

- Fix bug where checking for overridden properties incorrectly converted host name pattern to regular expression.

Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@
],
"menus": {
"commandPalette": [
{
"command": "coder.openFromSidebar",
"when": "false"
}
{
"command": "coder.openFromSidebar",
"when": "false"
}
],
"view/title": [
{
Expand Down Expand Up @@ -275,7 +275,7 @@
"test:ci": "CI=true yarn test"
},
"devDependencies": {
"@types/eventsource": "^1.1.15",
"@types/eventsource": "^3.0.0",
"@types/glob": "^7.1.3",
"@types/node": "^18.0.0",
"@types/node-forge": "^1.3.11",
Expand Down Expand Up @@ -309,7 +309,7 @@
"dependencies": {
"axios": "1.7.7",
"date-fns": "^3.6.0",
"eventsource": "^2.0.2",
"eventsource": "^3.0.5",
"find-process": "^1.4.7",
"jsonc-parser": "^3.3.1",
"memfs": "^4.9.3",
Expand Down
3 changes: 3 additions & 0 deletions src/api-helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { ErrorEvent } from "eventsource"
import { z } from "zod"

export function errToStr(error: unknown, def: string) {
Expand All @@ -9,6 +10,8 @@ export function errToStr(error: unknown, def: string) {
return error.response.data.message
} else if (isApiErrorResponse(error)) {
return error.message
} else if (error instanceof ErrorEvent) {
return error.code ? `${error.code}: ${error.message}` : error.message?.toString() || def
} else if (typeof error === "string" && error.trim().length > 0) {
return error
}
Expand Down
58 changes: 58 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AxiosInstance } from "axios"
import { spawn } from "child_process"
import { Api } from "coder/site/src/api/api"
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
import { FetchLikeInit } from "eventsource"
import fs from "fs/promises"
import { ProxyAgent } from "proxy-agent"
import * as vscode from "vscode"
Expand Down Expand Up @@ -120,6 +122,60 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
return restClient
}

/**
* Creates a fetch adapter using an Axios instance that returns streaming responses.
* This can be used with APIs that accept fetch-like interfaces.
*/
export function createStreamingFetchAdapter(axiosInstance: AxiosInstance) {
return async (url: string | URL, init?: FetchLikeInit) => {
const urlStr = url.toString()

const response = await axiosInstance.request({
url: urlStr,
headers: init?.headers as Record<string, string>,
responseType: "stream",
validateStatus: () => true, // Don't throw on any status code
})
const stream = new ReadableStream({
start(controller) {
response.data.on("data", (chunk: Buffer) => {
controller.enqueue(chunk)
})

response.data.on("end", () => {
controller.close()
})

response.data.on("error", (err: Error) => {
controller.error(err)
})
},

cancel() {
response.data.destroy()
return Promise.resolve()
},
})

const createReader = () => stream.getReader()

return {
body: {
getReader: () => createReader(),
},
url: urlStr,
status: response.status,
redirected: response.request.res.responseUrl !== urlStr,
headers: {
get: (name: string) => {
const value = response.headers[name.toLowerCase()]
return value === undefined ? null : String(value)
},
},
}
}
}

/**
* Start or update a workspace and return the updated workspace.
*/
Expand Down Expand Up @@ -212,6 +268,7 @@ export async function waitForBuild(
path += `&after=${logs[logs.length - 1].id}`
}

const agent = await createHttpAgent()
await new Promise<void>((resolve, reject) => {
try {
const baseUrl = new URL(baseUrlRaw)
Expand All @@ -224,6 +281,7 @@ export async function waitForBuild(
| undefined,
},
followRedirects: true,
agent: agent,
})
socket.binaryType = "nodebuffer"
socket.on("message", (data) => {
Expand Down
12 changes: 4 additions & 8 deletions src/workspaceMonitor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Api } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import { formatDistanceToNowStrict } from "date-fns"
import EventSource from "eventsource"
import { EventSource } from "eventsource"
import * as vscode from "vscode"
import { createStreamingFetchAdapter } from "./api"
import { errToStr } from "./api-helper"
import { Storage } from "./storage"

Expand Down Expand Up @@ -40,16 +41,11 @@ export class WorkspaceMonitor implements vscode.Disposable {
) {
this.name = `${workspace.owner_name}/${workspace.name}`
const url = this.restClient.getAxiosInstance().defaults.baseURL
const token = this.restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as
| string
| undefined
const watchUrl = new URL(`${url}/api/v2/workspaces/${workspace.id}/watch`)
this.storage.writeToCoderOutputChannel(`Monitoring ${this.name}...`)

const eventSource = new EventSource(watchUrl.toString(), {
headers: {
"Coder-Session-Token": token,
},
fetch: createStreamingFetchAdapter(this.restClient.getAxiosInstance()),
})

eventSource.addEventListener("data", (event) => {
Expand All @@ -64,7 +60,7 @@ export class WorkspaceMonitor implements vscode.Disposable {
})

eventSource.addEventListener("error", (event) => {
this.notifyError(event.data)
this.notifyError(event)
})

// Store so we can close in dispose().
Expand Down
8 changes: 3 additions & 5 deletions src/workspacesProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Api } from "coder/site/src/api/api"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import EventSource from "eventsource"
import { EventSource } from "eventsource"
import * as path from "path"
import * as vscode from "vscode"
import { createStreamingFetchAdapter } from "./api"
import {
AgentMetadataEvent,
AgentMetadataEventSchemaArray,
Expand Down Expand Up @@ -228,12 +229,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
function monitorMetadata(agentId: WorkspaceAgent["id"], restClient: Api): AgentWatcher {
// TODO: Is there a better way to grab the url and token?
const url = restClient.getAxiosInstance().defaults.baseURL
const token = restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as string | undefined
const metadataUrl = new URL(`${url}/api/v2/workspaceagents/${agentId}/watch-metadata`)
const eventSource = new EventSource(metadataUrl.toString(), {
headers: {
"Coder-Session-Token": token,
},
fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()),
})

let disposed = false
Expand Down
25 changes: 17 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -537,10 +537,12 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==

"@types/eventsource@^1.1.15":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.15.tgz#949383d3482e20557cbecbf3b038368d94b6be27"
integrity sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==
"@types/eventsource@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-3.0.0.tgz#6b1b50c677032fd3be0b5c322e8ae819b3df62eb"
integrity sha512-yEhFj31FTD29DtNeqePu+A+lD6loRef6YOM5XfN1kUwBHyy2DySGlA3jJU+FbQSkrfmlBVluf2Dub/OyReFGKA==
dependencies:
eventsource "*"

"@types/glob@^7.1.3":
version "7.2.0"
Expand Down Expand Up @@ -2529,10 +2531,17 @@ events@^3.2.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==

eventsource@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508"
integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==
eventsource-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57"
integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==

eventsource@*, eventsource@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.5.tgz#0cae1eee2d2c75894de8b02a91d84e5c57f0cc5a"
integrity sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==
dependencies:
eventsource-parser "^3.0.0"

exec@^0.2.1:
version "0.2.1"
Expand Down

0 comments on commit 56ce33c

Please sign in to comment.