Skip to content

Commit 35f6967

Browse files
authored
fix: replace ssh config block in-place (#211)
Fixes #210
1 parent 343d097 commit 35f6967

File tree

2 files changed

+63
-14
lines changed

2 files changed

+63
-14
lines changed

src/sshConfig.test.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ Host coder-vscode--*
9595
ProxyCommand some-command-here
9696
StrictHostKeyChecking no
9797
UserKnownHostsFile /dev/null
98-
# --- END CODER VSCODE ---`
98+
# --- END CODER VSCODE ---
99+
100+
Host *
101+
SetEnv TEST=1`
99102
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
100103

101104
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
@@ -124,7 +127,10 @@ Host coder--updated--vscode--*
124127
ProxyCommand some-command-here
125128
StrictHostKeyChecking no
126129
UserKnownHostsFile /dev/null
127-
# --- END CODER VSCODE ---`
130+
# --- END CODER VSCODE ---
131+
132+
Host *
133+
SetEnv TEST=1`
128134

129135
expect(mockFileSystem.writeFile).toBeCalledWith(sshFilePath, expectedOutput, {
130136
encoding: "utf-8",
@@ -168,6 +174,40 @@ Host coder-vscode--*
168174
})
169175
})
170176

177+
it("it does not remove a user-added block that only matches the host of an old coder SSH config", async () => {
178+
const existentSSHConfig = `Host coder-vscode--*
179+
ForwardAgent=yes`
180+
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
181+
182+
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
183+
await sshConfig.load()
184+
await sshConfig.update({
185+
Host: "coder-vscode--*",
186+
ProxyCommand: "some-command-here",
187+
ConnectTimeout: "0",
188+
StrictHostKeyChecking: "no",
189+
UserKnownHostsFile: "/dev/null",
190+
LogLevel: "ERROR",
191+
})
192+
193+
const expectedOutput = `Host coder-vscode--*
194+
ForwardAgent=yes
195+
196+
# --- START CODER VSCODE ---
197+
Host coder-vscode--*
198+
ConnectTimeout 0
199+
LogLevel ERROR
200+
ProxyCommand some-command-here
201+
StrictHostKeyChecking no
202+
UserKnownHostsFile /dev/null
203+
# --- END CODER VSCODE ---`
204+
205+
expect(mockFileSystem.writeFile).toBeCalledWith(sshFilePath, expectedOutput, {
206+
encoding: "utf-8",
207+
mode: 384,
208+
})
209+
})
210+
171211
it("override values", async () => {
172212
mockFileSystem.readFile.mockRejectedValueOnce("No file found")
173213
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)

src/sshConfig.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,21 @@ export class SSHConfig {
107107
// old configs
108108
this.cleanUpOldConfig()
109109
const block = this.getBlock()
110+
const newBlock = this.buildBlock(values, overrides)
110111
if (block) {
111-
this.eraseBlock(block)
112+
this.replaceBlock(block, newBlock)
113+
} else {
114+
this.appendBlock(newBlock)
112115
}
113-
this.appendBlock(values, overrides)
114116
await this.save()
115117
}
116118

117119
private async cleanUpOldConfig() {
118120
const raw = this.getRaw()
119121
const oldConfig = raw.split("\n\n").find((config) => config.startsWith("Host coder-vscode--*"))
120-
if (oldConfig) {
122+
// Perform additional sanity check that the block also contains a
123+
// ProxyCommand, otherwise it might be a different block.
124+
if (oldConfig && oldConfig.includes(" ProxyCommand ")) {
121125
this.raw = raw.replace(oldConfig, "")
122126
}
123127
}
@@ -149,13 +153,8 @@ export class SSHConfig {
149153
}
150154
}
151155

152-
private eraseBlock(block: Block) {
153-
this.raw = this.getRaw().replace(block.raw, "")
154-
}
155-
156156
/**
157-
*
158-
* appendBlock builds the ssh config block. The order of the keys is determinstic based on the input.
157+
* buildBlock builds the ssh config block. The order of the keys is determinstic based on the input.
159158
* Expected values are always in a consistent order followed by any additional overrides in sorted order.
160159
*
161160
* @param param0 - SSHValues are the expected SSH values for using ssh with coder.
@@ -164,7 +163,7 @@ export class SSHConfig {
164163
* If the key matches an expected value, the expected value is overridden. If it does not
165164
* match an expected value, it is appended to the end of the block.
166165
*/
167-
private appendBlock({ Host, ...otherValues }: SSHValues, overrides: Record<string, string>) {
166+
private buildBlock({ Host, ...otherValues }: SSHValues, overrides: Record<string, string>): Block {
168167
const lines = [this.startBlockComment, `Host ${Host}`]
169168

170169
// configValues is the merged values of the defaults and the overrides.
@@ -180,12 +179,22 @@ export class SSHConfig {
180179
})
181180

182181
lines.push(this.endBlockComment)
182+
return {
183+
raw: lines.join("\n"),
184+
}
185+
}
186+
187+
private replaceBlock(oldBlock: Block, newBlock: Block) {
188+
this.raw = this.getRaw().replace(oldBlock.raw, newBlock.raw)
189+
}
190+
191+
private appendBlock(block: Block) {
183192
const raw = this.getRaw()
184193

185194
if (this.raw === "") {
186-
this.raw = lines.join("\n")
195+
this.raw = block.raw
187196
} else {
188-
this.raw = `${raw.trimEnd()}\n\n${lines.join("\n")}`
197+
this.raw = `${raw.trimEnd()}\n\n${block.raw}`
189198
}
190199
}
191200

0 commit comments

Comments
 (0)