Skip to content

Commit 5e682fd

Browse files
committed
feat(runtime-services): add native ephemeral MariaDB provider
1 parent ea2f111 commit 5e682fd

17 files changed

Lines changed: 799 additions & 19 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Native MariaDB Runtime Service
2+
3+
`provider: "native"` provisions a short-lived MariaDB process for MySQL-compatible workloads on Docker-free hosts. The generic runtime-service layer owns its complete lifecycle. Recipes may select only `provider: "native"` and `engine: "mariadb"`; host, port, credentials, sockets, configuration files, and filesystem paths are not configurable.
4+
5+
## Isolation Contract
6+
7+
- The provider resolves and validates `mariadbd`, `mariadb-install-db`, and `mariadb` identities before allocation.
8+
- Every run gets a mode-`0700` `mkdtemp` root. The data directory, temporary directory, socket, PID file, and error log are children of that root.
9+
- Initialization, daemon, and client argument arrays begin with `--no-defaults`. No shell or default socket is used.
10+
- Administration uses only the private Unix socket. Workloads receive a generated least-privilege `runtime` account over loopback TCP through the existing ephemeral connector-secret channel.
11+
- Cleanup signals only the retained direct `ChildProcess`; the PID file is never trusted or read. Linux captures the owned process start-time token and revalidates it before every signal to reject PID reuse. Root device/inode identity and a symlink-free tree are revalidated before recursive removal.
12+
- Failures, aborts, timeouts, startup crashes, and partial initialization all enter the same graceful-shutdown, forced-shutdown, wait, and verified-removal state machine. Cleanup failure is terminal and retained in bounded lifecycle evidence.
13+
- Evidence contains service ID, engine/provider version, lifecycle state, and memory measurements only. It never contains credentials or private absolute paths.
14+
15+
## Daemon Arguments
16+
17+
The provider starts `mariadbd` directly with these fixed controls plus provider-owned absolute paths and a freshly allocated loopback port:
18+
19+
```text
20+
--no-defaults
21+
--datadir=<private-root>/data
22+
--socket=<private-root>/server.sock
23+
--pid-file=<private-root>/server.pid
24+
--log-error=<private-root>/server.log
25+
--tmpdir=<private-root>/tmp
26+
--bind-address=127.0.0.1
27+
--port=<ephemeral>
28+
--skip-name-resolve
29+
--skip-log-bin
30+
--skip-host-cache
31+
--skip-slave-start
32+
--skip-symbolic-links
33+
--local-infile=OFF
34+
--performance-schema=OFF
35+
--skip-feedback
36+
--innodb-buffer-pool-size=32M
37+
--innodb-log-buffer-size=4M
38+
--key-buffer-size=8M
39+
--aria-pagecache-buffer-size=8M
40+
--max-connections=10
41+
--thread-cache-size=0
42+
--table-open-cache=128
43+
--table-definition-cache=128
44+
--tmp-table-size=8M
45+
--max-heap-table-size=8M
46+
```
47+
48+
The configured service budget is 128 MiB. Linux runs record the owned daemon's post-readiness RSS in evidence; the MariaDB 10.11 integration gate requires the observed value to remain within that budget.
49+
50+
## Integration Contract
51+
52+
Homeboy Extensions issue #2412 may select this provider by emitting the generic runtime-service recipe declaration below. WP Codebox performs all provisioning and cleanup; callers do not supply or manage native process details.
53+
54+
```json
55+
{
56+
"id": "wordpress-database",
57+
"kind": "mysql",
58+
"configuration": { "provider": "native", "engine": "mariadb" },
59+
"outputs": {
60+
"host": "DB_HOST",
61+
"port": "DB_PORT",
62+
"username": "DB_USER",
63+
"password": "DB_PASSWORD",
64+
"database": "DB_NAME"
65+
}
66+
}
67+
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,9 @@
248248
"test:generic-ability-runtime-run": "tsx tests/generic-ability-runtime-run.test.ts",
249249
"test:provider-runtime-contracts": "tsx tests/provider-runtime-contracts.test.ts",
250250
"test:runtime-requirements-readiness": "tsx tests/runtime-requirements-readiness.test.ts",
251-
"test:runtime-services": "tsx tests/runtime-services.test.ts && tsx tests/external-mysql-runtime-service.test.ts",
251+
"test:runtime-services": "tsx tests/runtime-services.test.ts && tsx tests/external-mysql-runtime-service.test.ts && tsx tests/native-mariadb-runtime-service.test.ts",
252252
"test:runtime-services-lifecycle": "tsx tests/runtime-services-lifecycle.test.ts",
253+
"test:native-mariadb-runtime-service-integration": "tsx tests/native-mariadb-runtime-service.integration.test.ts",
253254
"test:disposable-mysql-mysqli-e2e": "tsx tests/disposable-mysql-mysqli.integration.test.ts",
254255
"test:runtime-contract-manifest": "tsx tests/runtime-contract-manifest.test.ts",
255256
"test:runtime-contract-package-exports": "tsx tests/runtime-contract-package-exports.test.ts",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ArtifactBundle, RecipeRunSummary, RuntimeInfo } from "@automattic/
44
import { stripUndefined } from "@automattic/wp-codebox-core/internals"
55
import type { RunOutput } from "../runtime-command-wrappers.js"
66
import type { RecipeArtifactPointerCommandStatus, RecipeArtifactPointerState, RecipeBrowserEvidence, RecipeDiagnosticArtifactRef, RecipePhaseEvidence } from "./recipe-run-types.js"
7+
import type { RuntimeServiceEvidence } from "../runtime-services.js"
78

89
export class RecipeArtifactPointerTracker {
910
private command: string | undefined
@@ -15,6 +16,7 @@ export class RecipeArtifactPointerTracker {
1516
private browserEvidence: RecipeBrowserEvidence[] = []
1617
private diagnosticArtifacts: RecipeDiagnosticArtifactRef[] = []
1718
private result: RecipeRunSummary | undefined
19+
private managedRuntimeServices: RuntimeServiceEvidence[] = []
1820

1921
constructor(private readonly directory: string | undefined, private readonly runId: string, private readonly recipePath: string, private readonly startedAt: string) {}
2022

@@ -32,6 +34,7 @@ export class RecipeArtifactPointerTracker {
3234
this.browserEvidence = state.browserEvidence ?? this.browserEvidence
3335
this.diagnosticArtifacts = state.diagnosticArtifacts ?? this.diagnosticArtifacts
3436
this.result = state.result ?? this.result
37+
this.managedRuntimeServices = state.managedRuntimeServices ?? this.managedRuntimeServices
3538

3639
const pointer = stripUndefined({
3740
schema: "wp-codebox/recipe-run-artifact-pointer/v1",
@@ -47,6 +50,7 @@ export class RecipeArtifactPointerTracker {
4750
failure: this.failure,
4851
failurePhase: recipeArtifactPointerFailurePhase(this.failure, this.phases),
4952
result: this.result,
53+
managedRuntimeServices: this.managedRuntimeServices.length > 0 ? this.managedRuntimeServices : undefined,
5054
browserEvidence: this.browserEvidence.length > 0 ? this.browserEvidence : undefined,
5155
diagnosticArtifacts: this.diagnosticArtifacts.length > 0 ? this.diagnosticArtifacts : undefined,
5256
...await recipeArtifactPointerArtifactState(this.directory, this.runtime, this.artifacts),

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ interface RecipeRunCommonOutputFields {
6767
replayStatus?: RecipeRunOutput["replayStatus"]
6868
fuzzRun?: RecipeRunOutput["fuzzRun"]
6969
provenance?: RecipeRunOutput["provenance"]
70+
managedRuntimeServices?: RecipeRunOutput["managedRuntimeServices"]
7071
artifacts?: ArtifactBundle
7172
interruption?: RecipeRunOutput["interruption"]
7273
}
@@ -194,7 +195,7 @@ export async function finalizeCompletedRecipeRun(args: FinalizeCompletedRecipeRu
194195
}) as RecipeRunOutput)
195196
runRecord = await args.runRegistry.update(args.runRecord.runId, { result: runtimeRunResultFromRecipeSummary(output.result!) })
196197
output.run = runRecord
197-
await args.artifactPointer.update({ commandStatus: args.success ? "completed" : "failed", runtime: args.runtime, artifacts: args.artifacts, failure: args.failure, phases: args.phaseEvidence, stepFailures: args.output.stepFailures, browserEvidence: args.browserEvidence, result: output.result })
198+
await args.artifactPointer.update({ commandStatus: args.success ? "completed" : "failed", runtime: args.runtime, artifacts: args.artifacts, failure: args.failure, phases: args.phaseEvidence, stepFailures: args.output.stepFailures, browserEvidence: args.browserEvidence, managedRuntimeServices: args.output.managedRuntimeServices, result: output.result })
198199
return output
199200
}
200201

@@ -220,19 +221,19 @@ export async function finalizeRecoveredRecipeFailure(args: FinalizeRecoveredReci
220221
}) as RecipeRunOutput)
221222
runRecord = await args.runRegistry.update(args.runRecord.runId, { result: runtimeRunResultFromRecipeSummary(output.result!) })
222223
output.run = runRecord
223-
await args.artifactPointer.update({ commandStatus: "failed", ...(args.runtime ? { runtime: args.runtime } : {}), ...(args.artifacts ? { artifacts: args.artifacts } : {}), failure: args.serializedError, phases: args.phaseEvidence, stepFailures: args.output.stepFailures, browserEvidence: args.browserEvidence, diagnosticArtifacts: args.diagnosticArtifacts, result: output.result })
224+
await args.artifactPointer.update({ commandStatus: "failed", ...(args.runtime ? { runtime: args.runtime } : {}), ...(args.artifacts ? { artifacts: args.artifacts } : {}), failure: args.serializedError, phases: args.phaseEvidence, stepFailures: args.output.stepFailures, browserEvidence: args.browserEvidence, diagnosticArtifacts: args.diagnosticArtifacts, managedRuntimeServices: args.output.managedRuntimeServices, result: output.result })
224225
return output
225226
}
226227

227-
export async function runRecipeCleanup(runRegistry: RuntimeRunRegistry, runRecord: RuntimeRunRecord, cleanup: () => Promise<void>): Promise<RunResourceCleanupEvidence> {
228+
export async function runRecipeCleanup(runRegistry: RuntimeRunRegistry, runRecord: RuntimeRunRecord, cleanup: () => Promise<void>, finalMetadata?: () => Record<string, unknown>): Promise<RunResourceCleanupEvidence> {
228229
const startedAtMs = Date.now()
229230
await runRegistry.update(runRecord.runId, { cleanup: { status: "running" } })
230231
try {
231232
await cleanup()
232-
const updatedRunRecord = await runRegistry.update(runRecord.runId, { cleanup: { status: "succeeded" } })
233+
const updatedRunRecord = await runRegistry.update(runRecord.runId, { cleanup: { status: "succeeded" }, ...(finalMetadata ? { metadata: finalMetadata() } : {}) })
233234
return cleanupEvidenceFromRunRecord(updatedRunRecord, Date.now() - startedAtMs)
234235
} catch (error) {
235-
const updatedRunRecord = await runRegistry.update(runRecord.runId, { cleanup: { status: "failed", error: serializeError(error) } })
236+
const updatedRunRecord = await runRegistry.update(runRecord.runId, { cleanup: { status: "failed", error: serializeError(error) }, ...(finalMetadata ? { metadata: finalMetadata() } : {}) })
236237
const cleanupError = serializeRecipeRunError(error)
237238
throw new RunResourceCleanupError(
238239
cleanupEvidenceFromRunRecord(updatedRunRecord, Date.now() - startedAtMs, cleanupError),

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { RecipeExternalServiceBoundaryHostCorrelation } from "../recipe-ext
55
import type { RecipeValidationIssue, RecipeWorkflowPhase } from "../recipe-validation.js"
66
import type { RunOutput } from "../runtime-command-wrappers.js"
77
import type { RecipeAdversarialCampaignOutput } from "../adversarial-recipe.js"
8+
import type { RuntimeServiceEvidence } from "../runtime-services.js"
89

910
export interface RecipeRunOptions {
1011
recipePath: string
@@ -83,6 +84,7 @@ export interface RecipeRunOutput {
8384
fuzzRun?: RecipeFuzzRunResult
8485
adversarialCampaigns?: RecipeAdversarialCampaignOutput[]
8586
provenance?: RecipeRunProvenance
87+
managedRuntimeServices?: RuntimeServiceEvidence[]
8688
result?: RecipeRunSummary
8789
artifacts?: ArtifactBundle
8890
run?: RuntimeRunRecord
@@ -263,6 +265,7 @@ export interface RecipeArtifactPointerState {
263265
browserEvidence?: RecipeBrowserEvidence[]
264266
diagnosticArtifacts?: RecipeDiagnosticArtifactRef[]
265267
result?: RecipeRunSummary
268+
managedRuntimeServices?: RuntimeServiceEvidence[]
266269
}
267270

268271
export interface RecipeEffectiveRecipeArtifact {

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
445445
browserEvidence,
446446
replayStatus: evidence.replayStatus ? recipeReplayStatusOutput(evidence.replayStatus) : undefined,
447447
failure: recipeFailure,
448-
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 }), ...(adversarialCampaigns.length > 0 ? { adversarialCampaigns } : {}), provenance: recipeRunProvenance(recipe, recipePath) },
448+
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 }), ...(adversarialCampaigns.length > 0 ? { adversarialCampaigns } : {}), provenance: recipeRunProvenance(recipe, recipePath), managedRuntimeServices: serviceEvidence },
449449
})
450450
}
451451

@@ -464,7 +464,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
464464
phaseEvidence: phaseTracker.list(),
465465
browserEvidence,
466466
replayStatus: evidence.replayStatus ? recipeReplayStatusOutput(evidence.replayStatus) : undefined,
467-
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 }), ...(adversarialCampaigns.length > 0 ? { adversarialCampaigns } : {}), provenance: recipeRunProvenance(recipe, recipePath) },
467+
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 }), ...(adversarialCampaigns.length > 0 ? { adversarialCampaigns } : {}), provenance: recipeRunProvenance(recipe, recipePath), managedRuntimeServices: serviceEvidence },
468468
})
469469
} catch (error) {
470470
const failedServiceEvidence = runtimeServiceEvidenceFromError(error)
@@ -592,6 +592,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
592592
...(adversarialCampaigns.length > 0 ? { adversarialCampaigns } : {}),
593593
diagnostics: recipeRuntimeDiagnostics(recipe, executions, error),
594594
provenance: recipeRunProvenance(recipe, recipePath, diagnosticArtifacts),
595+
managedRuntimeServices: serviceEvidence,
595596
},
596597
})
597598
} finally {
@@ -611,16 +612,12 @@ export async function runManagedServiceCleanup(
611612
cleanup: () => Promise<void>,
612613
): Promise<RunResourceCleanupEvidence> {
613614
try {
614-
return await runRecipeCleanup(runRegistry, runRecord, cleanup)
615+
return await runRecipeCleanup(runRegistry, runRecord, cleanup, () => ({ managedRuntimeServices: serviceEvidence }))
615616
} catch (error) {
616617
if (preservePrimaryFailure && error instanceof RunResourceCleanupError) {
617618
return error.evidence
618619
}
619620
throw error
620-
} finally {
621-
await runRegistry.update(runRecord.runId, {
622-
metadata: { managedRuntimeServices: serviceEvidence },
623-
})
624621
}
625622
}
626623

packages/cli/src/recipe-validation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,13 @@ function validateRecipeRuntimeServices(recipe: WorkspaceRecipe, addIssue: (code:
805805
if (service.configuration[field] !== undefined) addIssue("unsupported-external-runtime-service-option", `${path}.configuration.${field}`, `External MySQL services do not support ${field}.`)
806806
}
807807
}
808+
if (service.configuration?.provider === "native") {
809+
if (service.kind !== "mysql") addIssue("unsupported-runtime-service-provider", `${path}.configuration.provider`, "The native provider supports only MySQL-compatible services.")
810+
if (service.configuration.engine !== "mariadb") addIssue("unsupported-native-runtime-service-engine", `${path}.configuration.engine`, "The native provider requires engine=mariadb.")
811+
for (const field of ["externalService", "hostEnv", "portEnv", "usernameEnv", "passwordEnv", "image", "rootAuthentication", "foreignKeyTargetPolicy", "responseStatus", "responseBody"] as const) {
812+
if (service.configuration[field] !== undefined) addIssue("unsupported-native-runtime-service-option", `${path}.configuration.${field}`, `Native MariaDB services do not accept ${field}.`)
813+
}
814+
}
808815
const supportedOutputs: Record<string, RegExp> = {
809816
mysql: /^(host|port|username|password|database)$/,
810817
redis: /^(host|port|url)$/,

0 commit comments

Comments
 (0)