Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .dev/cdk8s-import-one.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Worker: import a single spec. Spawned in parallel by .dev/cdk8s-import.ts so
* that each import's download + codegen runs on its own process (real
* multi-core parallelism; in-process Promise.all serializes sync codegen).
*
* Usage: bun .dev/cdk8s-import-one.ts <spec> <outdir>
*/
import { createRequire } from "node:module";
import { patchCdk8sDownload } from "./lib/fetch-download";

const require = createRequire(import.meta.url);
patchCdk8sDownload(require);

const { matchImporter } = require("../node_modules/cdk8s-cli/lib/import/dispatch");

const spec = process.argv[2];
const outdir = process.argv[3] ?? "imports";

const importSpec = { source: spec, moduleNamePrefix: undefined as string | undefined };
const importer = await matchImporter(importSpec, { exclude: [] });
if (!importer) throw new Error(`unable to determine import type for "${spec}"`);

process.stderr.write(`Importing ${spec}...\n`);
await importer.import({
moduleNamePrefix: importSpec.moduleNamePrefix,
outdir,
targetLanguage: "typescript",
classNamePrefix: undefined,
});
process.exit(0);
75 changes: 75 additions & 0 deletions .dev/cdk8s-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Fast cdk8s import: download + generate .ts, in parallel.
*
* Runs each import from cdk8s.yaml in its own worker process (real multi-core
* parallelism) and patches cdk8s-cli's buggy download() with `fetch`. An
* output-level cache makes warm runs (unchanged cdk8s.yaml) O(1).
*
* The cache lives in CDK8S_IMPORT_CACHE (default ~/.cache/cdk8s-imports). The
* ArgoCD CMP points it at a persistent volume so `cdk8s:import` is a no-op
* across syncs as long as cdk8s.yaml is unchanged.
*
* Usage: bun .dev/cdk8s-import.ts [outdir]
*/
import { $ } from "bun";
import { createHash } from "node:crypto";
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { parse as parseYaml } from "yaml";

const CACHE_DIR = process.env.CDK8S_IMPORT_CACHE ?? join(homedir(), ".cache", "cdk8s-imports");
const CONCURRENCY = Number(process.env.CDK8S_IMPORT_PARALLELISM ?? 16);

const config = parseYaml(readFileSync("cdk8s.yaml", "utf-8")) as { imports?: string[] };
const imports = config.imports ?? [];
const OUTDIR = process.argv[2] ?? "imports";

// --- output cache: warm runs just link the previous result ---
const outputKey = createHash("sha256").update(readFileSync("cdk8s.yaml")).digest("hex");
const cachedImports = join(CACHE_DIR, "out", outputKey, "imports");
if (existsSync(cachedImports)) {
rmSync(OUTDIR, { recursive: true, force: true });
symlinkSync(cachedImports, OUTDIR, "dir");
console.error(`cached (${outputKey.slice(0, 8)}): linked -> ${OUTDIR}`);
process.exit(0);
}

// --- worker pool: one process per import, bounded concurrency ---
rmSync(OUTDIR, { recursive: true, force: true });
const started = Date.now();
const queue = [...imports];
let running = 0;
let failed = 0;

async function runWorker(spec: string): Promise<void> {
const result = await $`bun .dev/cdk8s-import-one.ts ${spec} ${OUTDIR}`.nothrow();
if (result.exitCode !== 0) failed++;
}

async function pump(): Promise<void> {
while (queue.length > 0 && running < CONCURRENCY) {
running++;
const spec = queue.shift()!;
void runWorker(spec).then(() => {
running--;
void pump();
});
}
}

await pump();

// wait for any stragglers
while (running > 0) {
await new Promise((r) => setTimeout(r, 100));
}

const wall = ((Date.now() - started) / 1000).toFixed(1);
console.error(`${imports.length - failed}/${imports.length} imports OK in ${wall}s`);
if (failed) process.exit(1);

