Skip to content

Commit e67d310

Browse files
committed
fix(docker): harden clone and shared-network fallback
1 parent a774630 commit e67d310

File tree

5 files changed

+136
-18
lines changed

5 files changed

+136
-18
lines changed

packages/lib/src/core/templates-entrypoint/tasks.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,15 @@ const renderCloneBodyRef = (config: TemplateConfig): string =>
139139
fi
140140
else
141141
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$REPO_REF' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
142-
DEFAULT_REF="$(git ls-remote --symref "$AUTH_REPO_URL" HEAD 2>/dev/null | awk '/^ref:/ {print $2}' | head -n 1 || true)"
143-
DEFAULT_BRANCH="$(printf "%s" "$DEFAULT_REF" | sed 's#^refs/heads/##')"
144-
if [[ -n "$DEFAULT_BRANCH" ]]; then
145-
echo "[clone] branch '$REPO_REF' missing; retrying with '$DEFAULT_BRANCH'"
146-
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$DEFAULT_BRANCH' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
147-
echo "[clone] git clone failed for $REPO_URL"
148-
CLONE_OK=0
149-
elif [[ "$REPO_REF" == issue-* ]]; then
150-
if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
151-
echo "[clone] failed to create local branch '$REPO_REF'"
152-
CLONE_OK=0
153-
fi
154-
fi
155-
else
142+
echo "[clone] branch '$REPO_REF' missing; retrying without --branch"
143+
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS '$AUTH_REPO_URL' '$TARGET_DIR'"; then
156144
echo "[clone] git clone failed for $REPO_URL"
157145
CLONE_OK=0
146+
elif [[ "$REPO_REF" == issue-* ]]; then
147+
if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
148+
echo "[clone] failed to create local branch '$REPO_REF'"
149+
CLONE_OK=0
150+
fi
158151
fi
159152
fi
160153
fi

packages/lib/src/shell/docker.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,31 @@ export const runDockerNetworkCreateBridge = (
381381
(exitCode) => new DockerCommandError({ exitCode })
382382
)
383383

384+
// CHANGE: create a Docker bridge network with an explicit subnet
385+
// WHY: allow callers to bypass default address-pool allocation when it is exhausted
386+
// QUOTE(ТЗ): "научилось создавать сети правильно"
387+
// REF: user-request-2026-02-20-network-fallback
388+
// SOURCE: n/a
389+
// FORMAT THEOREM: ∀(n,s): create(n,s)=0 -> exists(n) ∧ subnet(n)=s
390+
// PURITY: SHELL
391+
// EFFECT: Effect<void, DockerCommandError | PlatformError, CommandExecutor>
392+
// INVARIANT: network driver is always `bridge`
393+
// COMPLEXITY: O(command)
394+
export const runDockerNetworkCreateBridgeWithSubnet = (
395+
cwd: string,
396+
networkName: string,
397+
subnet: string
398+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
399+
runCommandWithExitCodes(
400+
{
401+
cwd,
402+
command: "docker",
403+
args: ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]
404+
},
405+
[Number(ExitCode(0))],
406+
(exitCode) => new DockerCommandError({ exitCode })
407+
)
408+
384409
// CHANGE: inspect how many containers are attached to a network
385410
// WHY: network GC must remove only detached networks
386411
// QUOTE(ТЗ): "Только так что бы текущие проекты не ложились"

packages/lib/src/usecases/docker-network-gc.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import {
1111
runDockerNetworkContainerCount,
1212
runDockerNetworkCreateBridge,
13+
runDockerNetworkCreateBridgeWithSubnet,
1314
runDockerNetworkExists,
1415
runDockerNetworkRemove
1516
} from "../shell/docker.js"
@@ -20,6 +21,54 @@ const protectedNetworkNames = new Set(["bridge", "host", "none"])
2021
const isProtectedNetwork = (networkName: string, sharedNetworkName: string): boolean =>
2122
protectedNetworkNames.has(networkName) || networkName === sharedNetworkName
2223

24+
const sharedNetworkFallbackSubnets: ReadonlyArray<string> = [
25+
"10.250.0.0/24",
26+
"10.251.0.0/24",
27+
"10.252.0.0/24",
28+
"10.253.0.0/24",
29+
"172.31.250.0/24",
30+
"172.31.251.0/24",
31+
"172.31.252.0/24",
32+
"172.31.253.0/24",
33+
"192.168.250.0/24",
34+
"192.168.251.0/24"
35+
]
36+
37+
const createSharedNetworkWithSubnetFallback = (
38+
cwd: string,
39+
networkName: string
40+
): Effect.Effect<boolean, PlatformError, CommandExecutor> =>
41+
Effect.gen(function*(_) {
42+
for (const subnet of sharedNetworkFallbackSubnets) {
43+
const created = yield* _(
44+
runDockerNetworkCreateBridgeWithSubnet(cwd, networkName, subnet).pipe(
45+
Effect.as(true),
46+
Effect.catchTag("DockerCommandError", (error) =>
47+
Effect.logWarning(
48+
`Shared network create fallback failed (${networkName}, subnet ${subnet}, exit ${error.exitCode}); trying next subnet.`
49+
).pipe(Effect.as(false))
50+
)
51+
)
52+
)
53+
if (created) {
54+
yield* _(Effect.log(`Created shared Docker network ${networkName} with subnet ${subnet}.`))
55+
return true
56+
}
57+
}
58+
return false
59+
})
60+
61+
const ensureSharedNetworkExists = (
62+
cwd: string,
63+
networkName: string
64+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor> =>
65+
runDockerNetworkCreateBridge(cwd, networkName).pipe(
66+
Effect.catchTag("DockerCommandError", (error) =>
67+
createSharedNetworkWithSubnetFallback(cwd, networkName).pipe(
68+
Effect.flatMap((created) => (created ? Effect.void : Effect.fail(error)))
69+
))
70+
)
71+
2372
// CHANGE: ensure shared docker network exists before compose up
2473
// WHY: avoid compose failures when using `external: true` shared network mode
2574
// QUOTE(ТЗ): "Что бы текущие проекты не ложились"
@@ -44,7 +93,7 @@ export const ensureComposeNetworkReady = (
4493
exists
4594
? Effect.void
4695
: Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
47-
Effect.zipRight(runDockerNetworkCreateBridge(cwd, networkName))
96+
Effect.zipRight(ensureSharedNetworkExists(cwd, networkName))
4897
))
4998
)
5099
}

