feat: add database reconciler
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { ctx } from "../lib/context";
|
||||
import {
|
||||
DATABASE_HOST,
|
||||
DATABASE_PORT,
|
||||
buildDatabaseUrl,
|
||||
getComposePostgresClaims,
|
||||
getRoleCredentials,
|
||||
} from "../lib/database";
|
||||
import { toTable } from "../lib/format";
|
||||
|
||||
const list = defineCommand({
|
||||
meta: {
|
||||
name: "ls",
|
||||
description: "List managed postgres claims",
|
||||
},
|
||||
async run() {
|
||||
const claims = getComposePostgresClaims(await ctx().compose());
|
||||
if (claims.length === 0) {
|
||||
console.log("No managed postgres volumes declared");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
toTable(
|
||||
claims.map((claim) => ({
|
||||
service: claim.service,
|
||||
username: claim.username,
|
||||
database: claim.database,
|
||||
secret: claim.secretName,
|
||||
})),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const creds = defineCommand({
|
||||
meta: {
|
||||
name: "creds",
|
||||
description: "Print postgres credentials for a service",
|
||||
},
|
||||
async run({ args }) {
|
||||
const service = args._[0];
|
||||
if (!service) throw new Error("Service name is required");
|
||||
|
||||
const claim = getComposePostgresClaims(await ctx().compose()).find(
|
||||
(entry) => entry.service === service,
|
||||
);
|
||||
if (!claim) {
|
||||
throw new Error(
|
||||
`Service ${service} does not declare a managed postgres volume.`,
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await getRoleCredentials(claim.username);
|
||||
const url = buildDatabaseUrl(claim, credentials);
|
||||
|
||||
console.log(
|
||||
toTable([
|
||||
{
|
||||
service: claim.service,
|
||||
username: credentials.username,
|
||||
database: claim.database,
|
||||
host: DATABASE_HOST,
|
||||
port: DATABASE_PORT,
|
||||
secret: claim.secretName,
|
||||
},
|
||||
]),
|
||||
);
|
||||
console.log(`DATABASE_URL=${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
export const db = defineCommand({
|
||||
meta: {
|
||||
name: "db",
|
||||
description: "Inspect managed postgres databases",
|
||||
},
|
||||
subCommands: {
|
||||
creds,
|
||||
ls: list,
|
||||
},
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
listManagedResources,
|
||||
sortResources,
|
||||
} from "../lib/apply";
|
||||
import { listManagedDatabaseResources } from "../lib/database";
|
||||
|
||||
export const down = defineCommand({
|
||||
meta: {
|
||||
@@ -30,6 +31,10 @@ export const down = defineCommand({
|
||||
resource.kind !== "PersistentVolumeClaim"),
|
||||
);
|
||||
|
||||
if (args.full) {
|
||||
resources.push(...(await listManagedDatabaseResources(project)));
|
||||
}
|
||||
|
||||
if (args.full) {
|
||||
resources.push({
|
||||
apiVersion: "v1",
|
||||
|
||||
+127
-98
@@ -11,10 +11,15 @@ import {
|
||||
} from "../lib/apply";
|
||||
import { buildServices } from "../lib/build";
|
||||
import { composeToKubernetes, type KubernetesResource } from "../lib/convert";
|
||||
import {
|
||||
getComposePostgresClaims,
|
||||
reconcilePostgresClaims,
|
||||
} from "../lib/database";
|
||||
import { restartDeployment, waitForDeploymentRollout } from "../lib/shared";
|
||||
|
||||
type UpContext = {
|
||||
compose?: ComposeSpecification;
|
||||
serviceEnv?: Record<string, Record<string, string>>;
|
||||
resources?: KubernetesResource[];
|
||||
staleResources?: KubernetesResource[];
|
||||
};
|
||||
@@ -30,117 +35,141 @@ export async function runUp(build: boolean) {
|
||||
const { project, compose, cwd } = ctx();
|
||||
await assertManagedNamespace(project);
|
||||
|
||||
await new Listr<UpContext>([
|
||||
{
|
||||
title: "Read compose",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.compose = await compose();
|
||||
task.output = `${Object.keys(taskCtx.compose.services ?? {}).length} services`;
|
||||
await new Listr<UpContext>(
|
||||
[
|
||||
{
|
||||
title: "Read compose",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.compose = await compose();
|
||||
task.output = `${Object.keys(taskCtx.compose.services ?? {}).length} services`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Build images",
|
||||
rendererOptions: {
|
||||
outputBar: 10,
|
||||
persistentOutput: true,
|
||||
{
|
||||
title: "Build images",
|
||||
rendererOptions: {
|
||||
outputBar: 10,
|
||||
persistentOutput: true,
|
||||
},
|
||||
enabled: () => build,
|
||||
skip: (taskCtx) =>
|
||||
Object.values(taskCtx.compose?.services ?? {}).some(
|
||||
(service) => service.build,
|
||||
)
|
||||
? false
|
||||
: "No buildable services",
|
||||
task: async (taskCtx, task) => {
|
||||
const built = await buildServices(project, taskCtx.compose!, cwd, {
|
||||
progress: (message) => {
|
||||
task.output = message;
|
||||
},
|
||||
stream: task.stdout(),
|
||||
});
|
||||
task.output = `Built ${built} image${built === 1 ? "" : "s"}`;
|
||||
},
|
||||
},
|
||||
enabled: () => build,
|
||||
skip: (taskCtx) =>
|
||||
Object.values(taskCtx.compose?.services ?? {}).some((service) => service.build)
|
||||
? false
|
||||
: "No buildable services",
|
||||
task: async (taskCtx, task) => {
|
||||
const built = await buildServices(project, taskCtx.compose!, cwd, {
|
||||
progress: (message) => {
|
||||
task.output = message;
|
||||
},
|
||||
stream: task.stdout(),
|
||||
});
|
||||
task.output = `Built ${built} image${built === 1 ? "" : "s"}`;
|
||||
{
|
||||
title: "Reconcile databases",
|
||||
skip: (taskCtx) =>
|
||||
getComposePostgresClaims(taskCtx.compose!).length > 0
|
||||
? false
|
||||
: "No managed postgres volumes",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.serviceEnv = await reconcilePostgresClaims(
|
||||
project,
|
||||
taskCtx.compose!,
|
||||
);
|
||||
task.output = `${Object.keys(taskCtx.serviceEnv).length} service${Object.keys(taskCtx.serviceEnv).length === 1 ? "" : "s"}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Render manifests",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.resources = await composeToKubernetes(project, taskCtx.compose!, cwd);
|
||||
task.output = `${taskCtx.resources.length} resources`;
|
||||
{
|
||||
title: "Render manifests",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.resources = await composeToKubernetes(
|
||||
project,
|
||||
taskCtx.compose!,
|
||||
cwd,
|
||||
taskCtx.serviceEnv,
|
||||
);
|
||||
task.output = `${taskCtx.resources.length} resources`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Plan reconciliation",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.staleResources = (await getStaleResources(
|
||||
project,
|
||||
taskCtx.resources!,
|
||||
)) as KubernetesResource[];
|
||||
task.output = `${taskCtx.staleResources.length} stale resource${taskCtx.staleResources.length === 1 ? "" : "s"}`;
|
||||
{
|
||||
title: "Plan reconciliation",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.staleResources = (await getStaleResources(
|
||||
project,
|
||||
taskCtx.resources!,
|
||||
)) as KubernetesResource[];
|
||||
task.output = `${taskCtx.staleResources.length} stale resource${taskCtx.staleResources.length === 1 ? "" : "s"}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Apply resources",
|
||||
task: (taskCtx, task) => {
|
||||
const resources = sortResources(taskCtx.resources!);
|
||||
{
|
||||
title: "Apply resources",
|
||||
task: (taskCtx, task) => {
|
||||
const resources = sortResources(taskCtx.resources!);
|
||||
|
||||
task.output = `${resources.length} resources queued`;
|
||||
return task.newListr(
|
||||
resources.map((resource) => ({
|
||||
title: `${resource.kind} ${resource.metadata?.name}`,
|
||||
task: () => applyResource(resource),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
task.output = `${resources.length} resources queued`;
|
||||
return task.newListr(
|
||||
resources.map((resource) => ({
|
||||
title: `${resource.kind} ${resource.metadata?.name}`,
|
||||
task: () => applyResource(resource),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Restart deployments",
|
||||
task: (taskCtx, task) => {
|
||||
const deployments = getDeploymentNames(taskCtx.resources!);
|
||||
{
|
||||
title: "Restart deployments",
|
||||
task: (taskCtx, task) => {
|
||||
const deployments = getDeploymentNames(taskCtx.resources!);
|
||||
|
||||
task.output = `${deployments.length} deployments queued`;
|
||||
return task.newListr(
|
||||
deployments.map((name) => ({
|
||||
title: `Deployment ${name}`,
|
||||
task: () => restartDeployment(name),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
task.output = `${deployments.length} deployments queued`;
|
||||
return task.newListr(
|
||||
deployments.map((name) => ({
|
||||
title: `Deployment ${name}`,
|
||||
task: () => restartDeployment(name),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Wait for rollout",
|
||||
task: (taskCtx, task) => {
|
||||
const deployments = getDeploymentNames(taskCtx.resources!);
|
||||
{
|
||||
title: "Wait for rollout",
|
||||
task: (taskCtx, task) => {
|
||||
const deployments = getDeploymentNames(taskCtx.resources!);
|
||||
|
||||
task.output = `${deployments.length} deployments queued`;
|
||||
return task.newListr(
|
||||
deployments.map((name) => ({
|
||||
title: `Deployment ${name}`,
|
||||
task: () => waitForDeploymentRollout(name),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
task.output = `${deployments.length} deployments queued`;
|
||||
return task.newListr(
|
||||
deployments.map((name) => ({
|
||||
title: `Deployment ${name}`,
|
||||
task: () => waitForDeploymentRollout(name),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Delete stale resources",
|
||||
skip: (taskCtx) =>
|
||||
taskCtx.staleResources && taskCtx.staleResources.length > 0
|
||||
? false
|
||||
: "No stale resources",
|
||||
task: (taskCtx, task) => {
|
||||
const resources = sortResources(taskCtx.staleResources!).reverse();
|
||||
{
|
||||
title: "Delete stale resources",
|
||||
skip: (taskCtx) =>
|
||||
taskCtx.staleResources && taskCtx.staleResources.length > 0
|
||||
? false
|
||||
: "No stale resources",
|
||||
task: (taskCtx, task) => {
|
||||
const resources = sortResources(taskCtx.staleResources!).reverse();
|
||||
|
||||
task.output = `${resources.length} resources queued`;
|
||||
return task.newListr(
|
||||
resources.map((resource) => ({
|
||||
title: `${resource.kind} ${resource.metadata?.name}`,
|
||||
task: () => deleteResource(resource),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
task.output = `${resources.length} resources queued`;
|
||||
return task.newListr(
|
||||
resources.map((resource) => ({
|
||||
title: `${resource.kind} ${resource.metadata?.name}`,
|
||||
task: () => deleteResource(resource),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
], { rendererOptions: { collapseErrors: false } }).run();
|
||||
],
|
||||
{ rendererOptions: { collapseErrors: false } },
|
||||
).run();
|
||||
}
|
||||
|
||||
export const up = defineCommand({
|
||||
|
||||
Reference in New Issue
Block a user