-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathcontainer-dev-server.ts
More file actions
190 lines (167 loc) · 6.23 KB
/
container-dev-server.ts
File metadata and controls
190 lines (167 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { CONTAINER_INTERNAL_PORT, DOCKERFILE_NAME } from '../../../lib';
import { getUvBuildArgs } from '../../../lib/packaging/build-args';
import { detectContainerRuntime, getStartHint } from '../../external-requirements/detect';
import { DevServer, type LogLevel, type SpawnConfig } from './dev-server';
import { convertEntrypointToModule } from './utils';
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
/** Dev server for Container agents. Builds and runs a Docker container with volume mount for hot-reload. */
export class ContainerDevServer extends DevServer {
private runtimeBinary = '';
/** Docker image names must be lowercase. */
private get imageName(): string {
return `agentcore-dev-${this.config.agentName}`.toLowerCase();
}
/** Container name for lifecycle management. */
private get containerName(): string {
return this.imageName;
}
/** Override kill to stop the container properly, cleaning up the port proxy. */
override kill(): void {
if (this.runtimeBinary) {
spawnSync(this.runtimeBinary, ['stop', this.containerName], { stdio: 'ignore' });
}
super.kill();
}
protected async prepare(): Promise<boolean> {
const { onLog } = this.options.callbacks;
// 1. Detect container runtime
const { runtime, notReadyRuntimes } = await detectContainerRuntime();
if (!runtime) {
if (notReadyRuntimes.length > 0) {
onLog(
'error',
`Found ${notReadyRuntimes.join(', ')} but not ready. Start a runtime:\n${getStartHint(notReadyRuntimes)}`
);
} else {
onLog('error', 'No container runtime found. Install Docker, Podman, or Finch.');
}
return false;
}
this.runtimeBinary = runtime.binary;
// 2. Verify Dockerfile exists
const dockerfilePath = join(this.config.directory, DOCKERFILE_NAME);
if (!existsSync(dockerfilePath)) {
onLog('error', `Dockerfile not found at ${dockerfilePath}. Container agents require a Dockerfile.`);
return false;
}
// 3. Remove any stale container from a previous run (prevents "proxy already running" errors)
spawnSync(this.runtimeBinary, ['rm', '-f', this.containerName], { stdio: 'ignore' });
// 4. Build the base container image
const baseImageName = `${this.imageName}-base`;
onLog('system', `Building container image: ${this.imageName}...`);
const buildResult = spawnSync(
this.runtimeBinary,
['build', '-t', baseImageName, '-f', dockerfilePath, ...getUvBuildArgs(), this.config.directory],
{ stdio: 'pipe' }
);
// Log build output for debugging
this.logBuildOutput(buildResult.stdout, buildResult.stderr, onLog);
if (buildResult.status !== 0) {
onLog('error', `Container build failed (exit code ${buildResult.status})`);
return false;
}
// 5. Build dev layer on top with uvicorn and project deps installed to system Python.
// At runtime, `-v source:/app` hides any .venv created during the base build,
// so we need all packages in system site-packages where the volume mount can't hide them.
// Prefers uv when available (template images ship it), falls back to pip for BYO images.
onLog('system', 'Preparing dev environment...');
const devDockerfile = [
`FROM ${baseImageName}`,
'USER root',
'RUN (uv pip install --system -q uvicorn && uv pip install --system /app)' +
' || (pip install -q uvicorn && pip install -q /app)',
].join('\n');
const devBuild = spawnSync(
this.runtimeBinary,
['build', '-t', this.imageName, '-f', '-', ...getUvBuildArgs(), this.config.directory],
{
input: devDockerfile,
stdio: ['pipe', 'pipe', 'pipe'],
}
);
this.logBuildOutput(devBuild.stdout, devBuild.stderr, onLog);
if (devBuild.status !== 0) {
onLog('error', `Dev layer build failed (exit code ${devBuild.status})`);
return false;
}
onLog('system', 'Container image built successfully.');
return true;
}
/** Log build stdout/stderr through the onLog callback at 'system' level so they persist to log files. */
private logBuildOutput(
stdout: Buffer | null,
stderr: Buffer | null,
onLog: (level: LogLevel, message: string) => void
): void {
for (const line of (stdout?.toString() ?? '').split('\n')) {
if (line.trim()) onLog('system', line);
}
for (const line of (stderr?.toString() ?? '').split('\n')) {
if (line.trim()) onLog('system', line);
}
}
protected getSpawnConfig(): SpawnConfig {
const { directory, module: entrypoint } = this.config;
const { port, envVars = {} } = this.options;
const uvicornModule = convertEntrypointToModule(entrypoint);
// Forward AWS credentials from host environment into the container
const awsEnvKeys = [
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_SESSION_TOKEN',
'AWS_REGION',
'AWS_DEFAULT_REGION',
'AWS_PROFILE',
];
const awsEnvVars: Record<string, string> = {};
for (const key of awsEnvKeys) {
if (process.env[key]) {
awsEnvVars[key] = process.env[key]!;
}
}
// Environment variables: AWS creds + user env + container-specific overrides
const envArgs = Object.entries({
...awsEnvVars,
...envVars,
LOCAL_DEV: '1',
PORT: String(CONTAINER_INTERNAL_PORT),
}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
// Mount ~/.aws for credential file / SSO / profile support
const awsDir = join(homedir(), '.aws');
const awsMountArgs = existsSync(awsDir) ? ['-v', `${awsDir}:/root/.aws:ro`] : [];
return {
cmd: this.runtimeBinary,
args: [
'run',
'--rm',
'--name',
this.containerName,
'--entrypoint',
'python',
'--user',
'root',
'-v',
`${directory}:/app`,
...awsMountArgs,
'-p',
`${port}:${CONTAINER_INTERNAL_PORT}`,
...envArgs,
this.imageName,
'-m',
'uvicorn',
uvicornModule,
'--reload',
'--reload-dir',
'/app',
'--host',
'0.0.0.0',
'--port',
String(CONTAINER_INTERNAL_PORT),
],
env: { ...process.env },
};
}
}