Skip to content

Commit 40d8586

Browse files
committed
Make MySQL provisioning pull-aware
1 parent 1da94d5 commit 40d8586

3 files changed

Lines changed: 34 additions & 6 deletions

File tree

packages/cli/src/commands/recipe-run.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { RecipePhaseError } from "./recipe-run-phases.js"
2929
import { markPreviewLeaseAvailable, markPreviewLeaseFailed, markPreviewLeaseReleased, startPreviewLeaseRecipeRun } from "./preview-lease.js"
3030
import { importRecipeSiteSeeds } from "./recipe-site-seeds.js"
3131
import { applyRecipeRuntimeSetup, cleanupInputMountBaselines, prepareRecipeRuntimeSetup, recipeRunDependencyOverlay, recipeRunExtraPlugin, recipeRunStagedFile, rewriteInputMountPathArgs } from "./recipe-runtime-setup.js"
32-
import { provisionRuntimeServices, RuntimeServiceProvisionError, type RuntimeServiceEvidence } from "../runtime-services.js"
32+
import { provisionRuntimeServices, runtimeServiceEvidenceFromError, type RuntimeServiceEvidence } from "../runtime-services.js"
3333
import { distributionStartupProbeFailure, executeRecipeCollectWorkloadResult, executeRecipeWorkflowStep, recipeAdvisoryFailure, recipeBrowserEvidence, recipeStepFailure, recipeWorkflowArgsEvidence, recipeWorkflowStepIsAdvisory, runDistributionSetupArtifacts, runDistributionStartupProbes, runRecipeProbes, withRecipeExecutionPhase } from "./recipe-run-workflow-evidence.js"
3434
import type { RecipeAdvisoryFailure, RecipeBrowserEvidence, RecipeDiagnosticArtifactRef, RecipeEffectiveRecipeArtifact, RecipeExecutionResult, RecipeFuzzCaseCommandRef, RecipeFuzzCaseResult, RecipeFuzzCaseStatus, RecipeFuzzRunResult, RecipeInterruptionController, RecipePhaseEvidence, RecipePhaseName, RecipePhpWasmRuntimeDiagnostic, RecipeRunCommandOutput, RecipeRunComponentContract, RecipeRunDeclaredArtifact, RecipeRunDistributionSetupArtifact, RecipeRunDistributionStartupProbe, RecipeRunFixtureDatabase, RecipeRunOptions, RecipeRunOutput, RecipeRunProbe, RecipeRunProvenance, RecipeRunStagedFile, RecipeRuntimeDiagnostic, RecipeStepFailure, RecipeValidateOptions, RecipeValidateOutput } from "./recipe-run-types.js"
3535

