Skip to content

Commit 35fb327

Browse files
committed
User smaller, pinned busybox image
Signed-off-by: David Gageot <[email protected]>
1 parent fade4ae commit 35fb327

File tree

11 files changed

+121
-320
lines changed

11 files changed

+121
-320
lines changed

src/extension/ui/src/Constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ export const CATALOG_LAYOUT_SX = {
2121

2222
export const ASSIGNED_SECRET_PLACEHOLDER = "********";
2323
export const UNASSIGNED_SECRET_PLACEHOLDER = "UNASSIGNED";
24+
25+
export const BUSYBOX = 'busybox@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f';
26+
27+
// Filenames in docker-prompts volume
28+
export const REGISTRY_YAML = 'registry.yaml'

src/extension/ui/src/FileUtils.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import { v1 } from "@docker/extension-api-client-types"
66
import { ExecResult } from "@docker/extension-api-client-types/dist/v0"
7-
import { Serializable } from "child_process"
7+
import { BUSYBOX } from "./Constants"
88

99
export const tryRunImageSync = async (client: v1.DockerDesktopClient, args: string[], ignoreError = false) => {
1010
const showError = ignoreError ? () => { } : client.desktopUI.toast.error
@@ -27,25 +27,42 @@ export const tryRunImageSync = async (client: v1.DockerDesktopClient, args: stri
2727
}
2828

2929
export const getUser = async (client: v1.DockerDesktopClient) => {
30-
const result = await tryRunImageSync(client, ['--rm', '-e', 'USER', 'alpine:latest', 'sh', '-c', `"echo $USER"`])
30+
const result = await tryRunImageSync(client, ['--rm', '-e', 'USER', BUSYBOX, '/bin/echo', '$USER'])
3131
return result.trim()
3232
}
3333

3434
export const readFileInPromptsVolume = async (client: v1.DockerDesktopClient, path: string) => {
35-
return tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'alpine:latest', 'sh', '-c', `"cat ${path}"`], true)
35+
return tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '-w', '/docker-prompts', BUSYBOX, '/bin/cat', `${path}`], true)
3636
}
3737

38-
export const writeFileToPromptsVolume = async (client: v1.DockerDesktopClient, content: string) => {
39-
// Workaround for inability to use shell operators w/ DD extension API, use write_files image
40-
return tryRunImageSync(client, ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'vonwig/function_write_files:latest', `'${content}'`])
38+
export const writeToPromptsVolume = async (client: v1.DockerDesktopClient, filename: string, content: string) => {
39+
return tryRunImageSync(client, [
40+
"--rm",
41+
"-v",
42+
"docker-prompts:/workdir",
43+
"-w",
44+
"/workdir",
45+
BUSYBOX,
46+
"/bin/sh",
47+
"-c",
48+
`'echo "${encodeBase64(content)}" | base64 -d > ${filename}'`,
49+
]);
4150
}
4251

