|
1 | 1 | import type * as CommandExecutor from "@effect/platform/CommandExecutor" |
| 2 | +import type { PlatformError } from "@effect/platform/Error" |
2 | 3 | import type * as FileSystem from "@effect/platform/FileSystem" |
3 | 4 | import type * as Path from "@effect/platform/Path" |
4 | 5 | import { Effect } from "effect" |
5 | 6 |
|
6 | 7 | import { composeFilesForMode, prepareControllerRevision, resolveControllerComposeFiles } from "./controller-compose.js" |
7 | 8 | import { |
8 | | - runCommandCapture, |
| 9 | + runCommandCaptureWithFailureOutput, |
9 | 10 | runCommandExitCode, |
10 | 11 | runCommandExitCodeStreaming, |
11 | 12 | runCommandWithCapturedOutput |
@@ -146,43 +147,235 @@ const formatDockerInvocationFailure = ( |
146 | 147 | `Exit code: ${exitCode}` |
147 | 148 | ].join("\n") |
148 | 149 |
|
| 150 | +// CHANGE: include captured Docker output in command failure diagnostics |
| 151 | +// WHY: callers need typed errors that can distinguish missing images from Docker access failures |
| 152 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 153 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 154 | +// SOURCE: n/a |
| 155 | +// FORMAT THEOREM: output = "" -> base_message; output != "" -> base_message + output |
| 156 | +// PURITY: CORE |
| 157 | +// EFFECT: n/a |
| 158 | +// INVARIANT: the original headline, invocation and exit code are always preserved |
| 159 | +// COMPLEXITY: O(n) where n = |output| |
| 160 | +/** |
| 161 | + * Formats Docker command failure diagnostics with optional captured output. |
| 162 | + * |
| 163 | + * @param headline - Human-readable failure headline. |
| 164 | + * @param invocation - Resolved Docker command invocation. |
| 165 | + * @param exitCode - Process exit code. |
| 166 | + * @param output - Combined stdout/stderr captured from the process. |
| 167 | + * @returns Stable multi-line diagnostic message. |
| 168 | + * |
| 169 | + * @pure true |
| 170 | + * @effect n/a |
| 171 | + * @invariant Empty output does not add an output section. |
| 172 | + * @precondition `headline` is non-empty and `exitCode` is the process exit code. |
| 173 | + * @postcondition The returned message preserves the command and exit code. |
| 174 | + * @complexity O(n) time and O(n) space where n = |output|. |
| 175 | + * @throws Never |
| 176 | + */ |
| 177 | +const formatDockerInvocationFailureWithOutput = ( |
| 178 | + headline: string, |
| 179 | + invocation: DockerInvocation, |
| 180 | + exitCode: number, |
| 181 | + output: string |
| 182 | +): string => |
| 183 | + [ |
| 184 | + formatDockerInvocationFailure(headline, invocation, exitCode), |
| 185 | + output.trim().length > 0 ? `Output:\n${output.trim()}` : "" |
| 186 | + ].filter((part) => part.length > 0).join("\n") |
| 187 | + |
| 188 | +// CHANGE: share Docker command resolution between exit-code and capture paths |
| 189 | +// WHY: all controller Docker operations must use the same direct/sudo resolution and argument composition |
| 190 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 191 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 192 | +// SOURCE: n/a |
| 193 | +// FORMAT THEOREM: resolve(args) = build(resolveDockerCommand(), args) |
| 194 | +// PURITY: SHELL |
| 195 | +// EFFECT: Effect<DockerInvocation, ControllerBootstrapError, ControllerRuntime> |
| 196 | +// INVARIANT: returned invocation always has a concrete command and immutable args |
| 197 | +// COMPLEXITY: O(|args|) |
| 198 | +/** |
| 199 | + * Resolves the Docker executable and composes it with operation arguments. |
| 200 | + * |
| 201 | + * @param args - Docker CLI arguments after the executable. |
| 202 | + * @returns Effect containing the concrete command invocation. |
| 203 | + * |
| 204 | + * @pure false |
| 205 | + * @effect CommandExecutor through Docker probing. |
| 206 | + * @invariant Invocation command defaults to `docker` only when the resolved command list is empty. |
| 207 | + * @precondition `args` is a finite argument vector. |
| 208 | + * @postcondition Sudo/direct Docker probing errors remain typed `ControllerBootstrapError` failures. |
| 209 | + * @complexity O(n) time and O(n) space where n = |args|. |
| 210 | + * @throws Never - all failures are represented in the Effect error channel. |
| 211 | + */ |
| 212 | +const resolveDockerInvocation = ( |
| 213 | + args: ReadonlyArray<string> |
| 214 | +): Effect.Effect<DockerInvocation, ControllerBootstrapError, ControllerRuntime> => |
| 215 | + resolveDockerCommand().pipe( |
| 216 | + Effect.map((dockerCommand) => buildDockerInvocation(dockerCommand, args)) |
| 217 | + ) |
| 218 | + |
149 | 219 | const runDockerExitCodeCommand = ( |
150 | 220 | args: ReadonlyArray<string> |
151 | 221 | ): Effect.Effect<number, ControllerBootstrapError, ControllerRuntime> => |
152 | | - Effect.gen(function*(_) { |
153 | | - const dockerCommand = yield* _(resolveDockerCommand()) |
154 | | - const invocation = buildDockerInvocation(dockerCommand, args) |
155 | | - return yield* _(runExitCode(invocation.command, invocation.args)) |
156 | | - }) |
| 222 | + resolveDockerInvocation(args).pipe( |
| 223 | + Effect.flatMap((invocation) => runExitCode(invocation.command, invocation.args)) |
| 224 | + ) |
157 | 225 |
|
158 | | -export const runDockerCapture = ( |
| 226 | +// CHANGE: preserve typed Docker capture errors while normalizing platform failures |
| 227 | +// WHY: callers must see daemon/socket diagnostics instead of nullable fallback for infrastructure failures |
| 228 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 229 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 230 | +// SOURCE: n/a |
| 231 | +// FORMAT THEOREM: ControllerBootstrapError -> same; PlatformError -> ControllerBootstrapError(label, details) |
| 232 | +// PURITY: CORE |
| 233 | +// EFFECT: n/a |
| 234 | +// INVARIANT: existing ControllerBootstrapError messages are preserved exactly |
| 235 | +// COMPLEXITY: O(|error|) |
| 236 | +/** |
| 237 | + * Builds a mapper from command runner errors into controller bootstrap errors. |
| 238 | + * |
| 239 | + * @param label - Operation label used for platform error diagnostics. |
| 240 | + * @returns A total error mapper for Docker capture effects. |
| 241 | + * |
| 242 | + * @pure true |
| 243 | + * @effect n/a |
| 244 | + * @invariant Existing `ControllerBootstrapError` values are returned unchanged. |
| 245 | + * @precondition `label` is finite human-readable text. |
| 246 | + * @postcondition Non-controller platform errors include the operation label and original details. |
| 247 | + * @complexity O(n) where n = |error string|. |
| 248 | + * @throws Never |
| 249 | + */ |
| 250 | +const mapDockerCaptureError = |
| 251 | + (label: string) => (error: ControllerBootstrapError | PlatformError): ControllerBootstrapError => |
| 252 | + error._tag === "ControllerBootstrapError" |
| 253 | + ? error |
| 254 | + : controllerBootstrapError(`${label} failed.\nDetails: ${String(error)}`) |
| 255 | + |
| 256 | +// CHANGE: choose whether a Docker capture failure includes process output |
| 257 | +// WHY: regular callers keep stable messages, while image inspection needs output for missing-image classification |
| 258 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 259 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 260 | +// SOURCE: n/a |
| 261 | +// FORMAT THEOREM: includeOutput -> failure_with_output; !includeOutput -> base_failure |
| 262 | +// PURITY: CORE |
| 263 | +// EFFECT: n/a |
| 264 | +// INVARIANT: both modes preserve headline, command and exit code |
| 265 | +// COMPLEXITY: O(|output|) |
| 266 | +/** |
| 267 | + * Formats a Docker capture failure according to the selected diagnostic mode. |
| 268 | + * |
| 269 | + * @param label - Operation label. |
| 270 | + * @param invocation - Resolved Docker invocation. |
| 271 | + * @param exitCode - Process exit code. |
| 272 | + * @param output - Combined stdout/stderr from the process. |
| 273 | + * @param includeOutput - Whether the message should include captured process output. |
| 274 | + * @returns Stable Docker failure message. |
| 275 | + * |
| 276 | + * @pure true |
| 277 | + * @effect n/a |
| 278 | + * @invariant Base diagnostics always include command and exit code. |
| 279 | + * @precondition `exitCode` is the observed process exit code. |
| 280 | + * @postcondition Captured output appears only when `includeOutput` is true and output is non-empty. |
| 281 | + * @complexity O(n) where n = |output|. |
| 282 | + * @throws Never |
| 283 | + */ |
| 284 | +const formatDockerCaptureFailure = ( |
| 285 | + label: string, |
| 286 | + invocation: DockerInvocation, |
| 287 | + exitCode: number, |
| 288 | + output: string, |
| 289 | + includeOutput: boolean |
| 290 | +): string => |
| 291 | + includeOutput |
| 292 | + ? formatDockerInvocationFailureWithOutput(`${label} failed.`, invocation, exitCode, output) |
| 293 | + : formatDockerInvocationFailure(`${label} failed.`, invocation, exitCode) |
| 294 | + |
| 295 | +// CHANGE: centralize Docker capture execution for regular and diagnostic modes |
| 296 | +// WHY: selective recovery must not duplicate Docker probing, invocation building, or platform error mapping |
| 297 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 298 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 299 | +// SOURCE: n/a |
| 300 | +// FORMAT THEOREM: docker_exit=0 -> stdout; docker_exit!=0 -> ControllerBootstrapError(mode) |
| 301 | +// PURITY: SHELL |
| 302 | +// EFFECT: Effect<string, ControllerBootstrapError, ControllerRuntime> |
| 303 | +// INVARIANT: no Docker capture failure is converted to success |
| 304 | +// COMPLEXITY: O(command_output) |
| 305 | +/** |
| 306 | + * Runs a Docker command and maps non-zero exits through the selected output mode. |
| 307 | + * |
| 308 | + * @param args - Docker CLI arguments after the executable. |
| 309 | + * @param label - Operation label used in diagnostics. |
| 310 | + * @param includeOutput - Whether non-zero exit diagnostics include captured stdout/stderr. |
| 311 | + * @returns Effect containing stdout on success. |
| 312 | + * |
| 313 | + * @pure false |
| 314 | + * @effect CommandExecutor, FileSystem, Path through ControllerRuntime. |
| 315 | + * @invariant Docker probing and command execution failures stay in the typed error channel. |
| 316 | + * @precondition `args` is finite and `label` is non-empty. |
| 317 | + * @postcondition Success implies Docker exited with code 0. |
| 318 | + * @complexity O(n) time and O(n) space where n is captured output size. |
| 319 | + * @throws Never - all failures are represented in the Effect error channel. |
| 320 | + */ |
| 321 | +const runDockerCaptureWithOutputMode = ( |
159 | 322 | args: ReadonlyArray<string>, |
160 | | - label: string |
| 323 | + label: string, |
| 324 | + includeOutput: boolean |
161 | 325 | ): Effect.Effect<string, ControllerBootstrapError, ControllerRuntime> => |
162 | | - Effect.gen(function*(_) { |
163 | | - const dockerCommand = yield* _(resolveDockerCommand()) |
164 | | - const invocation = buildDockerInvocation(dockerCommand, args) |
165 | | - const output = yield* _( |
166 | | - runCommandCapture( |
| 326 | + resolveDockerInvocation(args).pipe( |
| 327 | + Effect.flatMap((invocation) => |
| 328 | + runCommandCaptureWithFailureOutput( |
167 | 329 | { |
168 | 330 | cwd: process.cwd(), |
169 | 331 | command: invocation.command, |
170 | 332 | args: invocation.args |
171 | 333 | }, |
172 | 334 | [0], |
173 | | - (exitCode) => controllerBootstrapError(formatDockerInvocationFailure(`${label} failed.`, invocation, exitCode)) |
| 335 | + (exitCode, output) => |
| 336 | + controllerBootstrapError(formatDockerCaptureFailure(label, invocation, exitCode, output, includeOutput)) |
174 | 337 | ) |
175 | | - ) |
176 | | - |
177 | | - return output |
178 | | - }).pipe( |
179 | | - Effect.mapError((error): ControllerBootstrapError => |
180 | | - error._tag === "ControllerBootstrapError" |
181 | | - ? error |
182 | | - : controllerBootstrapError(`${label} failed.\nDetails: ${String(error)}`) |
183 | | - ) |
| 338 | + ), |
| 339 | + Effect.mapError(mapDockerCaptureError(label)) |
184 | 340 | ) |
185 | 341 |
|
| 342 | +export const runDockerCapture = ( |
| 343 | + args: ReadonlyArray<string>, |
| 344 | + label: string |
| 345 | +): Effect.Effect<string, ControllerBootstrapError, ControllerRuntime> => |
| 346 | + runDockerCaptureWithOutputMode(args, label, false) |
| 347 | + |
| 348 | +// CHANGE: preserve Docker stderr/stdout diagnostics for selective error recovery |
| 349 | +// WHY: image revision inspection must fallback only for absent images while surfacing daemon/socket failures |
| 350 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 351 | +// REF: CodeRabbit PR #344 review 4349265315 |
| 352 | +// SOURCE: n/a |
| 353 | +// FORMAT THEOREM: docker_exit ∉ ok -> ControllerBootstrapError(message includes output) |
| 354 | +// PURITY: SHELL |
| 355 | +// EFFECT: Effect<string, ControllerBootstrapError, ControllerRuntime> |
| 356 | +// INVARIANT: Docker access resolution errors remain ControllerBootstrapError failures |
| 357 | +// COMPLEXITY: O(command_output) |
| 358 | +/** |
| 359 | + * Runs a Docker command and includes captured stdout/stderr in the typed failure message. |
| 360 | + * |
| 361 | + * @param args - Docker CLI arguments after the resolved docker executable. |
| 362 | + * @param label - Human-readable operation label used in failure diagnostics. |
| 363 | + * @returns Effect containing stdout when Docker exits successfully. |
| 364 | + * |
| 365 | + * @pure false |
| 366 | + * @effect CommandExecutor, FileSystem, Path through ControllerRuntime. |
| 367 | + * @invariant Non-zero Docker exits are failures and preserve the combined command output. |
| 368 | + * @precondition `args` is a finite Docker argument vector and `label` is non-empty. |
| 369 | + * @postcondition Docker daemon/socket discovery errors are not converted to success. |
| 370 | + * @complexity O(n) time and O(n) space where n is captured command output. |
| 371 | + * @throws Never - all failures are represented in the Effect error channel. |
| 372 | + */ |
| 373 | +export const runDockerCaptureWithFailureOutput = ( |
| 374 | + args: ReadonlyArray<string>, |
| 375 | + label: string |
| 376 | +): Effect.Effect<string, ControllerBootstrapError, ControllerRuntime> => |
| 377 | + runDockerCaptureWithOutputMode(args, label, true) |
| 378 | + |
186 | 379 | export const runCompose = ( |
187 | 380 | args: ReadonlyArray<string> |
188 | 381 | ): Effect.Effect<void, ControllerBootstrapError, ControllerRuntime> => |
|
0 commit comments