// --- cache the generated output ---
mkdirSync(cachedImports, { recursive: true });
cpSync(OUTDIR, cachedImports, { recursive: true });
console.error(`cached -> ${cachedImports}`);
43 changes: 43 additions & 0 deletions .dev/cdk8s-synth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Render a single cdk8s Application: ./APP_NAME/app.ts.
*
* `app.ts` default-exports a crafted `App` (see PLAN.md); this script just
* imports it and calls `.synth()`. The output directory is passed through the
* `CDK8S_OUTDIR` env var, which `new App()` picks up, so app.ts stays free of
* build plumbing.
*
* Usage: bun .dev/cdk8s-synth.ts APP_NAME [--output DIR]
*
* ArgoCD runs this per cdk8s Application via the Config Management Plugin
* sidecar; `dist/APP_NAME/*.k8s.yaml` is what gets applied.
*/
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";

const args = process.argv.slice(2);
const outputFlag = args.indexOf("--output");
const positional = args.filter((_, i) => (outputFlag < 0 ? true : i !== outputFlag && i !== outputFlag + 1));
const appName = positional[0];

if (!appName) {
console.error("usage: bun .dev/cdk8s-synth.ts APP_NAME [--output DIR]");
process.exit(1);
}

const outDir = outputFlag >= 0 ? args[outputFlag + 1] : join("dist", appName);
process.env.CDK8S_OUTDIR = outDir;

const appPath = resolve(import.meta.dir, "..", appName, "app.ts");
if (!existsSync(appPath)) {
console.error(`✗ ${appPath} does not exist`);
process.exit(1);
}

const module = (await import(appPath)) as { default?: { synth(): void } };
if (!module.default || typeof module.default.synth !== "function") {
console.error(`✗ ${appPath} must default-export a cdk8s App`);
process.exit(1);
}

