Skip to content

feat: Add feature to allow config-ssh value overrides #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
getWorkspaceBuildLogs,
getWorkspaceByOwnerAndName,
startWorkspace,
getDeploymentSSHConfig,
} from "coder/site/src/api/api"
import { ProvisionerJobLog, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { ProvisionerJobLog, SSHConfigResponse, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import EventSource from "eventsource"
import find from "find-process"
import * as fs from "fs/promises"
Expand All @@ -18,7 +19,7 @@ import prettyBytes from "pretty-bytes"
import * as semver from "semver"
import * as vscode from "vscode"
import * as ws from "ws"
import { SSHConfig } from "./sshConfig"
import { SSHConfig, defaultSSHConfigResponse } from "./sshConfig"
import { Storage } from "./storage"

export class Remote {
Expand Down Expand Up @@ -440,6 +441,37 @@ export class Remote {
// updateSSHConfig updates the SSH configuration with a wildcard that handles
// all Coder entries.
private async updateSSHConfig() {
let deploymentConfig: SSHConfigResponse = defaultSSHConfigResponse
try {
deploymentConfig = await getDeploymentSSHConfig()
} catch (error) {
if (!axios.isAxiosError(error)) {
throw error
}
switch (error.response?.status) {
case 404: {
// Deployment does not support overriding ssh config yet. Likely an
// older version, just use the default.
deploymentConfig = defaultSSHConfigResponse
break
}
case 401: {
const result = await this.vscodeProposed.window.showInformationMessage(
"Your session expired...",
{
useCustom: true,
modal: true,
detail: "You must login again to access your workspace.",
},
"Login",
)
throw error
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the best way to handle this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just throws some error if there is an unauthorized response code. It shouldn't happen because we are logged in, so it's an edge case that we don't need to handle all too well?

default:
throw error
}
}

let sshConfigFile = vscode.workspace.getConfiguration().get<string>("remote.SSH.configFile")
if (!sshConfigFile) {
sshConfigFile = path.join(os.homedir(), ".ssh", "config")
Expand Down Expand Up @@ -480,7 +512,7 @@ export class Remote {
SetEnv: "CODER_SSH_SESSION_TYPE=vscode",
}

await sshConfig.update(sshValues)
await sshConfig.update(sshValues, deploymentConfig)
}

// showNetworkUpdates finds the SSH process ID that is being used by this
Expand Down
38 changes: 38 additions & 0 deletions src/sshConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,41 @@ Host coder-vscode--*
mode: 384,
})
})

it("override values", async () => {
mockFileSystem.readFile.mockRejectedValueOnce("No file found")
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem, {
ssh_config_options: {
loglevel: "DEBUG", // This tests case insensitive
ConnectTimeout: "500",
ExtraKey: "ExtraValue",
Foo: "bar",
Buzz: "baz",
},
hostname_prefix: "",
})
await sshConfig.load()
await sshConfig.update({
Host: "coder-vscode--*",
ProxyCommand: "some-command-here",
ConnectTimeout: "0",
StrictHostKeyChecking: "no",
UserKnownHostsFile: "/dev/null",
LogLevel: "ERROR",
})

const expectedOutput = `# --- START CODER VSCODE ---
Host coder-vscode--*
ProxyCommand some-command-here
ConnectTimeout 500
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel DEBUG
Buzz baz
ExtraKey ExtraValue
Foo bar
# --- END CODER VSCODE ---`

expect(mockFileSystem.readFile).toBeCalledWith(sshFilePath, expect.anything())
expect(mockFileSystem.writeFile).toBeCalledWith(sshFilePath, expectedOutput, expect.anything())
})
50 changes: 47 additions & 3 deletions src/sshConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SSHConfigResponse } from "coder/site/src/api/typesGenerated"
import { writeFile, readFile } from "fs/promises"
import { ensureDir } from "fs-extra"
import path from "path"
Expand Down Expand Up @@ -30,6 +31,12 @@ const defaultFileSystem: FileSystem = {
writeFile,
}

export const defaultSSHConfigResponse: SSHConfigResponse = {
ssh_config_options: {},
// The prefix is not used by the vscode-extension
hostname_prefix: "coder.",
}

export class SSHConfig {
private filePath: string
private fileSystem: FileSystem
Expand All @@ -51,15 +58,15 @@ export class SSHConfig {
}
}

async update(values: SSHValues) {
async update(values: SSHValues, overrides: SSHConfigResponse = defaultSSHConfigResponse) {
// We should remove this in March 2023 because there is not going to have
// old configs
this.cleanUpOldConfig()
const block = this.getBlock()
if (block) {
this.eraseBlock(block)
}
this.appendBlock(values)
this.appendBlock(values, overrides.ssh_config_options)
await this.save()
}

Expand Down Expand Up @@ -102,12 +109,49 @@ export class SSHConfig {
this.raw = this.getRaw().replace(block.raw, "")
}

private appendBlock({ Host, ...otherValues }: SSHValues) {
/**
*
* appendBlock builds the ssh config block. The order of the keys is determinstic based on the input.
* Expected values are always in a consistent order followed by any additional overrides in sorted order.
*
* @param param0 - SSHValues are the expected SSH values for using ssh with coder.
* @param overrides - Overrides typically come from the deployment api and are used to override the default values.
* The overrides are given as key:value pairs where the key is the ssh config file key.
* If the key matches an expected value, the expected value is overridden. If it does not
* match an expected value, it is appended to the end of the block.
*/
private appendBlock({ Host, ...otherValues }: SSHValues, overrides: Record<string, string>) {
const lines = [this.startBlockComment, `Host ${Host}`]
// We need to do a case insensitive match for the overrides as ssh config keys are case insensitive.
// To get the correct key:value, use:
// key = caseInsensitiveOverrides[key.toLowerCase()]
// value = overrides[key]
const caseInsensitiveOverrides: Record<string, string> = {}
Object.keys(overrides).forEach((key) => {
caseInsensitiveOverrides[key.toLowerCase()] = key
})

const keys = Object.keys(otherValues) as Array<keyof typeof otherValues>
keys.forEach((key) => {
const lower = key.toLowerCase()
if (caseInsensitiveOverrides[lower]) {
const correctCaseKey = caseInsensitiveOverrides[lower]
// If the key is in overrides, use the override value.
// Doing it this way maintains the default order of the keys.
lines.push(this.withIndentation(`${key} ${overrides[correctCaseKey]}`))
// Remove the key from the overrides so we don't write it again.
delete caseInsensitiveOverrides[lower]
return
}
lines.push(this.withIndentation(`${key} ${otherValues[key]}`))
})
// Write remaining overrides that have not been written yet. Sort to maintain deterministic order.
const remainingKeys = (Object.keys(caseInsensitiveOverrides) as Array<keyof typeof caseInsensitiveOverrides>).sort()
remainingKeys.forEach((key) => {
const correctKey = caseInsensitiveOverrides[key]
lines.push(this.withIndentation(`${key} ${overrides[correctKey]}`))
})

lines.push(this.endBlockComment)
const raw = this.getRaw()

Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,7 @@ [email protected]:

"coder@https://github.com/coder/coder":
version "0.0.0"
resolved "https://github.com/coder/coder#7a1731b6205d9c68f6308ee362ff2d62124b6950"
resolved "https://github.com/coder/coder#a6fa8cac582f2fc54eca0191bd54fd43d6d67ac2"

collapse-white-space@^1.0.2:
version "1.0.6"
Expand Down