packages/lib/tests/usecases/docker-network-gc.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ type FakeState = {
2121
readonly containerCountByNetwork: Map<string, number>
2222
}
2323

24+
type FakeExecutorOptions = {
25+
readonly failNetworkCreateWithoutSubnet: boolean
26+
}
27+
2428
type SharedTemplate = {
2529
readonly serviceName: string
2630
readonly dockerNetworkMode: "shared"
@@ -38,7 +42,8 @@ const encode = (value: string): Uint8Array => new TextEncoder().encode(value)
3842
const makeFakeExecutor = (
3943
recorded: Array<RecordedCommand>,
4044
initialNetworks: ReadonlyArray<string>,
41-
containerCounts: ReadonlyArray<readonly [string, number]>
45+
containerCounts: ReadonlyArray<readonly [string, number]>,
46+
options: FakeExecutorOptions = { failNetworkCreateWithoutSubnet: false }
4247
): CommandExecutor.CommandExecutor => {
4348
const state: FakeState = {
4449
existingNetworks: new Set(initialNetworks),
@@ -78,8 +83,13 @@ const makeFakeExecutor = (
7883
}
7984

8085
if (isDockerNetworkCreate) {
81-
const networkName = args[4] ?? ""
82-
if (networkName.length > 0) {
86+
const subnetFlagIndex = args.indexOf("--subnet")
87+
const hasSubnet = subnetFlagIndex >= 0
88+
const networkName = hasSubnet ? (args[subnetFlagIndex + 2] ?? "") : (args[4] ?? "")
89+
90+
if (!hasSubnet && options.failNetworkCreateWithoutSubnet) {
91+
exitCode = 1
92+
} else if (networkName.length > 0) {
8393
state.existingNetworks.add(networkName)
8494
if (!state.containerCountByNetwork.has(networkName)) {
8595
state.containerCountByNetwork.set(networkName, 0)
@@ -173,6 +183,45 @@ describe("docker network shared mode", () => {
173183
)
174184
expect(created).toBe(false)
175185
}))
186+
187+
it.effect("falls back to explicit subnet when default network create fails", () =>
188+
Effect.gen(function*(_) {
189+
const recorded: Array<RecordedCommand> = []
190+
const executor = makeFakeExecutor(
191+
recorded,
192+
[],
193+
[],
194+
{ failNetworkCreateWithoutSubnet: true }
195+
)
196+
const template: SharedTemplate = {
197+
serviceName: "dg-test",
198+
dockerNetworkMode: "shared",
199+
dockerSharedNetworkName: "docker-git-shared"
200+
}
201+
202+
yield* _(
203+
ensureComposeNetworkReady("/tmp", template).pipe(
204+
Effect.provideService(CommandExecutor.CommandExecutor, executor)
205+
)
206+
)
207+
208+
const defaultCreateTried = recorded.some(
209+
(entry) =>
210+
entry.command === "docker" &&
211+
entry.args[0] === "network" &&
212+
entry.args[1] === "create" &&
213+
!entry.args.includes("--subnet")
214+
)
215+
const subnetCreateTried = recorded.some(
216+
(entry) =>
217+
entry.command === "docker" &&
218+
entry.args[0] === "network" &&
219+
entry.args[1] === "create" &&
220+
entry.args.includes("--subnet")
221+
)
222+
expect(defaultCreateTried).toBe(true)
223+
expect(subnetCreateTried).toBe(true)
224+
}))
176225
})
177226

178227
describe("docker network gc", () => {

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ describe("prepareProjectFiles", () => {
128128
expect(entrypoint).toContain('OPENCODE_SHARED_HOME="/home/dev/.codex-shared/opencode"')
129129
expect(entrypoint).toContain('OPENCODE_CONFIG_DIR="/home/dev/.config/opencode"')
130130
expect(entrypoint).toContain('"plugin": ["oh-my-opencode"]')
131+
expect(entrypoint).toContain("branch '$REPO_REF' missing; retrying without --branch")
132+
expect(entrypoint).not.toContain("git ls-remote --symref")
131133
expect(composeBefore).toContain(":/home/dev/.docker-git")
132134
expect(composeBefore).not.toContain("dg-test-browser")
133135
expect(composeBefore).toContain("docker-git-shared")

0 commit comments

Comments
 (0)