Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 20 additions & 10 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,10 @@ if [ -n "$HTTP_PROXY" ]; then

# Maven proxy config (~/.m2/settings.xml)
# Only create if the file does not already exist, to avoid clobbering user-provided settings
mkdir -p "${JVM_HOME_PREFIX}/.m2"
if [ ! -f "${JVM_HOME_PREFIX}/.m2/settings.xml" ]; then
cat > "${JVM_HOME_PREFIX}/.m2/settings.xml" << MAVEN_EOF
if ! mkdir -p "${JVM_HOME_PREFIX}/.m2" 2>/dev/null || [ ! -w "${JVM_HOME_PREFIX}/.m2" ]; then
echo "[entrypoint] ⚠ Cannot write ${JVM_HOME_PREFIX}/.m2 (read-only home); skipping Maven proxy config"
elif [ ! -f "${JVM_HOME_PREFIX}/.m2/settings.xml" ]; then
if cat > "${JVM_HOME_PREFIX}/.m2/settings.xml" << MAVEN_EOF
<settings>
<proxies>
<proxy>
Expand All @@ -355,25 +356,34 @@ if [ -n "$HTTP_PROXY" ]; then
</proxies>
</settings>
MAVEN_EOF
chown awfuser:awfuser "${JVM_HOME_PREFIX}/.m2/settings.xml" 2>/dev/null || true
echo "[entrypoint] ✓ Created Maven proxy config (${JVM_HOME_PREFIX}/.m2/settings.xml)"
then
chown awfuser:awfuser "${JVM_HOME_PREFIX}/.m2/settings.xml" 2>/dev/null || true
echo "[entrypoint] ✓ Created Maven proxy config (${JVM_HOME_PREFIX}/.m2/settings.xml)"
else
echo "[entrypoint] ⚠ Failed to write ${JVM_HOME_PREFIX}/.m2/settings.xml; skipping Maven proxy config"
fi
else
echo "[entrypoint] ✓ Maven settings.xml already exists, skipping proxy config creation"
fi