43-
export const escapeJSONForPlatformShell = (json: Serializable, platform: string) => {
44-
const jsonString = JSON.stringify(json, null, 2)
45-
if (platform === 'win32') {
46-
// Use triple quotes to escape quotes
47-
return `"${jsonString.replace(/"/g, '\\"')}"`
48-
}
49-
return `'${jsonString}'`
52+
export const writeToMount = async (client: v1.DockerDesktopClient, mount: string, filename: string, content: string) => {
53+
return tryRunImageSync(client, [
54+
"--rm",
55+
'--mount',
56+
mount,
57+
BUSYBOX,
58+
"/bin/sh",
59+
"-c",
60+
`'echo "${encodeBase64(content)}" | base64 -d > ${filename}'`,
61+
]);
5062
}
5163

64+
const encodeBase64 = (input: string): string => {
65+
const utf8Bytes = new TextEncoder().encode(input);
66+
const binary = Array.from(utf8Bytes).map(byte => String.fromCharCode(byte)).join('');
67+
return btoa(binary);
68+
};

src/extension/ui/src/Registry.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { v1 } from "@docker/extension-api-client-types";
22
import { parse, stringify } from "yaml";
3-
import { readFileInPromptsVolume, writeFileToPromptsVolume } from "./FileUtils";
3+
import { REGISTRY_YAML } from "./Constants";
4+
import { readFileInPromptsVolume, writeToPromptsVolume } from "./FileUtils";
45
import { mergeDeep } from "./MergeDeep";
56
import { ParsedParameters } from "./types/config";
67

@@ -19,7 +20,7 @@ export const getRegistry = async (client: v1.DockerDesktopClient) => {
1920
const writeRegistryIfNotExists = async () => {
2021
const registry = await readFileInPromptsVolume(client, 'registry.yaml')
2122
if (!registry) {
22-
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'registry.yaml', content: 'registry: {}' }] }))
23+
await writeToPromptsVolume(client, REGISTRY_YAML, 'registry: {}')
2324
}
2425
}
2526
try {
@@ -43,7 +44,7 @@ export const getStoredConfig = async (client: v1.DockerDesktopClient) => {
4344
const writeConfigIfNotExists = async () => {
4445
const config = await readFileInPromptsVolume(client, 'config.yaml')
4546
if (!config) {
46-
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'config.yaml', content: '{}' }] }))
47+
await writeToPromptsVolume(client, 'config.yaml', '{}')
4748
}
4849
}
4950
try {
@@ -79,7 +80,7 @@ export const syncConfigWithRegistry = async (client: v1.DockerDesktopClient, reg
7980
}
8081
const newConfigString = JSON.stringify(config)
8182
if (oldConfigString !== newConfigString) {
82-
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'config.yaml', content: stringify(config) }] }))
83+
await writeToPromptsVolume(client, 'config.yaml', stringify(config))
8384
}
8485
return config
8586
}
@@ -105,7 +106,7 @@ export const syncRegistryWithConfig = async (client: v1.DockerDesktopClient, reg
105106
}
106107
const newRegString = JSON.stringify(registry)
107108
if (oldRegString !== newRegString) {
108-
await writeFileToPromptsVolume(client, JSON.stringify({ files: [{ path: 'registry.yaml', content: stringify({ registry }) }] }))
109+
await writeToPromptsVolume(client, REGISTRY_YAML, stringify({ registry }))
109110
}
110111
return registry
111112
}

