Something went wrong. Try again.
Terraform and Bun scripts to deploy one self-hosted Knot server to Hetzner VM
knot tailscale typescript bun terraform hetzner
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162/** * Image release entrypoint. * It builds the pinned image, pushes it, and prints the digest. * Run this before updating `KNOT_IMAGE_DIGEST` in `.env`. */
import { $ } from "bun";import { join, resolve } from "node:path";
import { envFrom, requireEnvVarsFrom } from "./lib";
const requiredEnvNames = [ "KNOT_IMAGE_REPO", "DOCKERHUB_USERNAME", "DOCKERHUB_PUSH_TOKEN",] as const;
const knotDockerDirectory = resolve(import.meta.dir, "../vendor/knot-docker");
/** Returns the usage text. */function usage(): string { return [ "Usage:", " bun run release-image", "", "Build the vendored knot image, push it, and print the immutable digest to paste into `.env`.", ].join("\n");}
/** Builds a timestamped image tag. */function buildImageTag(imageRepository: string, releaseTime: Date): string { const timestamp = releaseTime .toISOString() .replaceAll(":", "") .replaceAll(".", "");
return `${imageRepository}:${timestamp}`;}
/** Reads the manifest digest. */function readManifestDigest(manifestText: string): string { const manifest = JSON.parse(manifestText) as { digest?: unknown }; if (typeof manifest.digest !== "string" || manifest.digest === "") { throw new Error("docker buildx imagetools inspect did not return a digest"); }
return manifest.digest;}
/** Checks that the vendored build context exists. */async function ensureKnotDockerExists(): Promise<void> { if (!(await Bun.file(join(knotDockerDirectory, "Dockerfile")).exists())) { throw new Error( "knot-docker submodule is missing; run git submodule update --init --recursive", ); }}
/** Logs in to Docker Hub. */async function dockerLogin(username: string, token: string): Promise<void> { const login = Bun.spawn( ["docker", "login", "--username", username, "--password-stdin"], { stdin: "pipe", stdout: "inherit", stderr: "inherit", }, );
login.stdin.write(`${token}\n`); login.stdin.end();
const exitCode = await login.exited; if (exitCode !== 0) { throw new Error(`docker login failed with exit code ${exitCode}`); }}
/** Builds and pushes the image tag. */async function dockerBuildAndPush(imageTag: string): Promise<void> { const build = await $`${[ "docker", "buildx", "build", "--platform", "linux/amd64", "--tag", imageTag, "--push", knotDockerDirectory, ]}`.nothrow(); const exitCode = build.exitCode;
if (exitCode !== 0) { throw new Error(`docker buildx build failed with exit code ${exitCode}`); }}
/** Reads the pushed image digest. */async function inspectManifestDigest(imageTag: string): Promise<string> { const inspect = await $`${[ "docker", "buildx", "imagetools", "inspect", imageTag, "--format", "{{json .Manifest}}", ]}` .quiet() .nothrow(); const exitCode = inspect.exitCode; if (exitCode !== 0) { throw new Error( `docker buildx imagetools inspect failed with exit code ${exitCode}`, ); }
return readManifestDigest(await inspect.text());}
/** Builds, pushes, and prints the release digest. */async function releaseImage(): Promise<void> { requireEnvVarsFrom(Bun.env, requiredEnvNames);
await ensureKnotDockerExists();
const imageRepository = envFrom(Bun.env, "KNOT_IMAGE_REPO"); const imageTag = buildImageTag(imageRepository, new Date()); await dockerLogin( envFrom(Bun.env, "DOCKERHUB_USERNAME"), envFrom(Bun.env, "DOCKERHUB_PUSH_TOKEN"), ); await dockerBuildAndPush(imageTag); const digest = await inspectManifestDigest(imageTag);
console.log(`KNOT_IMAGE_DIGEST=${digest}`); console.log(`KNOT_IMAGE_REF=${imageRepository}@${digest}`);}
/** Runs the CLI entrypoint. */export async function main(args: readonly string[] = Bun.argv.slice(2)): Promise<number> { if (args.includes("--help") || args.includes("-h") || args.includes("help")) { console.log(usage()); return 0; }
await releaseImage(); return 0;}
if (import.meta.main) { try { const exitCode = await main(); process.exit(exitCode); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(message); process.exit(1); }}