Skip to content

Commit c7ca51b

Browse files
committed
feat: add external MySQL runtime provider
1 parent 869e0f7 commit c7ca51b

7 files changed

Lines changed: 409 additions & 19 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@
247247
"test:generic-ability-runtime-run": "tsx tests/generic-ability-runtime-run.test.ts",
248248
"test:provider-runtime-contracts": "tsx tests/provider-runtime-contracts.test.ts",
249249
"test:runtime-requirements-readiness": "tsx tests/runtime-requirements-readiness.test.ts",
250-
"test:runtime-services": "tsx tests/runtime-services.test.ts",
250+
"test:runtime-services": "tsx tests/runtime-services.test.ts && tsx tests/external-mysql-runtime-service.test.ts",
251251
"test:runtime-services-lifecycle": "tsx tests/runtime-services-lifecycle.test.ts",
252252
"test:disposable-mysql-mysqli-e2e": "tsx tests/disposable-mysql-mysqli.integration.test.ts",
253253
"test:runtime-contract-manifest": "tsx tests/runtime-contract-manifest.test.ts",

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
140140
}
141141
const secretEnvResolution = resolveRecipeSecretEnv(recipe.inputs?.secretEnv ?? [], { field: "--secret-env name" })
142142
const secretEnv = secretEnvResolution.values
143-
const effectivePolicy = Object.keys(secretEnv).length > 0 ? { ...policy, secrets: "connector-scoped" as const } : policy
143+
let effectivePolicy = Object.keys(secretEnv).length > 0 ? { ...policy, secrets: "connector-scoped" as const } : policy
144144
let workspaceMounts: PreparedWorkspaceMount[] = []
145145
let extraPlugins: PreparedExtraPlugin[] = []
146146
let dependencyOverlays: PreparedDependencyOverlay[] = []
@@ -198,6 +198,11 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
198198
))
199199
serviceEvidence = managedServices.evidence
200200
Object.assign(runtimeEnv, managedServices.env)
201+
for (const [name, value] of Object.entries(managedServices.secretEnv)) {
202+
delete runtimeEnv[name]
203+
secretEnv[name] = value
204+
}
205+
if (Object.keys(managedServices.secretEnv).length > 0) effectivePolicy = { ...policy, secrets: "connector-scoped" }
201206
const runtimeEnvironment = {
202207
kind: "wordpress" as const,
203208
name: plan.runtime.name,

packages/cli/src/recipe-validation.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,17 +733,28 @@ export async function validateWorkspaceRecipeSemantics(recipe: WorkspaceRecipe,
733733

734734
function validateRecipeRuntimeServices(recipe: WorkspaceRecipe, addIssue: (code: string, path: string, message: string) => void): void {
735735
const ids = new Set<string>()
736-
const environment = new Set<string>([
736+
const exposedEnvironment = new Set<string>([
737737
...Object.keys(recipe.distribution?.env ?? {}),
738738
...Object.keys(recipe.inputs?.runtimeEnv ?? {}),
739739
...(recipe.inputs?.secretEnv ?? []),
740740
])
741+
const environment = new Set(exposedEnvironment)
741742
for (const [index, service] of (recipe.inputs?.services ?? []).entries()) {
742743
const path = `$.inputs.services[${index}]`
743744
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(service.id)) addIssue("invalid-runtime-service-id", `${path}.id`, "Runtime service ids must be stable identifiers.")
744745
if (ids.has(service.id)) addIssue("duplicate-runtime-service-id", `${path}.id`, `Runtime service ids must be unique: ${service.id}`)
745746
ids.add(service.id)
746747
if (!["mysql", "redis", "smtp", "http"].includes(service.kind)) addIssue("unsupported-runtime-service-kind", `${path}.kind`, `Unsupported managed runtime service kind: ${service.kind}`)
748+
if (service.configuration?.provider === "external") {
749+
if (service.kind !== "mysql") addIssue("unsupported-runtime-service-provider", `${path}.configuration.provider`, "The external provider supports only MySQL-compatible services.")
750+
for (const field of ["hostEnv", "portEnv", "usernameEnv", "passwordEnv"] as const) {
751+
const name = service.configuration[field]
752+
if (name && exposedEnvironment.has(name)) addIssue("runtime-service-admin-env-exposed", `${path}.configuration.${field}`, `External service administration environment must remain host-only: ${name}`)
753+
}
754+
for (const field of ["image", "rootAuthentication", "foreignKeyTargetPolicy"] as const) {
755+
if (service.configuration[field] !== undefined) addIssue("unsupported-external-runtime-service-option", `${path}.configuration.${field}`, `External MySQL services do not support ${field}.`)
756+
}
757+
}
747758
const supportedOutputs: Record<string, RegExp> = {
748759
mysql: /^(host|port|username|password|database)$/,
749760
redis: /^(host|port|url)$/,

0 commit comments

Comments
 (0)