Skip to content

Commit 7071dfe

Browse files
committed
test: support running in GHA
1 parent ce6cd2d commit 7071dfe

5 files changed

Lines changed: 111 additions & 8 deletions

File tree

resources/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
1010
FROM ${BUNDLING_IMAGE}
1111

1212
COPY --from=uv /uv /uvx /bin/
13-
COPY *.sh /root
14-
RUN chmod +x /root/*.sh
15-
WORKDIR /root
16-
ENTRYPOINT [ "/root/entrypoint.sh" ]
13+
COPY *.sh /opt/uv-python-lambda/
14+
RUN chmod +x /opt/uv-python-lambda/*.sh
15+
WORKDIR /opt/uv-python-lambda
16+
ENTRYPOINT [ "/opt/uv-python-lambda/entrypoint.sh" ]

resources/entrypoint.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ export LOCK_FILE=/uvbuild/uv-python-lambda.lock
1212
export UV_LINK_MODE=hardlink
1313
export UV_NO_INSTALLER_METADATA=1
1414
export UV_PYTHON_LAMBDA_NOFILE_LIMIT="${UV_PYTHON_LAMBDA_NOFILE_LIMIT:-1048576}"
15+
export HOME=/tmp/uv-python-lambda-home
1516

1617
ulimit -n "$UV_PYTHON_LAMBDA_NOFILE_LIMIT"
1718

1819
rm -f "$LOCK_FILE"
1920

2021
mkdir -p /uvbuild/uvcache
21-
mkdir -p /root/.cache
22-
ln -sf /uvbuild/uvcache /root/.cache/uv
22+
mkdir -p "$HOME/.cache"
23+
ln -sf /uvbuild/uvcache "$HOME/.cache/uv"
2324

2425
touch "$LOCK_FILE"
2526

resources/export.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ NAME=${0##*/}
1515
#
1616
# - Warm-cache package installs are the main fast path. They benefit from
1717
# cache reuse across functions and synth runs in the same process.
18-
# - Hardlink mode is preferred because /root/.cache/uv and /uvbuild live on the
18+
# - Hardlink mode is preferred because $HOME/.cache/uv and /uvbuild live on the
1919
# same mounted filesystem. Hardlinks avoid the extra copy work and file-handle
2020
# churn that `copy` mode caused on large dependency trees.
2121
# - Workspace package handling copies only the local package directories that
@@ -26,6 +26,7 @@ export LOCK_FILE=/uvbuild/uv-python-lambda.lock
2626
export UV_LINK_MODE=hardlink
2727
export UV_NO_INSTALLER_METADATA=1
2828
export UV_PYTHON_LAMBDA_NOFILE_LIMIT="${UV_PYTHON_LAMBDA_NOFILE_LIMIT:-1048576}"
29+
export HOME=/tmp/uv-python-lambda-home
2930

3031
# Raise the per-process FD limit inside the container. The builder
3132
# container itself is already started with a large nofile ulimit, this mirrors

