Skip to content

fix: Inject coder ssh configuration between comments #48

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 23 commits into from
Jan 27, 2023
Merged
Changes from 1 commit
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
84 changes: 54 additions & 30 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ import * as ws from "ws"
import { Storage } from "./storage"

export class Remote {
// Prefix is a magic string that is prepended to SSH
// hosts to indicate that they should be handled by
// this extension.
// Prefix is a magic string that is prepended to SSH hosts to indicate that
// they should be handled by this extension.
public static readonly Prefix = "coder-vscode--"

public constructor(
Expand All @@ -35,16 +34,17 @@ export class Remote {

public async setup(remoteAuthority: string): Promise<vscode.Disposable | undefined> {
const authorityParts = remoteAuthority.split("+")
// If the URI passed doesn't have the proper prefix
// ignore it. We don't need to do anything special,
// because this isn't trying to open a Coder workspace.
// If the URI passed doesn't have the proper prefix ignore it. We don't need
// to do anything special, because this isn't trying to open a Coder
// workspace.
if (!authorityParts[1].startsWith(Remote.Prefix)) {
return
}
const sshAuthority = authorityParts[1].substring(Remote.Prefix.length)

// Authorities are in the format: coder-vscode--<username>--<workspace>--<agent>
// Agent can be omitted then will be prompted for instead.
// Authorities are in the format:
// coder-vscode--<username>--<workspace>--<agent> Agent can be omitted then
// will be prompted for instead.
const parts = sshAuthority.split("--")
if (parts.length < 2 || parts.length > 3) {
throw new Error(`Invalid Coder SSH authority. Must be: <username>--<workspace>--<agent?>`)
Expand Down Expand Up @@ -142,12 +142,12 @@ export class Remote {
}
}

// If a build is running we should stream the logs to the user so
// they can watch what's going on!
// If a build is running we should stream the logs to the user so they can
// watch what's going on!
if (workspace.latest_build.status === "pending" || workspace.latest_build.status === "starting") {
const writeEmitter = new vscode.EventEmitter<string>()
// We use a terminal instead of an output channel because it feels
// more familiar to a user!
// We use a terminal instead of an output channel because it feels more
// familiar to a user!
const terminal = vscode.window.createTerminal({
name: "Build Log",
location: vscode.TerminalLocation.Panel,
Expand Down Expand Up @@ -218,8 +218,8 @@ export class Remote {
agent = agents[0]
}

// If there are multiple agents, we should select one here!
// TODO: Support multiple agents!
// If there are multiple agents, we should select one here! TODO: Support
// multiple agents!
}

if (!agent) {
Expand Down Expand Up @@ -337,11 +337,11 @@ export class Remote {
return
}

// This ensures the Remote SSH extension resolves
// the host to execute the Coder binary properly.
// This ensures the Remote SSH extension resolves the host to execute the
// Coder binary properly.
//
// If we didn't write to the SSH config file,
// connecting would fail with "Host not found".
// If we didn't write to the SSH config file, connecting would fail with
// "Host not found".
await this.updateSSHConfig(authorityParts[1])

this.findSSHProcessID().then((pid) => {
Expand Down Expand Up @@ -372,9 +372,11 @@ export class Remote {
}
}

// updateSSHConfig updates the SSH configuration with a wildcard
// that handles all Coder entries.
// updateSSHConfig updates the SSH configuration with a wildcard that handles
// all Coder entries.
private async updateSSHConfig(sshHost: string) {
const startBlockComment = "# --- START CODER VSCODE ---"
const endBlockComment = "# --- END CODER VSCODE ---"
let sshConfigFile = vscode.workspace.getConfiguration().get<string>("remote.SSH.configFile")
if (!sshConfigFile) {
sshConfigFile = path.join(os.homedir(), ".ssh", "config")
Expand All @@ -386,7 +388,27 @@ export class Remote {
// Probably just doesn't exist!
sshConfigRaw = ""
}
const parsedConfig = SSHConfig.parse(sshConfigRaw)

// We are going to extract only the coder config
let coderConfigRaw = ""
const startOfCoderConfig = sshConfigRaw.indexOf(startBlockComment)
const endOfCoderConfig = sshConfigRaw.lastIndexOf(endBlockComment)

// There is an existent coder config
if (startOfCoderConfig > -1 && endOfCoderConfig > -1) {
coderConfigRaw = sshConfigRaw
.substring(startOfCoderConfig, endOfCoderConfig)
.replace(startBlockComment, "")
.replace(endBlockComment, "")
// We are going to override the configuration so we can remove it
// including the comments from the ssh config
sshConfigRaw = sshConfigRaw
.replace(sshConfigRaw.substring(startOfCoderConfig, endOfCoderConfig), "")
.replace(startBlockComment, "")
.replace(endBlockComment, "")
}

const parsedConfig = SSHConfig.parse(coderConfigRaw)
const computedHost = parsedConfig.compute(sshHost)

let binaryPath: string | undefined
Expand Down Expand Up @@ -415,11 +437,14 @@ export class Remote {
})

await ensureDir(path.dirname(sshConfigFile))
await fs.writeFile(sshConfigFile, parsedConfig.toString())
const sshConfigContent = [sshConfigRaw.trimEnd(), startBlockComment, parsedConfig.toString(), endBlockComment].join(
"\n",
)
await fs.writeFile(sshConfigFile, sshConfigContent, "utf-8")
}

// showNetworkUpdates finds the SSH process ID that is being used by
// this workspace and reads the file being created by the Coder CLI.
// showNetworkUpdates finds the SSH process ID that is being used by this
// workspace and reads the file being created by the Coder CLI.
private showNetworkUpdates(sshPid: number): vscode.Disposable {
const networkStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1000)
const networkInfoFile = path.join(this.storage.getNetworkInfoPath(), `${sshPid}.json`)
Expand Down Expand Up @@ -510,14 +535,13 @@ export class Remote {
}
}

// findSSHProcessID returns the currently active SSH process ID
// that is powering the remote SSH connection.
// findSSHProcessID returns the currently active SSH process ID that is
// powering the remote SSH connection.
private async findSSHProcessID(timeout = 15000): Promise<number | undefined> {
const search = async (logPath: string): Promise<number | undefined> => {
// This searches for the socksPort that Remote SSH is connecting to.
// We do this to find the SSH process that is powering this connection.
// That SSH process will be logging network information periodically to
// a file.
// This searches for the socksPort that Remote SSH is connecting to. We do
// this to find the SSH process that is powering this connection. That SSH
// process will be logging network information periodically to a file.
const text = await fs.readFile(logPath, "utf8")
const matches = text.match(/-> socksPort (\d+) ->/)
if (!matches) {
Expand Down