src/extension/ui/src/components/LoadingState.tsx

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -137,62 +137,6 @@ const LoadingState: React.FC<LoadingStateProps> = ({ appProps }) => {
137137
variant="determinate"
138138
value={progress}
139139
/>
140-
141-
{imagesLoading && imageStates && (
142-
<Box
143-
sx={{
144-
mt: 2,
145-
p: 2,
146-
width: '100%',
147-
backgroundColor: 'rgba(0, 0, 0, 0.03)',
148-
borderRadius: 1,
149-
}}
150-
>
151-
<Typography variant="subtitle2" fontWeight="medium" gutterBottom>
152-
Docker Images
153-
</Typography>
154-
155-
<Stack spacing={1.5} mt={1}>
156-
{/* Type assertion for imageStates */}
157-
{Object.entries(imageStates as Record<string, ImageState>).map(([imageName, state]) => (
158-
<Box
159-
key={imageName}
160-
sx={{
161-
display: 'flex',
162-
justifyContent: 'space-between',
163-
alignItems: 'center',
164-
p: 1,
165-
borderRadius: 1,
166-
backgroundColor: 'background.paper',
167-
}}
168-
>
169-
<Typography variant="body2" fontWeight="medium">{imageName}</Typography>
170-
<Box
171-
sx={{
172-
display: 'flex',
173-
alignItems: 'center',
174-
gap: 1,
175-
}}
176-
>
177-
{state.status === 'loading' && (
178-
<CircularProgress size={14} thickness={4} />
179-
)}
180-
<Typography
181-
variant="body2"
182-
sx={{
183-
color: getStatusColor(state.status),
184-
fontWeight: 'medium',
185-
textTransform: 'capitalize'
186-
}}
187-
>
188-
{state.status}
189-
</Typography>
190-
</Box>
191-
</Box>
192-
))}
193-
</Stack>
194-
</Box>
195-
)}
196140
</Stack>
197141
</Paper>
198142
</Box>

src/extension/ui/src/mcp-clients/ClaudeDesktop.ts

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { getUser, escapeJSONForPlatformShell } from "../FileUtils";
2+
import { BUSYBOX } from "../Constants";
3+
import { getUser, writeToMount } from "../FileUtils";
34
import { MCPClient, SAMPLE_MCP_CONFIG } from "./MCPTypes";
45

56
class ClaudeDesktopClient implements MCPClient {
@@ -39,7 +40,7 @@ class ClaudeDesktopClient implements MCPClient {
3940
const user = await getUser(client)
4041
path = path.replace('$USER', user)
4142
try {
42-
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/config.json`, 'alpine:latest', 'sh', '-c', `"cat /config.json"`])
43+
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/config.json`, BUSYBOX, '/bin/cat', '/config.json'])
4344
return { content: result.stdout || undefined, path: path };
4445
} catch (e) {
4546
return { content: null, path: path };
@@ -63,30 +64,31 @@ class ClaudeDesktopClient implements MCPClient {
6364
}
6465
const user = await getUser(client)
6566
path = path.replace('$USER', user)
66-
let payload = {
67-
mcpServers: {
68-
MCP_DOCKER: SAMPLE_MCP_CONFIG.mcpServers.MCP_DOCKER
69-
}
70-
}
67+
68+
let payload: Record<string, any> = {}
7169
try {
72-
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, 'alpine:latest', 'sh', '-c', `"cat /claude_desktop_config/claude_desktop_config.json"`])
70+
const result = await client.docker.cli.exec('run', [
71+
'--rm',
72+
'--mount',
73+
`type=bind,source="${path}",target=/claude_desktop_config`,
74+
BUSYBOX,
75+
'/bin/cat',
76+
'/claude_desktop_config/claude_desktop_config.json',
77+
])
7378
if (result.stdout) {
7479
payload = JSON.parse(result.stdout)
75-
payload.mcpServers.MCP_DOCKER = SAMPLE_MCP_CONFIG.mcpServers.MCP_DOCKER
7680
}
7781
} catch (e) {
7882
// No config or malformed config found, overwrite it
7983
}
84+
85+
if (!payload.mcpServers) {
86+
payload.mcpServers = {}
87+
}
88+
payload.mcpServers.MCP_DOCKER = SAMPLE_MCP_CONFIG.mcpServers.MCP_DOCKER
89+
8090
try {
81-
await client.docker.cli.exec('run',
82-
[
83-
'--rm',
84-
'--mount',
85-
`type=bind,source="${path}",target=/claude_desktop_config`,
86-
'--workdir',
87-
'/claude_desktop_config',
88-
'vonwig/function_write_files:latest',
89-
escapeJSONForPlatformShell({ files: [{ path: 'claude_desktop_config.json', content: JSON.stringify(payload, null, 2) }] }, client.host.platform)])
91+
await writeToMount(client, `type=bind,source="${path}",target=/claude_desktop_config`, '/claude_desktop_config/claude_desktop_config.json', JSON.stringify(payload, null, 2));
9092
} catch (e) {
9193
client.desktopUI.toast.error((e as any).stderr)
9294
}
@@ -111,26 +113,10 @@ class ClaudeDesktopClient implements MCPClient {
111113
path = path.replace('$USER', user)
112114
try {
113115
// This method is only called after the config has been validated, so we can safely assume it's a valid config.
114-
const previousConfig = JSON.parse((await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, '--workdir', '/claude_desktop_config', 'alpine:latest', 'sh', '-c', `"cat /claude_desktop_config/claude_desktop_config.json"`])).stdout || '{}')
116+
const previousConfig = JSON.parse((await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, '-w', '/claude_desktop_config', BUSYBOX, '/bin/cat', '/claude_desktop_config/claude_desktop_config.json'])).stdout || '{}')
115117
const newConfig = { ...previousConfig }
116118
delete newConfig.mcpServers.MCP_DOCKER
117-
await client.docker.cli.exec('run', [
118-
'--rm',
119-
'--mount',
120-
`type=bind,source="${path}",target=/claude_desktop_config`,
121-
'--workdir',
122-
'/claude_desktop_config',
123-
'vonwig/function_write_files:latest',
124-
escapeJSONForPlatformShell(
125-
{
126-
files:
127-
[{
128-
path: 'claude_desktop_config.json',
129-
content: JSON.stringify(newConfig, null, 2)
130-
}]
131-
},
132-
client.host.platform)
133-
])
119+
await writeToMount(client, `type=bind,source="${path}",target=/claude_desktop_config`, '/claude_desktop_config/claude_desktop_config.json', JSON.stringify(newConfig, null, 2));
134120
} catch (e) {
135121
client.desktopUI.toast.error((e as any).stderr)
136122
}