# Gradle proxy config (~/.gradle/gradle.properties)
# Only create if the file does not already exist, to avoid clobbering user-provided settings
# (e.g., org.gradle.jvmargs, build cache settings, private repo credentials)
mkdir -p "${JVM_HOME_PREFIX}/.gradle"
if [ ! -f "${JVM_HOME_PREFIX}/.gradle/gradle.properties" ]; then
cat > "${JVM_HOME_PREFIX}/.gradle/gradle.properties" << GRADLE_EOF
if ! mkdir -p "${JVM_HOME_PREFIX}/.gradle" 2>/dev/null || [ ! -w "${JVM_HOME_PREFIX}/.gradle" ]; then
echo "[entrypoint] ⚠ Cannot write ${JVM_HOME_PREFIX}/.gradle (read-only home); skipping Gradle proxy config"
elif [ ! -f "${JVM_HOME_PREFIX}/.gradle/gradle.properties" ]; then
if cat > "${JVM_HOME_PREFIX}/.gradle/gradle.properties" << GRADLE_EOF
systemProp.http.proxyHost=${PROXY_HOST}
systemProp.http.proxyPort=${PROXY_PORT}
systemProp.https.proxyHost=${PROXY_HOST}
systemProp.https.proxyPort=${PROXY_PORT}
GRADLE_EOF
chown awfuser:awfuser "${JVM_HOME_PREFIX}/.gradle/gradle.properties" 2>/dev/null || true
echo "[entrypoint] ✓ Created Gradle proxy config (${JVM_HOME_PREFIX}/.gradle/gradle.properties)"
then
chown awfuser:awfuser "${JVM_HOME_PREFIX}/.gradle/gradle.properties" 2>/dev/null || true
echo "[entrypoint] ✓ Created Gradle proxy config (${JVM_HOME_PREFIX}/.gradle/gradle.properties)"
else
echo "[entrypoint] ⚠ Failed to write ${JVM_HOME_PREFIX}/.gradle/gradle.properties; skipping Gradle proxy config"
fi
else
echo "[entrypoint] ✓ Gradle gradle.properties already exists, skipping proxy config creation"
fi
Expand Down
25 changes: 25 additions & 0 deletions docs/arc-dind.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ Language SDKs (Go, Node, Java, .NET) are NOT baked into the sysroot image. They
- run: echo "RUNNER_TOOL_CACHE=/tmp/gh-aw/tool-cache" >> "$GITHUB_ENV"
```

## Writable home under sysroot staging

Sysroot staging drops agent bind mounts whose sources the DinD daemon cannot
resolve, including AWF's own `${workDir}-chroot-home` volume for `/host$HOME`.
An explicitly supplied mount is exempt from that filter: if the caller passes
`--mount <daemon-visible-home>:$HOME:rw` (the gh-aw compiler does this for
`${RUNNER_TEMP}/gh-aw/home`), the resulting `/host$HOME` mount is kept, because
the caller vouches for the source being visible to the daemon. The exemption
matches on both source and target, so AWF's own mounts to the same target stay
subject to the filter.

A writable `/host$HOME` matters for two reasons:

- the `/dev/null` credential-hiding overlays are mounted under `/host$HOME`, and
runc cannot create those mountpoints under a read-only parent;
- `entrypoint.sh` pre-seeds JVM build tool proxy config (`~/.m2`, `~/.gradle`)
under the chroot home.

If no writable `/host$HOME` survives the filter, AWF logs a warning and skips
the `/host$HOME` credential overlays instead of failing container creation — the
overlays at the un-prefixed `$HOME` path (on the agent's own rootfs) are still
applied, but credential files under the chroot home are not masked for that run.
The entrypoint likewise warns and skips JVM proxy pre-seeding rather than
aborting.

## What AWF handles automatically

- Split-filesystem probing for `--docker-host-path-prefix`
Expand Down
29 changes: 26 additions & 3 deletions src/compose-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ describe('generateDockerCompose', () => {

generateDockerCompose(config, mockNetworkConfig);

expect(warnSpy).not.toHaveBeenCalled();
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('under /opt'));
warnSpy.mockRestore();
});

Expand Down Expand Up @@ -650,7 +650,9 @@ describe('generateDockerCompose', () => {
expect(homeTargets).toContain(`/host${workspaceDir}`);
expect(homeTargets.some(target => target.startsWith(`/host${effectiveHomeForFilter}/.`))).toBe(false);

// Home root mounts (including trailing slash source) should be dropped.
// An explicitly supplied home-root mount (including trailing slash source)
// survives the filter: the caller vouches for its daemon visibility, and a
// writable /host$HOME is required by the credential overlays and entrypoint.
const effectiveHome = getRealUserHome();
const configWithHomeRootMount = {
...config,
Expand All @@ -661,7 +663,28 @@ describe('generateDockerCompose', () => {
const target = v.split(':')[1];
return target === `/host${effectiveHome}` || target === `/host${effectiveHome}/`;
});
expect(homeRootMounts).toHaveLength(0);
expect(homeRootMounts).toEqual([`${effectiveHome}/:/host${effectiveHome}:rw`]);

// The chroot-home volume sourced from workDir is still dropped.
expect(
(resultWithHomeRootMount.services.agent.volumes as string[]).some(v =>
v.startsWith('/tmp/awf-12345-chroot-home'),
),
).toBe(false);

// Credential overlays under /host$HOME are kept when a writable home survives.
expect(
(resultWithHomeRootMount.services.agent.volumes as string[]).some(
v => v.startsWith('/dev/null:') && v.split(':')[1].startsWith(`/host${effectiveHome}/`),
),
).toBe(true);

// Without such a mount, those overlays are skipped (no writable parent exists).
expect(
volumes.some(
v => v.startsWith('/dev/null:') && v.split(':')[1].startsWith(`/host${effectiveHome}/`),
),
).toBe(false);

// Should still have /tmp:/tmp, /sys, /dev, sysroot volume
expect(volumes).toContain('/tmp:/tmp:rw');
Expand Down
5 changes: 5 additions & 0 deletions src/services/agent-volumes/workspace-mounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,9 @@ describe('buildCustomVolumeMounts', () => {
const result = buildCustomVolumeMounts(['/a:/b', '/c:/d:ro', 'named']);
expect(result).toEqual(['/a:/host/b', '/c:/host/d:ro', 'named']);
});

it('does not double-prefix targets that already start with /host', () => {
const result = buildCustomVolumeMounts(['/data:/host/data:ro', '/root:/host']);
expect(result).toEqual(['/data:/host/data:ro', '/root:/host']);
});
});
19 changes: 16 additions & 3 deletions src/services/agent-volumes/workspace-mounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,19 @@ function isExecutableFile(candidate: string): boolean {
export function buildCustomVolumeMounts(
volumeMounts?: string[],
dockerHostPathPrefix?: string,
options: { quiet?: boolean } = {},
): string[] {
if (!volumeMounts || volumeMounts.length === 0) {
return [];
}

logger.debug(`Adding ${volumeMounts.length} custom volume mount(s)`);
// `quiet` is used by callers that only re-derive the transformed specs for
// comparison (e.g. the sysroot volume filter) and must not log them twice.
const debug = (message: string) => {
if (!options.quiet) logger.debug(message);
};

debug(`Adding ${volumeMounts.length} custom volume mount(s)`);

// Custom mount sources always use the runner's filesystem view. Translate
// them even when a source already starts with the daemon-side prefix; this
Expand All @@ -114,11 +121,17 @@ export function buildCustomVolumeMounts(
const hostPath = parts[0];
const containerPath = parts[1];
const mode = parts[2] || '';
const chrootContainerPath = `/host${containerPath}`;
// Targets that already carry the chroot prefix (some callers emit both an
// un-prefixed and a `/host`-prefixed mount) must not be prefixed again,
// otherwise they land at `/host/host/…` and mount nothing meaningful.
const chrootContainerPath =
containerPath === '/host' || containerPath.startsWith('/host/')
? containerPath
: `/host${containerPath}`;
const transformedMount = mode
? `${hostPath}:${chrootContainerPath}:${mode}`
: `${hostPath}:${chrootContainerPath}`;
logger.debug(`Adding custom volume mount: ${volumeMounts[index]} -> ${transformedMount} (chroot-adjusted)`);
debug(`Adding custom volume mount: ${volumeMounts[index]} -> ${transformedMount} (chroot-adjusted)`);
return transformedMount;
}

Expand Down
80 changes: 80 additions & 0 deletions src/services/optional-services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('optional-services helpers', () => {
const config: WrapperConfig = {
...baseConfig,
workDir: '/tmp/awf-work',
volumeMounts: ['/home/runner:/home/runner:rw'],
};

const filtered = testHelpers.filterAgentVolumesForSysroot(
Expand All @@ -81,11 +82,90 @@ describe('optional-services helpers', () => {
);

expect(filtered).toEqual([
'/home/runner:/host/home/runner:rw',
'/home/runner/_work/_temp/gh-aw:/host/home/runner/_work/_temp/gh-aw:rw',
'/tmp:/tmp:rw',
'/dev/null:/host/home/runner/.npmrc:ro',
'bad-volume-entry',
]);
});

it('drops the chroot-home volume but keeps an explicitly mounted writable home', () => {
const config: WrapperConfig = {
...baseConfig,
workDir: '/tmp/awf-work',
volumeMounts: ['/home/runner/_work/_temp/gh-aw/home:/home/runner/_work/_temp/gh-aw/home:rw'],
};
const home = '/home/runner/_work/_temp/gh-aw/home';

const filtered = testHelpers.filterAgentVolumesForSysroot(
[
`/tmp/awf-work-chroot-home:/host${home}:rw`,
`${home}:/host${home}:rw`,
`/dev/null:/host${home}/.npmrc:ro`,
`/dev/null:${home}/.npmrc:ro`,
],
config,
home,
);

expect(filtered).toEqual([
`${home}:/host${home}:rw`,
`/dev/null:/host${home}/.npmrc:ro`,
`/dev/null:${home}/.npmrc:ro`,
]);
});

it('does not exempt AWF home mounts that merely share a target with an explicit mount', () => {
const config: WrapperConfig = {
...baseConfig,
workDir: '/tmp/awf-work',
volumeMounts: [
'/daemon/cache:/home/runner/.cache:rw',
'/home/runner/_work/_temp/gh-aw/home:/home/runner:rw',
],
};

const filtered = testHelpers.filterAgentVolumesForSysroot(
[
'/home/runner/.cache:/host/home/runner/.cache:rw',
'/daemon/cache:/host/home/runner/.cache:rw',
'/home/runner/_work/_temp/gh-aw/home:/host/home/runner:rw',
],
config,
'/home/runner',
);

// AWF's own $HOME/.cache bind (runner-side source) is still dropped even
// though an explicit mount targets the same path.
expect(filtered).toEqual([
'/daemon/cache:/host/home/runner/.cache:rw',
'/home/runner/_work/_temp/gh-aw/home:/host/home/runner:rw',
]);
});

it('skips /host$HOME credential overlays when no writable /host$HOME survives', () => {
const config: WrapperConfig = {
...baseConfig,
workDir: '/tmp/awf-work',
};
const home = '/home/runner/_work/_temp/gh-aw/home';

const filtered = testHelpers.filterAgentVolumesForSysroot(
[
`/tmp/awf-work-chroot-home:/host${home}:rw`,
`/dev/null:/host${home}/.npmrc:ro`,
`/dev/null:${home}/.npmrc:ro`,
'/tmp:/tmp:rw',
],
config,
home,
);

expect(filtered).toEqual([
`/dev/null:${home}/.npmrc:ro`,
'/tmp:/tmp:rw',
]);
});
});
});
Loading
Loading