fix: migrate external runtime PostgreSQL schema
This commit is contained in:
@@ -261,7 +261,16 @@ async function plannedFiles(args) {
|
|||||||
if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations }));
|
if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations }));
|
||||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, nodeId: args.nodeId, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, nodeId: args.nodeId, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||||
if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source, deploy }));
|
if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({
|
||||||
|
profile,
|
||||||
|
config: externalPostgres,
|
||||||
|
source,
|
||||||
|
deploy,
|
||||||
|
migrationSources,
|
||||||
|
catalog: artifactCatalog,
|
||||||
|
registryPrefix: args.registryPrefix,
|
||||||
|
useDeployImages: args.useDeployImages
|
||||||
|
}));
|
||||||
if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile), deploy }));
|
if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile), deploy }));
|
||||||
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source, deploy }));
|
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source, deploy }));
|
||||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source, deploy }));
|
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source, deploy }));
|
||||||
|
|||||||
@@ -728,7 +728,22 @@ test("v03 render keeps node identity as data instead of generated structure", as
|
|||||||
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
||||||
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
||||||
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
||||||
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["Service", "EndpointSlice"]);
|
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["ConfigMap", "Job", "Service", "EndpointSlice"]);
|
||||||
|
const externalPostgresMigrationConfig = (externalPostgres.items ?? []).find((item) => item.kind === "ConfigMap");
|
||||||
|
const externalPostgresMigrationJob = (externalPostgres.items ?? []).find((item) => item.kind === "Job");
|
||||||
|
assert.deepEqual(
|
||||||
|
Object.keys(externalPostgresMigrationConfig?.data ?? {}).filter((name) => name.endsWith(".sql")),
|
||||||
|
CLOUD_CORE_MIGRATIONS.map((migration) => path.basename(migration.path))
|
||||||
|
);
|
||||||
|
assert.equal(externalPostgresMigrationJob?.metadata?.annotations?.["argocd.argoproj.io/sync-wave"], "-1");
|
||||||
|
assert.equal(
|
||||||
|
externalPostgresMigrationJob?.spec?.template?.spec?.containers?.[0]?.image,
|
||||||
|
`127.0.0.1:5000/hwlab/hwlab-cloud-api-env@sha256:${"1".repeat(64)}`
|
||||||
|
);
|
||||||
|
assert.deepEqual(externalPostgresMigrationJob?.spec?.template?.spec?.containers?.[0]?.env?.[0]?.valueFrom?.secretKeyRef, {
|
||||||
|
name: "hwlab-cloud-api-v03-db",
|
||||||
|
key: "database-url"
|
||||||
|
});
|
||||||
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
||||||
assert.equal(externalPostgresSlice?.metadata?.name, "jd01-pk01-platform-postgres-host");
|
assert.equal(externalPostgresSlice?.metadata?.name, "jd01-pk01-platform-postgres-host");
|
||||||
assert.equal(externalPostgresSlice?.metadata?.labels?.["kubernetes.io/service-name"], "jd01-pk01-platform-postgres");
|
assert.equal(externalPostgresSlice?.metadata?.labels?.["kubernetes.io/service-name"], "jd01-pk01-platform-postgres");
|
||||||
|
|||||||
@@ -740,8 +740,24 @@ function externalPostgresConfigForLane(deploy, profile, args = {}) {
|
|||||||
return { serviceName: effective.serviceName, endpointAddress: effective.endpointAddress, port };
|
return { serviceName: effective.serviceName, endpointAddress: effective.endpointAddress, port };
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalPostgresManifest({ profile = "v03", config, source, deploy = null }) {
|
function externalPostgresManifest({ profile = "v03", config, source, deploy = null, migrationSources = [], catalog = null, registryPrefix = defaultRegistryPrefix, useDeployImages = false }) {
|
||||||
const namespace = namespaceNameForProfile(profile, deploy);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
|
assert.ok(Array.isArray(migrationSources) && migrationSources.length > 0, "external Postgres migration chain is required");
|
||||||
|
const migrationData = Object.fromEntries(migrationSources.map((migration) => [path.basename(migration.path), migration.sql]));
|
||||||
|
const migrationSha256 = createHash("sha256").update(migrationSources.map((migration) => migration.sql).join("\n")).digest("hex");
|
||||||
|
const migrationRunner = externalPostgresMigrationRunner();
|
||||||
|
const migrationImage = runtimeImageForService({
|
||||||
|
catalog,
|
||||||
|
deploy,
|
||||||
|
serviceId: "hwlab-cloud-api",
|
||||||
|
source,
|
||||||
|
registryPrefix,
|
||||||
|
useDeployImages,
|
||||||
|
digestPin: true,
|
||||||
|
envReuseServiceIds: envReuseServiceIdsForLane(deploy, profile)
|
||||||
|
});
|
||||||
|
const executionSha256 = createHash("sha256").update(`${migrationSha256}\n${migrationImage}\n${migrationRunner}`).digest("hex");
|
||||||
|
const migrationName = `${namespace}-external-postgres-migrate-${executionSha256.slice(0, 12)}`;
|
||||||
const labels = {
|
const labels = {
|
||||||
"app.kubernetes.io/name": config.serviceName,
|
"app.kubernetes.io/name": config.serviceName,
|
||||||
"app.kubernetes.io/part-of": "hwlab",
|
"app.kubernetes.io/part-of": "hwlab",
|
||||||
@@ -756,6 +772,50 @@ function externalPostgresManifest({ profile = "v03", config, source, deploy = nu
|
|||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "List",
|
kind: "List",
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "ConfigMap",
|
||||||
|
metadata: { name: migrationName, namespace, labels, annotations: { ...annotations, "hwlab.pikastech.local/migration-sha256": migrationSha256 } },
|
||||||
|
data: {
|
||||||
|
"run.mjs": migrationRunner,
|
||||||
|
...migrationData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "batch/v1",
|
||||||
|
kind: "Job",
|
||||||
|
metadata: {
|
||||||
|
name: migrationName,
|
||||||
|
namespace,
|
||||||
|
labels,
|
||||||
|
annotations: {
|
||||||
|
...annotations,
|
||||||
|
"argocd.argoproj.io/sync-wave": "-1",
|
||||||
|
"hwlab.pikastech.local/migration-sha256": migrationSha256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
backoffLimit: 2,
|
||||||
|
template: {
|
||||||
|
metadata: { labels: { ...labels, "app.kubernetes.io/name": migrationName } },
|
||||||
|
spec: {
|
||||||
|
restartPolicy: "Never",
|
||||||
|
containers: [{
|
||||||
|
name: "migrate",
|
||||||
|
image: migrationImage,
|
||||||
|
imagePullPolicy: "IfNotPresent",
|
||||||
|
command: ["node", "/opt/hwlab-migrations/run.mjs"],
|
||||||
|
env: [{
|
||||||
|
name: "DATABASE_URL",
|
||||||
|
valueFrom: { secretKeyRef: { name: `hwlab-cloud-api-${profile}-db`, key: "database-url" } }
|
||||||
|
}],
|
||||||
|
volumeMounts: [{ name: "migrations", mountPath: "/opt/hwlab-migrations", readOnly: true }]
|
||||||
|
}],
|
||||||
|
volumes: [{ name: "migrations", configMap: { name: migrationName } }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "Service",
|
kind: "Service",
|
||||||
@@ -774,6 +834,27 @@ function externalPostgresManifest({ profile = "v03", config, source, deploy = nu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function externalPostgresMigrationRunner() {
|
||||||
|
return [
|
||||||
|
'import { readdir, readFile } from "node:fs/promises";',
|
||||||
|
'import { createRequire } from "node:module";',
|
||||||
|
'const require = createRequire("/opt/hwlab-env/package.json");',
|
||||||
|
'const { Pool } = require("/opt/hwlab-env/node_modules/pg");',
|
||||||
|
'const root = "/opt/hwlab-migrations";',
|
||||||
|
'const files = (await readdir(root)).filter((name) => /^\\d+_.*\\.sql$/u.test(name)).sort();',
|
||||||
|
'const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 1 });',
|
||||||
|
'try {',
|
||||||
|
' for (const file of files) {',
|
||||||
|
' await pool.query(await readFile(`${root}/${file}`, "utf8"));',
|
||||||
|
' console.error(JSON.stringify({ event: "hwlab-runtime-migration-applied", file, valuesPrinted: false }));',
|
||||||
|
' }',
|
||||||
|
'} finally {',
|
||||||
|
' await pool.end();',
|
||||||
|
'}',
|
||||||
|
''
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function v02OpenFgaManifest({ profile = "v02", source, deploy = null }) {
|
function v02OpenFgaManifest({ profile = "v02", source, deploy = null }) {
|
||||||
const namespace = namespaceNameForProfile(profile, deploy);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const name = "hwlab-openfga";
|
const name = "hwlab-openfga";
|
||||||
|
|||||||
Reference in New Issue
Block a user