fix: restart after reconcile
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from "../lib/apply";
|
||||
import { buildServices } from "../lib/build";
|
||||
import { composeToKubernetes, type KubernetesResource } from "../lib/convert";
|
||||
import { restartDeployment, waitForDeploymentRollout } from "../lib/shared";
|
||||
|
||||
type UpContext = {
|
||||
compose?: ComposeSpecification;
|
||||
@@ -18,6 +19,13 @@ type UpContext = {
|
||||
staleResources?: KubernetesResource[];
|
||||
};
|
||||
|
||||
function getDeploymentNames(resources: KubernetesResource[]): string[] {
|
||||
return resources
|
||||
.filter((resource) => resource.kind === "Deployment")
|
||||
.map((resource) => resource.metadata?.name)
|
||||
.filter((name): name is string => Boolean(name));
|
||||
}
|
||||
|
||||
export async function runUp(build: boolean) {
|
||||
const { project, compose, cwd } = ctx();
|
||||
await assertManagedNamespace(project);
|
||||
@@ -83,6 +91,36 @@ export async function runUp(build: boolean) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Delete stale resources",
|
||||
skip: (taskCtx) =>
|
||||
|
||||
+107
-9
@@ -1,5 +1,5 @@
|
||||
import { basename, isAbsolute, relative, resolve } from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
||||
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { hostname, tmpdir } from "node:os";
|
||||
import type { Writable } from "node:stream";
|
||||
import type { ComposeSpecification, Service } from "../schema/docker.d";
|
||||
@@ -199,6 +199,49 @@ async function getTrackedDiff(repoRoot: string): Promise<string> {
|
||||
return Bun.$.cwd(repoRoot)`git diff --binary HEAD`.text();
|
||||
}
|
||||
|
||||
async function getUntrackedFiles(repoRoot: string): Promise<string[]> {
|
||||
const result = Bun.spawn(
|
||||
["git", "ls-files", "--others", "--exclude-standard"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
);
|
||||
const output = await runWithOutput(result);
|
||||
|
||||
return output.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function getIgnoredDotenvFiles(repoRoot: string): Promise<string[]> {
|
||||
const result = Bun.spawn(
|
||||
[
|
||||
"git",
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--ignored",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
".env*",
|
||||
"**/.env*",
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
);
|
||||
const output = await runWithOutput(result);
|
||||
|
||||
return output.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function getRemoteHead(remoteRepo: string): Promise<string | undefined> {
|
||||
const result = Bun.spawn(
|
||||
[
|
||||
@@ -253,17 +296,72 @@ async function syncRemoteRepo(
|
||||
}
|
||||
|
||||
const diff = await getTrackedDiff(repoRoot);
|
||||
if (diff.trim().length === 0) return;
|
||||
if (diff.trim().length > 0) {
|
||||
await reporter?.progress?.("Applying local git diff on remote builder");
|
||||
const diffPath = `${tempDir}/repo.diff`;
|
||||
const remoteDiff = `${REMOTE_BUILD_ROOT}/${basename(repoRoot)}.diff`;
|
||||
await Bun.write(diffPath, diff);
|
||||
await runWithOutput(scp(diffPath, remoteDiff), reporter);
|
||||
await runWithOutput(ssh(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`git -C ${shellQuote(remoteRepo)} apply --allow-binary-replacement "$PWD/${remoteDiff}"`,
|
||||
].join("; "),
|
||||
), reporter);
|
||||
}
|
||||
|
||||
await reporter?.progress?.("Applying local git diff on remote builder");
|
||||
const diffPath = `${tempDir}/repo.diff`;
|
||||
const remoteDiff = `${REMOTE_BUILD_ROOT}/${basename(repoRoot)}.diff`;
|
||||
await Bun.write(diffPath, diff);
|
||||
await runWithOutput(scp(diffPath, remoteDiff), reporter);
|
||||
const untrackedFiles = await getUntrackedFiles(repoRoot);
|
||||
if (untrackedFiles.length > 0) {
|
||||
await reporter?.progress?.("Syncing untracked files to remote builder");
|
||||
const untrackedDir = `${tempDir}/untracked`;
|
||||
|
||||
for (const file of untrackedFiles) {
|
||||
const source = resolve(repoRoot, file);
|
||||
const target = resolve(untrackedDir, file);
|
||||
await mkdir(dirname(target), { recursive: true });
|
||||
await cp(source, target, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
const untrackedArchive = `${tempDir}/untracked.tar`;
|
||||
const remoteUntrackedArchive = `${REMOTE_BUILD_ROOT}/${basename(repoRoot)}-untracked.tar`;
|
||||
await Bun.$`tar -C ${untrackedDir} -cf ${untrackedArchive} .`;
|
||||
await runWithOutput(scp(untrackedArchive, remoteUntrackedArchive), reporter);
|
||||
await runWithOutput(ssh(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`tar -C ${shellQuote(remoteRepo)} -xf "$PWD/${remoteUntrackedArchive}"`,
|
||||
].join("; "),
|
||||
), reporter);
|
||||
}
|
||||
|
||||
const ignoredDotenvFiles = await getIgnoredDotenvFiles(repoRoot);
|
||||
await runWithOutput(ssh(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`git -C ${shellQuote(remoteRepo)} apply --allow-binary-replacement "$PWD/${remoteDiff}"`,
|
||||
`git -C ${shellQuote(remoteRepo)} clean -fdX -- .env* '**/.env*'`,
|
||||
].join("; "),
|
||||
), reporter);
|
||||
|
||||
if (ignoredDotenvFiles.length === 0) return;
|
||||
|
||||
await reporter?.progress?.("Syncing ignored .env* files to remote builder");
|
||||
const dotenvDir = `${tempDir}/dotenv`;
|
||||
|
||||
for (const file of ignoredDotenvFiles) {
|
||||
const source = resolve(repoRoot, file);
|
||||
const target = resolve(dotenvDir, file);
|
||||
await mkdir(dirname(target), { recursive: true });
|
||||
await cp(source, target, { force: true });
|
||||
}
|
||||
|
||||
const dotenvArchive = `${tempDir}/dotenv.tar`;
|
||||
const remoteDotenvArchive = `${REMOTE_BUILD_ROOT}/${basename(repoRoot)}-dotenv.tar`;
|
||||
await Bun.$`tar -C ${dotenvDir} -cf ${dotenvArchive} .`;
|
||||
await runWithOutput(scp(dotenvArchive, remoteDotenvArchive), reporter);
|
||||
await runWithOutput(ssh(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`tar -C ${shellQuote(remoteRepo)} -xf "$PWD/${remoteDotenvArchive}"`,
|
||||
].join("; "),
|
||||
), reporter);
|
||||
} finally {
|
||||
|
||||
@@ -88,6 +88,40 @@ export async function restartDeployment(name: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function waitForDeploymentRollout(
|
||||
name: string,
|
||||
timeoutMs = 300000,
|
||||
) {
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const deployment = await getDeployment(name);
|
||||
const desiredReplicas = deployment.spec?.replicas ?? 1;
|
||||
const observedGeneration = deployment.status?.observedGeneration ?? 0;
|
||||
const generation = deployment.metadata?.generation ?? 0;
|
||||
const updatedReplicas = deployment.status?.updatedReplicas ?? 0;
|
||||
const availableReplicas = deployment.status?.availableReplicas ?? 0;
|
||||
const unavailableReplicas = deployment.status?.unavailableReplicas ?? 0;
|
||||
|
||||
if (
|
||||
observedGeneration >= generation &&
|
||||
updatedReplicas === desiredReplicas &&
|
||||
availableReplicas === desiredReplicas &&
|
||||
unavailableReplicas === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await delay(2000);
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for deployment ${name} rollout`);
|
||||
}
|
||||
|
||||
export function throwWhen<T, Narrowed extends T>(
|
||||
condition: (arg: T) => arg is Narrowed,
|
||||
): (arg: T) => Exclude<T, Narrowed>;
|
||||
|
||||
Reference in New Issue
Block a user