src/extension/ui/src/mcp-clients/ContinueDotDev.ts

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { escapeJSONForPlatformShell, getUser } from "../FileUtils";
3-
import { MCPClient, SAMPLE_MCP_CONFIG } from "./MCPTypes";
4-
import { DOCKER_MCP_COMMAND } from "../Constants";
5-
import { mergeDeep } from "../MergeDeep";
62
import { parse, stringify } from "yaml";
3+
import { BUSYBOX } from "../Constants";
4+
import { getUser, writeToMount } from "../FileUtils";
5+
import { mergeDeep } from "../MergeDeep";
6+
import { MCPClient, SAMPLE_MCP_CONFIG } from "./MCPTypes";
7+
78
class ContinueDotDev implements MCPClient {
89
name = "Continue.dev";
910
url = "https://continue.dev/";
@@ -26,13 +27,13 @@ class ContinueDotDev implements MCPClient {
2627
await getUser(client)
2728
);
2829
try {
29-
const result = await client.docker.cli.exec("run", [
30-
"--rm",
31-
"--mount",
30+
const result = await client.docker.cli.exec('run', [
31+
'--rm',
32+
'--mount',
3233
`type=bind,source=${configPath},target=/continue/config.yaml`,
33-
"alpine:latest",
34-
"cat",
35-
"/continue/config.yaml",
34+
BUSYBOX,
35+
'/bin/cat',
36+
'/continue/config.yaml',
3637
]);
3738
return {
3839
content: result.stdout,
@@ -61,18 +62,7 @@ class ContinueDotDev implements MCPClient {
6162
}
6263
const payload = mergeDeep(continueConfig, SAMPLE_MCP_CONFIG);
6364
try {
64-
await client.docker.cli.exec("run", [
65-
"--rm",
66-
"--mount",
67-
`type=bind,source="${config.path}",target=/continue/config.yaml`,
68-
"--workdir",
69-
"/continue",
70-
"vonwig/function_write_files:latest",
71-
escapeJSONForPlatformShell(
72-
{ files: [{ path: "config.yaml", content: stringify(payload) }] },
73-
client.host.platform
74-
),
75-
]);
65+
await writeToMount(client, `type=bind,source=${config.path},target=/continue/config.yaml`, '/continue/config.yaml', stringify(payload));
7666
} catch (e) {
7767
if ((e as any).stderr) {
7868
client.desktopUI.toast.error((e as any).stderr);
@@ -99,7 +89,7 @@ class ContinueDotDev implements MCPClient {
9989
} catch (e) {
10090
client.desktopUI.toast.error(
10191
"Failed to disconnect. Invalid Continue.dev config found at " +
102-
config.path
92+
config.path
10393
);
10494
return;
10595
}
@@ -112,18 +102,7 @@ class ContinueDotDev implements MCPClient {
112102
),
113103
};
114104
try {
115-
await client.docker.cli.exec("run", [
116-
"--rm",
117-
"--mount",
118-
`type=bind,source="${config.path}",target=/continue/config.yaml`,
119-
"--workdir",
120-
"/continue",
121-
"vonwig/function_write_files:latest",
122-
escapeJSONForPlatformShell(
123-
{ files: [{ path: "config.yaml", content: stringify(payload) }] },
124-
client.host.platform
125-
),
126-
]);
105+
await writeToMount(client, `type=bind,source=${config.path},target=/continue/config.yaml`, '/continue/config.yaml', stringify(payload));
127106
} catch (e) {
128107
if ((e as any).stderr) {
129108
client.desktopUI.toast.error((e as any).stderr);

0 commit comments

Comments
 (0)