@@ -422,8 +422,9 @@ async function runRecipe(options: RecipeRunOptions, interruption?: RecipeInterru
422422
output: { ...completedRecipeOutputFields({ executions, componentContracts: componentContractResults(recipe, extraPlugins, phaseTracker.list(), executions), stagedFiles: stagedFiles.map(recipeRunStagedFile), fixtureDatabases, siteSeeds, distributionSetupArtifacts, distributionStartupProbes, probes, declaredArtifacts, stepFailures, phaseEvidence: phaseTracker.list(), advisoryFailures, browserEvidence, benchResultsList, fuzzRun: fuzzRunResult, evidence }), provenance: recipeRunProvenance(recipe, recipePath) },
423423
})
424424
} catch (error) {
425-
if (error instanceof RuntimeServiceProvisionError) {
426-
serviceEvidence = error.evidence
425+
const failedServiceEvidence = runtimeServiceEvidenceFromError(error)
426+
if (failedServiceEvidence) {
427+
serviceEvidence = failedServiceEvidence
427428
await runRegistry.update(runRecord.runId, { metadata: { managedRuntimeServices: serviceEvidence } })
428429
}
429430
await markPreviewLeaseFailed(options.previewLeaseFile, error)

packages/cli/src/runtime-services.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ export class RuntimeServiceProvisionError extends Error {
2525
}
2626
}
2727

28+
export function runtimeServiceEvidenceFromError(error: unknown): RuntimeServiceEvidence[] | undefined {
29+
let current = error
30+
const seen = new Set<unknown>()
31+
while (current instanceof Error && !seen.has(current)) {
32+
if (current instanceof RuntimeServiceProvisionError) return current.evidence
33+
seen.add(current)
34+
current = current.cause
35+
}
36+
return undefined
37+
}
38+
2839
interface ManagedRuntimeService {
2940
env: Record<string, string>
3041
evidence: RuntimeServiceEvidence
@@ -103,6 +114,7 @@ async function provisionMysqlDockerService(service: WorkspaceRecipeRuntimeServic
103114
let started = false
104115
try {
105116
throwIfAborted(signal)
117+
await ensureDockerImage(dependencies, signal)
106118
await dependencies.execute("docker", runArgs, { env: childEnvironment, signal, timeout: 30_000 })
107119
started = true
108120
const { stdout } = await dependencies.execute("docker", ["port", container, "3306/tcp"], { signal, timeout: 10_000 })
@@ -122,6 +134,15 @@ async function provisionMysqlDockerService(service: WorkspaceRecipeRuntimeServic
122134
}
123135
}
124136

137+
async function ensureDockerImage(dependencies: RuntimeServiceDependencies, signal?: AbortSignal): Promise<void> {
138+
try {
139+
await dependencies.execute("docker", ["image", "inspect", MYSQL_IMAGE], { signal, timeout: 10_000 })
140+
} catch {
141+
throwIfAborted(signal)
142+
await dependencies.execute("docker", ["pull", MYSQL_IMAGE], { signal, timeout: 5 * 60_000 })
143+
}
144+
}
145+
125146
async function releaseServices(services: ManagedRuntimeService[]): Promise<void> {
126147
const results = await Promise.allSettled([...services].reverse().map(async (service) => await service.release()))
127148
const failure = results.find((result): result is PromiseRejectedResult => result.status === "rejected")

tests/runtime-services.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert/strict"
22
import { createServer } from "node:net"
3-
import { parseLoopbackPort, provisionRuntimeServices, RuntimeServiceProvisionError, runtimeServicePlan, waitForMysqlProtocol, type RuntimeServiceDependencies } from "../packages/cli/src/runtime-services.ts"
3+
import { parseLoopbackPort, provisionRuntimeServices, RuntimeServiceProvisionError, runtimeServiceEvidenceFromError, runtimeServicePlan, waitForMysqlProtocol, type RuntimeServiceDependencies } from "../packages/cli/src/runtime-services.ts"
44
import { validateWorkspaceRecipeJsonSchema } from "../packages/runtime-core/src/recipe-schema.ts"
55

66
const service = { id: "test-db", kind: "mysql", outputs: { host: "DB_HOST", port: "DB_PORT", password: "DB_PASSWORD" } } as const
@@ -35,9 +35,11 @@ const dependencies: RuntimeServiceDependencies = {
3535
const provisioned = await provisionRuntimeServices([service], { dependencies })
3636
assert.equal(provisioned.env.DB_PORT, "41001")
3737
assert.equal(provisioned.env.DB_PASSWORD, Buffer.alloc(24, 7).toString("base64url"))
38-
assert.ok(calls[0]?.args.includes("MYSQL_PASSWORD"))
39-
assert.equal(calls[0]?.args.some((arg) => arg.includes(provisioned.env.DB_PASSWORD)), false, "credentials never enter Docker argv")
38+
const runCall = calls.find((call) => call.args[0] === "run")
39+
assert.ok(runCall?.args.includes("MYSQL_PASSWORD"))
40+
assert.equal(runCall?.args.some((arg) => arg.includes(provisioned.env.DB_PASSWORD)), false, "credentials never enter Docker argv")
4041
assert.equal(JSON.stringify(provisioned.evidence).includes(provisioned.env.DB_PASSWORD), false, "credentials never enter evidence")
42+
assert.equal(calls[0]?.args[0], "image", "the provider checks the image before starting the service")
4143
await provisioned.release()
4244
await provisioned.release()
4345
assert.equal(calls.filter((call) => call.args[0] === "rm").length, 1, "release is idempotent")
@@ -71,4 +73,8 @@ assert.equal(failedCleanup, true)
7173
const controller = new AbortController()
7274
controller.abort()
7375
await assert.rejects(provisionRuntimeServices([service], { dependencies, signal: controller.signal }), (error: unknown) => error instanceof RuntimeServiceProvisionError && error.evidence[0]?.diagnostic?.code === "interrupted")
76+
77+
const nestedEvidence = [{ id: "nested", kind: "mysql", provider: "test", version: "test", readiness: "failed", lifecycle: "failed" }] satisfies import("../packages/cli/src/runtime-services.ts").RuntimeServiceEvidence[]
78+
const nestedError = new Error("phase failed", { cause: new RuntimeServiceProvisionError("service failed", nestedEvidence) })
79+
assert.equal(runtimeServiceEvidenceFromError(nestedError), nestedEvidence, "phase wrappers retain structured service evidence")
7480
console.log("runtime services tests passed")

0 commit comments

Comments
 (0)