feat: implement rollback command for deployments
- Added a new `rollback` command to roll back deployments to their previous release. - Introduced `findRollbackCandidates` and `rollbackDeployment` functions to handle rollback logic. - Updated CLI command definitions to include the new `rollback` command with an alias. - Enhanced deployment conversion to use resolved images for buildable services. - Introduced a new `graph` module to visualize Kubernetes resources and their relationships. - Added tests for the new rollback functionality and graph rendering. - Updated package version to 1.3.0.
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ export const exportCommand = defineCommand({
|
||||
project,
|
||||
await compose(),
|
||||
cwd,
|
||||
config.registry,
|
||||
config,
|
||||
);
|
||||
await config.postRender?.(
|
||||
resources as KuberResource[],
|
||||
|
||||
+3
-1
@@ -7,6 +7,7 @@ import { exportCommand } from "./export";
|
||||
import { logs } from "./logs";
|
||||
import { ps } from "./ps";
|
||||
import { restart } from "./restart";
|
||||
import { rollback } from "./rollback";
|
||||
import { s3 } from "./s3";
|
||||
import { start } from "./start";
|
||||
import { stop } from "./stop";
|
||||
@@ -15,7 +16,7 @@ import { up } from "./up";
|
||||
export const main = defineCommand({
|
||||
meta: {
|
||||
name: "kuber",
|
||||
version: "1.2.2",
|
||||
version: "1.3.0",
|
||||
description: "Docker Compose -> K8s translation layer",
|
||||
},
|
||||
args: {
|
||||
@@ -32,6 +33,7 @@ export const main = defineCommand({
|
||||
logs,
|
||||
ps,
|
||||
restart,
|
||||
rollback,
|
||||
s3,
|
||||
start,
|
||||
stop,
|
||||
|
||||
+9
-9
@@ -1,6 +1,10 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { listDeployments } from "../lib/shared";
|
||||
import { toTable } from "../lib/format";
|
||||
import {
|
||||
buildNamespaceGraphs,
|
||||
fetchNamespaceObjects,
|
||||
renderNamespaceGraphs,
|
||||
} from "../lib/graph";
|
||||
import { getProject } from "../lib/shared";
|
||||
|
||||
export const ps = defineCommand({
|
||||
meta: {
|
||||
@@ -16,14 +20,10 @@ export const ps = defineCommand({
|
||||
},
|
||||
},
|
||||
async run({ args: { all } }) {
|
||||
const objects = await fetchNamespaceObjects(getProject());
|
||||
console.log(
|
||||
toTable(
|
||||
(await listDeployments())
|
||||
.filter((d) => all || d.status?.replicas)
|
||||
.map((d) => ({
|
||||
name: d.metadata?.name,
|
||||
status: `${d.status?.availableReplicas}/${d.status?.replicas}`,
|
||||
})),
|
||||
renderNamespaceGraphs(
|
||||
buildNamespaceGraphs(objects, { includeIdle: all }),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { Listr } from "listr2";
|
||||
import { assertManagedNamespace } from "../lib/apply";
|
||||
import { ctx } from "../lib/context";
|
||||
import { planRollback, rollbackDeployment } from "../lib/rollback";
|
||||
import { waitForDeploymentRollout } from "../lib/shared";
|
||||
|
||||
export const rollback = defineCommand({
|
||||
meta: {
|
||||
name: "rollback",
|
||||
description: "Roll deployments back to the previous release",
|
||||
alias: "fuck",
|
||||
},
|
||||
args: {
|
||||
deployment: {
|
||||
type: "positional",
|
||||
description: "Deployment name to roll back (defaults to all managed)",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const { project, config } = ctx();
|
||||
await assertManagedNamespace(project);
|
||||
|
||||
const names = args.deployment ? [args.deployment] : undefined;
|
||||
const candidates = await planRollback(names);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
if (names) {
|
||||
console.log(`${names[0]} has no previous release to roll back to`);
|
||||
} else {
|
||||
console.log("No deployments have a previous release to roll back to");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await new Listr([
|
||||
{
|
||||
title: "Roll back deployments",
|
||||
task: (taskCtx, task) => {
|
||||
task.output = `${candidates.length} deployment${candidates.length === 1 ? "" : "s"} queued`;
|
||||
return task.newListr(
|
||||
candidates.map((candidate) => ({
|
||||
title: `Deployment ${candidate.name}`,
|
||||
task: () => rollbackDeployment(candidate),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Wait for rollout",
|
||||
task: (taskCtx, task) => {
|
||||
task.output = `${candidates.length} deployment${candidates.length === 1 ? "" : "s"} queued`;
|
||||
return task.newListr(
|
||||
candidates.map((candidate) => ({
|
||||
title: `Deployment ${candidate.name}`,
|
||||
task: () =>
|
||||
waitForDeploymentRollout(candidate.name, config.rolloutTimeoutMs),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
]).run();
|
||||
},
|
||||
});
|
||||
+21
-24
@@ -10,18 +10,18 @@ import {
|
||||
getStaleResources,
|
||||
sortResources,
|
||||
} from "../lib/apply";
|
||||
import { buildServices } from "../lib/build";
|
||||
import { buildServices, resolveBuildImages } from "../lib/build";
|
||||
import { composeToKubernetes, type KubernetesResource } from "../lib/convert";
|
||||
import {
|
||||
getComposePostgresClaims,
|
||||
reconcilePostgresClaims,
|
||||
} from "../lib/database";
|
||||
import { getComposeS3Claims, reconcileS3Claims } from "../lib/storage";
|
||||
import { restartDeployment, waitForDeploymentRollout } from "../lib/shared";
|
||||
import { waitForDeploymentRollout } from "../lib/shared";
|
||||
|
||||
type UpContext = {
|
||||
compose?: ComposeSpecification;
|
||||
changedDeployments?: string[];
|
||||
buildImages?: Record<string, string>;
|
||||
serviceEnv?: Record<string, Record<string, string>>;
|
||||
resources?: KubernetesResource[];
|
||||
staleResources?: KubernetesResource[];
|
||||
@@ -85,11 +85,27 @@ export async function runUp(build: boolean) {
|
||||
},
|
||||
config,
|
||||
);
|
||||
taskCtx.changedDeployments = result.changed;
|
||||
taskCtx.buildImages = result.images;
|
||||
await config.postBuild?.(result, await getHookContext());
|
||||
task.output = `Built ${result.built.length} image${result.built.length === 1 ? "" : "s"}, ${result.changed.length} changed`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Resolve images",
|
||||
enabled: async () =>
|
||||
!build &&
|
||||
Object.values((await compose()).services ?? {}).some(
|
||||
(service) => service.build,
|
||||
),
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.buildImages = await resolveBuildImages(
|
||||
project,
|
||||
taskCtx.compose!,
|
||||
config,
|
||||
);
|
||||
task.output = `${Object.keys(taskCtx.buildImages).length} image${Object.keys(taskCtx.buildImages).length === 1 ? "" : "s"}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Reconcile databases",
|
||||
enabled: async () =>
|
||||
@@ -121,7 +137,7 @@ export async function runUp(build: boolean) {
|
||||
taskCtx.compose!,
|
||||
cwd,
|
||||
taskCtx.serviceEnv,
|
||||
config.registry,
|
||||
taskCtx.buildImages,
|
||||
);
|
||||
await config.postRender?.(
|
||||
taskCtx.resources as KuberResource[],
|
||||
@@ -169,25 +185,6 @@ export async function runUp(build: boolean) {
|
||||
{ rendererOptions: { collapseErrors: false } },
|
||||
).run();
|
||||
|
||||
const changedDeployments = taskCtx.changedDeployments ?? [];
|
||||
if (changedDeployments.length > 0) {
|
||||
await new Listr([
|
||||
{
|
||||
title: "Restart changed deployments",
|
||||
task: (_taskCtx, task) => {
|
||||
task.output = `${changedDeployments.length} deployments queued`;
|
||||
return task.newListr(
|
||||
changedDeployments.map((name) => ({
|
||||
title: `Deployment ${name}`,
|
||||
task: () => restartDeployment(name),
|
||||
})),
|
||||
{ concurrent: false, exitOnError: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
]).run();
|
||||
}
|
||||
|
||||
const deployments = getDeploymentNames(taskCtx.resources!);
|
||||
if (deployments.length > 0) {
|
||||
await new Listr([
|
||||
|
||||
Reference in New Issue
Block a user