src/bundling.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const DEFAULT_ASSET_EXCLUDES = [
3434

3535
export const DEFAULT_UV_VERSION = '0.5.27';
3636

37+
const BUILDER_TOOL_DIR = '/opt/uv-python-lambda';
3738
const BUILDER_READY_LOG = 'Builder container is ready and waiting';
3839
const BUILDER_NOFILE_LIMIT = '1048576:1048576';
3940

@@ -173,6 +174,7 @@ export class Bundling {
173174
const buildImage = this.createDockerImage();
174175
const hostUvBuildDir = path.join(cdkOutDir, this.containerBuilderKey);
175176
const hostRootDir = path.resolve(this.props.rootDir);
177+
const builderUser = getDockerUserArg();
176178

177179
mkdirSync(hostUvBuildDir, { recursive: true });
178180

@@ -187,6 +189,10 @@ export class Bundling {
187189
this.containerBuilderName,
188190
];
189191

192+
if (builderUser) {
193+
dockerArgs.push('--user', builderUser);
194+
}
195+
190196
for (const [name, value] of this.getBuilderEnvironmentEntries()) {
191197
dockerArgs.push('--env', `${name}=${value}`);
192198
}
@@ -215,14 +221,19 @@ export class Bundling {
215221

216222
const containerOutputDir = this.getContainerFunctionOutputDir();
217223
const command = ['docker', 'exec'];
224+
const builderUser = getDockerUserArg();
225+
226+
if (builderUser) {
227+
command.push('--user', builderUser);
228+
}
218229

219230
for (const [name, value] of this.getBuilderEnvironmentEntries()) {
220231
command.push('-e', `${name}=${value}`);
221232
}
222233

223234
command.push(
224235
this.containerBuilderName,
225-
'/root/export.sh',
236+
`${BUILDER_TOOL_DIR}/export.sh`,
226237
'--output',
227238
containerOutputDir,
228239
);
@@ -334,3 +345,14 @@ function getBuilderEnvironment(
334345
...environment,
335346
};
336347
}
348+
349+
function getDockerUserArg() {
350+
if (
351+
typeof process.getuid !== 'function' ||
352+
typeof process.getgid !== 'function'
353+
) {
354+
return undefined;
355+
}
356+
357+
return `${process.getuid()}:${process.getgid()}`;
358+
}

test/bundling.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,44 @@ import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda';
33
import { Bundling, DEFAULT_UV_VERSION } from '../src/bundling';
44
import type { ICommandHooks } from '../src/types';
55

6+
type BundlingModule = typeof import('../src/bundling');
7+
68
function decodeCommands(value: string) {
79
return JSON.parse(Buffer.from(value, 'base64').toString('utf8')) as string[];
810
}
911

12+
function getExpectedDockerUserArg() {
13+
if (
14+
typeof process.getuid !== 'function' ||
15+
typeof process.getgid !== 'function'
16+
) {
17+
throw new Error('process.getuid() and process.getgid() are required');
18+
}
19+
20+
return `${process.getuid()}:${process.getgid()}`;
21+
}
22+
23+
function loadBundlingModule(ensureBuilderContainerMock: jest.Mock) {
24+
let loadedModule: BundlingModule | undefined;
25+
26+
jest.isolateModules(() => {
27+
jest.doMock('../src/build-container', () => ({
28+
BUILDER_LABEL: 'com.fourtheorem.uv-python-lambda.builder',
29+
ensureBuilderContainer: ensureBuilderContainerMock,
30+
}));
31+
loadedModule = require('../src/bundling') as BundlingModule;
32+
});
33+
34+
if (!loadedModule) {
35+
throw new Error('Failed to load bundling module');
36+
}
37+
38+
return loadedModule;
39+
}
40+
1041
describe('Bundling', () => {
1142
afterEach(() => {
43+
jest.resetModules();
1244
jest.restoreAllMocks();
1345
});
1446

@@ -104,6 +136,23 @@ describe('Bundling', () => {
104136
expect(command).toContain('UV_PYTHON_LAMBDA_NOFILE_LIMIT=1048576');
105137
});
106138

139+
test('runs export commands as the host user when uid and gid are available', () => {
140+
const bundling = new Bundling({
141+
rootDir: '/tmp/project-user',
142+
runtime: Runtime.PYTHON_3_12,
143+
architecture: Architecture.X86_64,
144+
workspacePackage: 'app',
145+
});
146+
147+
const command = Reflect.get(bundling, 'createBundlingCommand').call(
148+
bundling,
149+
) as string[];
150+
151+
expect(command).toContain('--user');
152+
expect(command).toContain(getExpectedDockerUserArg());
153+
expect(command).toContain('/opt/uv-python-lambda/export.sh');
154+
});
155+
107156
test('builds the builder image with the default uv version', () => {
108157
const fromBuildSpy = jest
109158
.spyOn(DockerImage, 'fromBuild')
@@ -188,4 +237,34 @@ describe('Bundling', () => {
188237
Reflect.get(overriddenBundling, 'containerBuilderKey'),
189238
);
190239
});
240+
241+
test('starts the builder container as the host user when uid and gid are available', () => {
242+
const ensureBuilderContainerMock = jest.fn();
243+
const bundlingModule = loadBundlingModule(ensureBuilderContainerMock);
244+
const fromBuildSpy = jest
245+
.spyOn(DockerImage, 'fromBuild')
246+
.mockReturnValue({ image: 'mock-image' } as DockerImage);
247+
248+
const bundling = new bundlingModule.Bundling({
249+
rootDir: '/tmp/project-run-user',
250+
runtime: Runtime.PYTHON_3_12,
251+
architecture: Architecture.X86_64,
252+
});
253+
254+
Reflect.get(bundling, 'ensureBuilderReady').call(
255+
bundling,
256+
'/tmp/cdk-run-user',
257+
);
258+
259+
expect(fromBuildSpy).toHaveBeenCalled();
260+
expect(ensureBuilderContainerMock).toHaveBeenCalledWith(
261+
expect.objectContaining({
262+
args: expect.arrayContaining([
263+
'--user',
264+
getExpectedDockerUserArg(),
265+
'mock-image',
266+
]),
267+
}),
268+
);
269+
});
191270
});

0 commit comments

Comments
 (0)