module.default.synth();
console.error(`✓ synthesized ${appName} -> ${outDir}`);
21 changes: 21 additions & 0 deletions .dev/dev-storage-classes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Apply these when running the cluster in a dev environment
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: node-local-zfs
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: memory-hdd
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: memory-ssd
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
61 changes: 61 additions & 0 deletions .dev/git-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
apiVersion: v1
kind: Service
metadata:
name: git-server
namespace: argocd
spec:
selector:
app: git-server
ports:
- port: 9418
targetPort: 9418
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: git-server
namespace: argocd
spec:
replicas: 1
selector:
matchLabels:
app: git-server
template:
metadata:
labels:
app: git-server
spec:
containers:
- name: git
image: nixery.dev/shell/git
command: [ bash, -c ]
args:
- |
set -e
if [ ! -d /srv/k8s.git ]; then
git init --bare /srv/k8s.git
fi
git -C /srv/k8s.git config daemon.receivepack true
exec git daemon --reuseaddr --export-all --enable=receive-pack --base-path=/srv --listen=0.0.0.0 --port=9418
ports:
- containerPort: 9418
readinessProbe:
tcpSocket:
port: 9418
initialDelaySeconds: 2
periodSeconds: 2
resources:
requests:
cpu: 10m
memory: 32Mi
ephemeral-storage: 0
limits:
cpu: 200m
memory: 128Mi
ephemeral-storage: 200Mi
volumeMounts:
- name: repos
mountPath: /srv
volumes:
- name: repos
emptyDir: { }
70 changes: 70 additions & 0 deletions .dev/is-local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Are we rendering for the local test cluster (or a developer's machine) rather
* than production?
*
* Used by `application-set/app.ts` to decide where the generated Applications
* point. The ArgoCD CMP runs `app.ts` on every render, both in prod and in the
* local cluster:
*
* - prod -> https://github.com/kir-dev/k8s
* - local -> git://git-server.argocd.svc.cluster.local:9418/k8s.git
* (in-cluster git daemon, see .dev/local-cluster.ts)
*
* Detection, in order:
* 1. `K8S_LOCAL` env (set by `local-cluster:*` for host-side synthesis).
* 2. `ARGOCD_APP_SOURCE_REPO_URL` (ArgoCD passes the Application's source URL
* to CMP plugins; a `git://` URL means the in-cluster git server).
* 3. That git server actually being reachable, which is true in the local
* cluster and false in prod even when the Application source is stale.
*/
import { $ } from "bun";
import { resolve } from "node:path";

const ROOT = resolve(import.meta.dir, "..");
const LOCAL_GIT_HOST = "git-server.argocd.svc.cluster.local";
const LOCAL_GIT_PORT = 9418;

export async function isLocal(): Promise<boolean> {
const env = process.env.K8S_LOCAL;
if (env !== undefined) return env !== "" && env !== "0" && env !== "false";
if ((process.env.ARGOCD_APP_SOURCE_REPO_URL ?? "").startsWith("git://")) return true;
if (await localGitServerReachable()) return true;
return (await repoUrl()).startsWith("git://");
}

async function localGitServerReachable(): Promise<boolean> {
if (process.platform !== "linux") return false;
const probe = `echo > /dev/tcp/${LOCAL_GIT_HOST}/${LOCAL_GIT_PORT}`;
const result = await $`timeout 3 bash -c ${probe}`.quiet().nothrow();
return result.exitCode === 0;
}

/** The URL of the repository `app.ts` is currently being rendered from. */
export async function repoUrl(): Promise<string> {
const result = await $`git -C ${ROOT} remote get-url origin`.quiet().nothrow();
if (result.exitCode !== 0) return "";
return result.text().trim();
}

/** Where Application sync from in production. */
export const PROD_REPO_URL = "https://github.com/kir-dev/k8s";

/** The branch `local-cluster:sync` pushes to. */
export const LOCAL_REVISION = "argocd-head";

/**
* The repository the generated Applications should point at.
*
* In prod that's always kir-dev/k8s. Locally it's the git daemon serving the
* developer's working copy; `local-cluster:up` passes K8S_LOCAL_REPO_URL when
* bootstrapping from a checkout whose origin is still the normal remote.
*/
export async function sourceRepoUrl(): Promise<string> {
if (!(await isLocal())) return PROD_REPO_URL;
return process.env.K8S_LOCAL_REPO_URL || repoUrl();
}

/** The revision the generated Applications should track. */
export async function sourceRevision(): Promise<string> {
return (await isLocal()) ? LOCAL_REVISION : "HEAD";
}
41 changes: 41 additions & 0 deletions .dev/lib/fetch-download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Shared: patch cdk8s-cli's `download()` to use `fetch` + a content cache.
*
* cdk8s-cli's own `download()` (src/util.ts) never drains 301/302 redirect
* bodies, so a redirecting URL holds a socket open and the process hangs ~30s.
* `fetch` follows and drains redirects correctly. The cache avoids re-fetching
* unchanged sources (k8s schema + CRD URLs).
*/
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const CACHE_DIR = process.env.CDK8S_IMPORT_CACHE ?? join(homedir(), ".cache", "cdk8s-imports");

/**
* Must be called with the same `require` used to load cdk8s-cli so we mutate
* the exact module instance its crd.js / k8s.js call into.
*/
export function patchCdk8sDownload(require: NodeJS.Require): void {
// Resolved relative to the `require` passed in (createRequire lives in
// .dev/cdk8s-import-one.ts), so `../node_modules` is the repo root.
const util = require("../node_modules/cdk8s-cli/lib/util");
const original = util.download;
util.download = async (url: string): Promise<string> => {
if (!/^https?:/i.test(url)) {
return original(url); // file: / relative paths -> passthrough
}
const key = createHash("sha256").update(url).digest("hex");
const cacheFile = join(CACHE_DIR, key);
if (existsSync(cacheFile)) {
return readFileSync(cacheFile, "utf-8");
}
const res = await fetch(url, { redirect: "follow" });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`);
const text = await res.text();
mkdirSync(CACHE_DIR, { recursive: true });
writeFileSync(cacheFile, text);
return text;
};
}
Loading