diff --git a/.dev/bootstrap-prod.ts b/.dev/bootstrap-prod.ts new file mode 100644 index 0000000..0d592dd --- /dev/null +++ b/.dev/bootstrap-prod.ts @@ -0,0 +1,10 @@ +// Set KIRDEV_ENVIRONMENT=Production and KIRDEV_K8S_REPO before running. + +import { $ } from "bun"; +import { kubectlApplyCdk8sApp } from "./kubectl-utils.ts"; +import applicationSet from "../application-set/app.ts"; + +await $`bun run cdk8s:import`; +await $`vcluster create vc2 -n vc2 -f .vclusters/vc2/vcluster.yaml`; +await $`kubectl kustomize --enable-helm argocd/ | kubectl apply -f -`; +await kubectlApplyCdk8sApp(applicationSet); diff --git a/.dev/cdk8s-download-patch.ts b/.dev/cdk8s-download-patch.ts new file mode 100644 index 0000000..43f5ba8 --- /dev/null +++ b/.dev/cdk8s-download-patch.ts @@ -0,0 +1,42 @@ +/** + * Patch cdk8s download() https://github.com/cdk8s-team/cdk8s-cli/blob/7d810de7cbd34d1729e35192c05a3bf00ac33dd7/src/util.ts#L164 + * to fix redirect handling and add caching. + */ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; + +const cacheDir = 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 => { + // Let the original function handle non-https requests + if (!/^https?:/i.test(url)) { + return original(url); + } + + // Check the cache + const cacheKey = createHash("sha256").update(url).digest("hex"); + const cacheFilePath = join(cacheDir, cacheKey); + if (existsSync(cacheFilePath)) { + return readFileSync(cacheFilePath, "utf-8"); + } + + // No cache, request and store in cache + const res = await fetch(url, { redirect: "follow" }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`); + const text = await res.text(); + mkdirSync(cacheDir, { recursive: true }); + writeFileSync(cacheFilePath, text); + return text; + }; +} diff --git a/.dev/cdk8s-import-one.ts b/.dev/cdk8s-import-one.ts new file mode 100644 index 0000000..68bd96e --- /dev/null +++ b/.dev/cdk8s-import-one.ts @@ -0,0 +1,25 @@ +/** + * Usage: bun .dev/cdk8s-import-one.ts + */ +import { createRequire } from "node:module"; +import { patchCdk8sDownload } from "./cdk8s-download-patch"; + +const require = createRequire(import.meta.url); +patchCdk8sDownload(require); + +const { matchImporter } = require("../node_modules/cdk8s-cli/lib/import/dispatch"); + +const spec = process.argv[2]; + +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, + targetLanguage: "typescript", + outdir: "imports", + classNamePrefix: undefined, +}); +process.exit(0); diff --git a/.dev/cdk8s-import.ts b/.dev/cdk8s-import.ts new file mode 100644 index 0000000..7b00862 --- /dev/null +++ b/.dev/cdk8s-import.ts @@ -0,0 +1,71 @@ +/** + * Default `cdk8s import` is slow and has a bug when receiving and redirect that makes it even slower. + * This script runs each import in parallel and patches cdk8s's download(). + */ +import { $ } from "bun"; +import { createHash } from "node:crypto"; +import { cpSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { parse as parseYaml } from "yaml"; + +/** + * Recreate `srcDir` at `destDir`, hardlinking files where the filesystem allows + * it (same inode, so no extra disk writes) and copying otherwise. + * + * Hardlinks are transparent to module resolution — unlike symlinks, the file + * keeps the path it was opened through, so the generated imports resolve + * `cdk8s`/`constructs` from the project's `node_modules`. + */ +function linkOrCopyTree(srcDir: string, destDir: string): void { + mkdirSync(destDir, { recursive: true }); + for (const entry of readdirSync(srcDir, { withFileTypes: true })) { + const src = join(srcDir, entry.name); + const dest = join(destDir, entry.name); + if (entry.isDirectory()) { + linkOrCopyTree(src, dest); + } else { + try { + linkSync(src, dest); + } catch { + cpSync(src, dest); + } + } + } +} + +const cacheDir = process.env.CDK8S_IMPORT_CACHE ?? join(homedir(), ".cache", "cdk8s-imports"); + +const cdk8sConfig = parseYaml(readFileSync("cdk8s.yaml", "utf-8")) as { imports: string[] }; +const cdk8sImports = cdk8sConfig.imports; +const importsDir = "imports"; + +// Reuse cached imports dir if cdk8s.yaml hasn't changed +const cdk8sConfigHash = createHash("sha256").update(readFileSync("cdk8s.yaml")).digest("hex"); +const cachedImportsDir = join(cacheDir, "out", cdk8sConfigHash, "imports"); +if (existsSync(cachedImportsDir)) { + rmSync(importsDir, { recursive: true, force: true }); + // Hardlink (not symlink) into the project: symlinks make Bun resolve the + // generated files' `cdk8s`/`constructs` imports against the cache path, + // where there is no `node_modules`, so it silently auto-installs a wrong + // cdk8s version. Hardlinks keep the project path, and share inodes so the + // import cache costs no extra disk writes. + linkOrCopyTree(cachedImportsDir, importsDir); + process.exit(0); +} + +// cdk8s.yaml changed, create new imports dir +rmSync(importsDir, { recursive: true, force: true }); +const started = Date.now(); + +const shellOutputs = await Promise.all(cdk8sImports.map((spec) => $`bun .dev/cdk8s-import-one.ts ${spec}`.nothrow())); + +const failed = shellOutputs.filter((result) => result.exitCode !== 0).length; +const wall = ((Date.now() - started) / 1000).toFixed(1); +console.error(`${cdk8sImports.length - failed}/${cdk8sImports.length} imports OK in ${wall}s`); +if (failed) process.exit(1); + +// Cache +mkdirSync(cachedImportsDir, { recursive: true }); +cpSync(importsDir, cachedImportsDir, { recursive: true }); +console.error(`cached -> ${cachedImportsDir}`); diff --git a/.dev/cdk8s-synth.ts b/.dev/cdk8s-synth.ts new file mode 100644 index 0000000..525aaf8 --- /dev/null +++ b/.dev/cdk8s-synth.ts @@ -0,0 +1,39 @@ +/** + * 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 + * + * 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 appName = process.argv[2]; +if (!appName) { + console.error("usage: bun run cdk8s:synth APP_NAME"); + process.exit(1); +} + +const outDir = 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}`); diff --git a/.dev/cdk8s-utils.ts b/.dev/cdk8s-utils.ts new file mode 100644 index 0000000..d323e14 --- /dev/null +++ b/.dev/cdk8s-utils.ts @@ -0,0 +1,22 @@ +import { App, Chart } from "cdk8s"; +import { Construct } from "constructs"; +import { KubeNamespace } from "../imports/k8s"; + +interface SingletonAppOptions { + namespace: string; + createNamespace?: boolean; +} + +// Helper for cdk8s apps that can only have a single instance in the cluster. +// +// Always specify resource names so that cdk8s doesn't generate it. +export function singletonApp(options: SingletonAppOptions, factory: (scope: Construct) => void): App { + const app = new App(); + const chart = new Chart(app, "chart", { namespace: options.namespace }); + if (options.createNamespace) { + const namespaceChart = new Chart(app, "namespace"); + new KubeNamespace(namespaceChart, "namespace", { metadata: { name: options.namespace } }); + } + factory(chart); + return app; +} diff --git a/.dev/dev-storage-classes.yaml b/.dev/dev-storage-classes.yaml new file mode 100644 index 0000000..b6765d5 --- /dev/null +++ b/.dev/dev-storage-classes.yaml @@ -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 \ No newline at end of file diff --git a/.dev/environment.ts b/.dev/environment.ts new file mode 100644 index 0000000..8503489 --- /dev/null +++ b/.dev/environment.ts @@ -0,0 +1,16 @@ +export const environment: "Development" | "Production" = ((e) => { + if (e == "Development" || e == "Production") return e; + if (!e) return "Development"; + throw new Error("Invalid KIRDEV_ENVIRONMENT."); +})(process.env.KIRDEV_ENVIRONMENT); + +export const k8sRepoUrl = + process.env.KIRDEV_K8S_REPO_URL ?? + (() => { + if (environment == "Development") { + return `git://git-server.argocd.svc.cluster.local:9418/k8s.git`; + } + throw new Error("KIRDEV_K8S_REPO_URL not set."); + })(); + +export const k8sRepoRevision = process.env.KIRDEV_K8S_REPO_REVISION; diff --git a/.dev/git-server.yaml b/.dev/git-server.yaml new file mode 100644 index 0000000..51062d9 --- /dev/null +++ b/.dev/git-server.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: git-server + namespace: argocd +spec: + selector: + app: git-server + ports: + - port: 9418 + targetPort: 9418 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: git-server-repos + namespace: argocd +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: git-server + namespace: argocd +spec: + replicas: 1 + strategy: + type: Recreate + 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 --initial-branch=main /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: + exec: + command: + - git + - ls-remote + - git://127.0.0.1:9418/k8s.git + initialDelaySeconds: 2 + periodSeconds: 10 + 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 + persistentVolumeClaim: + claimName: git-server-repos diff --git a/.dev/kubectl-utils.ts b/.dev/kubectl-utils.ts new file mode 100644 index 0000000..5487356 --- /dev/null +++ b/.dev/kubectl-utils.ts @@ -0,0 +1,10 @@ +import { $ } from "bun"; +import { App } from "cdk8s"; + +export async function kubectlApplyYaml(yaml: string) { + await $`kubectl apply -f - < ${Buffer.from(yaml)}`; +} + +export async function kubectlApplyCdk8sApp(app: App) { + await kubectlApplyYaml(app.synthYaml()); +} diff --git a/.dev/local-cluster.ts b/.dev/local-cluster.ts new file mode 100644 index 0000000..10dd90a --- /dev/null +++ b/.dev/local-cluster.ts @@ -0,0 +1,179 @@ +import { $ } from "bun"; +import { join, resolve } from "node:path"; +import { check, confirm, have, ok, text } from "./shell-utils"; +import applicationSet from "../application-set/app.ts"; +import { kubectlApplyCdk8sApp } from "./kubectl-utils.ts"; + +const ROOT = resolve(import.meta.dir, ".."); +const K3D_CLUSTER_NAME = "kirdev-local-cluster"; +const K3S_IMAGE = "rancher/k3s:v1.35.0-k3s1"; +const GIT_SERVER_TARGET_BRANCH = "main"; +const GIT_SERVER_NAMESPACE = "argocd"; +const GIT_SERVER_SERVICE = "git-server"; +const GIT_SERVER_PROXY_LOCAL_PORT = 19418; + +const VCLUSTERS = [ + { name: "vc1", namespace: "vc1", file: join(ROOT, ".vclusters/vc1/vcluster.yaml") }, + { name: "vc2", namespace: "vc2", file: join(ROOT, ".vclusters/vc2/vcluster.yaml") }, +]; + +async function isDirty(): Promise { + return (await text($`git -C ${ROOT} status --porcelain`)) !== ""; +} + +// --- cluster -------------------------------------------------------------- + +async function k3dClusterExists(): Promise { + const r = await $`k3d cluster list -o json`.quiet().nothrow(); + if (r.exitCode !== 0) return false; + try { + const clusters = JSON.parse(r.text()) as { name: string }[]; + return clusters.some((c) => c.name === K3D_CLUSTER_NAME); + } catch { + return false; + } +} + +async function vclusterExists(name: string): Promise { + const r = await $`vcluster list -o json`.quiet().nothrow(); + if (r.exitCode !== 0) return false; + try { + const vcs = JSON.parse(r.text()) as { name: string }[]; + return vcs.some((v) => v.name === name); + } catch { + return false; + } +} + +/** Create the vCluster on the current context, or connect to it if it exists. */ +async function ensureVcluster(vcluster: (typeof VCLUSTERS)[number]): Promise { + if (!(await vclusterExists(vcluster.name))) { + await check($`vcluster create ${vcluster.name} -n ${vcluster.namespace} -f ${vcluster.file} < /dev/null`); // < /dev/null stops dumb questions + return; + } + console.log(`✓ vcluster ${vcluster.name} exists`); + if (!(await currentContext()).includes(`vcluster_${vcluster.name}_`)) { + await check($`vcluster connect ${vcluster.name} -n ${vcluster.namespace}`); + } +} + +async function currentContext(): Promise { + return text($`kubectl config current-context`); +} + +async function assertLocalContext(): Promise { + const ctx = await currentContext(); + if (!ctx.includes("vcluster") || !ctx.includes(K3D_CLUSTER_NAME)) { + console.error(`✗ current kubectl context "${ctx}" does not look like the local cluster (${K3D_CLUSTER_NAME}).`); + console.error(" Run `bun run local-cluster:up`."); + process.exit(1); + } +} + +async function installGitServer(): Promise { + await check($`kubectl apply -f ${join(ROOT, ".dev/git-server.yaml")}`); + await check($`kubectl -n ${GIT_SERVER_NAMESPACE} rollout status deployment/${GIT_SERVER_SERVICE} --timeout=180s`); +} + +/** + * Push the current HEAD into the in-cluster bare repo as `main`. + * + * The local port-forward is the only path from the host into vc2, so the + * cluster's own git daemon can't be reached directly. + */ +async function publishHead(): Promise { + const forward = Bun.spawn( + ["kubectl", "-n", "argocd", "port-forward", `svc/git-server`, `${GIT_SERVER_PROXY_LOCAL_PORT}:9418`], + { stdout: "ignore", stderr: "ignore" }, + ); + try { + const localUrl = `git://127.0.0.1:${GIT_SERVER_PROXY_LOCAL_PORT}/k8s.git`; + let ready = false; + for (let i = 0; i < 40 && !ready; i++) { + ready = await ok($`timeout 2 git ls-remote ${localUrl}`); + if (!ready) await Bun.sleep(500); + } + if (!ready) { + console.error(`✗ could not reach the in-cluster git server on 127.0.0.1:${GIT_SERVER_PROXY_LOCAL_PORT}`); + process.exit(1); + } + await check($`git -C ${ROOT} push --force ${localUrl} HEAD:refs/heads/${GIT_SERVER_TARGET_BRANCH}`); + } finally { + forward.kill(); + } +} + +// --- ArgoCD --------------------------------------------------------------- + +async function installArgoCd(): Promise { + await check($`kubectl kustomize --enable-helm argocd/ | kubectl apply -f -`.cwd(ROOT)); + await check( + $`kubectl wait --for=condition=Established --timeout=180s crd/applications.argoproj.io crd/applicationsets.argoproj.io`, + ); + await check($`kubectl -n argocd rollout status deployment/argocd-applicationset-controller --timeout=180s`); +} + +async function applyDevStorageClasses(): Promise { + await check($`kubectl apply -f ${join(ROOT, ".dev/dev-storage-classes.yaml")}`); +} + +async function up(): Promise { + for (const bin of ["k3d", "vcluster", "kubectl", "helm", "git", "bun"]) { + if (!(await have(bin))) { + console.error(`✗ missing required tool: ${bin}`); + process.exit(1); + } + } + + if (await isDirty()) { + console.warn("⚠ the working tree has uncommitted changes."); + console.warn(" ArgoCD only sees committed work; run `bun run local-cluster:sync` to publish HEAD."); + } + + await check($`bun install`.cwd(ROOT)); + await check($`bun run cdk8s:import`.cwd(ROOT)); + + if (!(await k3dClusterExists())) { + await check($`k3d cluster create ${K3D_CLUSTER_NAME} --image ${K3S_IMAGE}`); + } else { + console.log(`✓ k3d cluster ${K3D_CLUSTER_NAME} exists`); + } + + await check($`kubectl config use-context k3d-${K3D_CLUSTER_NAME}`); + await applyDevStorageClasses(); + await ensureVcluster(VCLUSTERS[0]); + await ensureVcluster(VCLUSTERS[1]); + + await installArgoCd(); + await installGitServer(); + await publishHead(); + await kubectlApplyCdk8sApp(applicationSet); + + await sync(); +} + +async function sync(): Promise { + await assertLocalContext(); + + if (await isDirty()) { + console.warn("⚠ the working tree is dirty. Only committed work is pushed to"); + console.warn(` ${GIT_SERVER_TARGET_BRANCH}, so ArgoCD will not see your uncommitted changes.`); + if (!(await confirm("Continue anyway?"))) process.exit(1); + } + + await publishHead(); + + await $`kubectl -n argocd annotate applicationset/application-set argocd.argoproj.io/application-set-refresh=true --overwrite`; +} + +async function down(): Promise { + await $`k3d cluster delete ${K3D_CLUSTER_NAME}`; +} + +const command = process.argv[2]; +const commands = { up, sync, down } as const; +if (!command || !(command in commands)) { + console.error("usage: bun run local-cluster:{up,sync,down} [--yes]"); + process.exit(1); +} +await commands[command as keyof typeof commands](); diff --git a/.dev/renovate-config.ts b/.dev/renovate-config.ts new file mode 100644 index 0000000..3f9586a --- /dev/null +++ b/.dev/renovate-config.ts @@ -0,0 +1,56 @@ +import type { AllConfig } from "renovate/dist/config/types"; + +export interface RenovateAppOptions { + /** Repository that holds the ArgoCD Applications. */ + repository?: string; + /** Bot identity used for commits/PRs. */ + gitAuthor?: string; +} + +/** + * Build the Renovate *global* config for one cdk8s app. + * + * Each app's `renovate.ts` is passed via `RENOVATE_CONFIG_FILE` by + * `.dev/renovate.ts`. Renovate runs in the app's own CI (see PLAN.md), opens a + * PR against kir-dev/k8s updating `/versions.ts`, and only that file: the + * custom regex manager is scoped to it and every other manager is disabled. + * + * `versions.ts` entries are `owner/image:tag@sha256:...` strings, so the + * `docker` datasource + `pinDigests` keeps both the tag and the digest current. + */ +export function appConfig(app: string, options: RenovateAppOptions = {}): AllConfig { + const repository = options.repository ?? "kir-dev/k8s"; + const gitAuthor = options.gitAuthor ?? "Kir-Dev Bot <258595904+kir-dev-bot@users.noreply.github.com>"; + + return { + platform: "github", + onboarding: false, + requireConfig: "optional", + gitAuthor, + token: process.env.RENOVATE_TOKEN, + repositories: [ + { + repository, + enabledManagers: ["custom.regex"], + customManagers: [ + { + customType: "regex", + managerFilePatterns: [`/^${app}\\/versions\\.ts$/`], + matchStrings: [ + `['"](?[^@'"\\s]+):(?[^@'"\\s]+)(?:@(?sha256:[a-f0-9]{64}))?['"]`, + ], + datasourceTemplate: "docker", + versioningTemplate: "docker", + }, + ], + packageRules: [ + { + matchManagers: ["custom.regex"], + groupName: `${app} images`, + pinDigests: true, + }, + ], + }, + ], + }; +} diff --git a/.dev/renovate.ts b/.dev/renovate.ts new file mode 100644 index 0000000..8261764 --- /dev/null +++ b/.dev/renovate.ts @@ -0,0 +1,36 @@ +/** + * Run Renovate for a single app from that app's own CI pipeline (PLAN.md). + * + * Usage (in an app repository's GitHub Actions): + * + * git clone https://github.com/kir-dev/k8s --depth 1 + * cd k8s + * bun install + * bun run renovate APP_NAME + * + * This renders `/renovate.ts` (which typically imports + * `appConfig()` from `.dev/renovate-config.ts`) and hands it to Renovate. + */ +import { $ } from "bun"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +// Renovate's native `re2` addon is built against Node's V8 ABI and crashes +// under bun, so bunx runs it with Node (its bin shebang) rather than `--bun`. +const RENOVATE_VERSION = "44.103.0"; + +const app = process.argv[2]; +if (!app) { + console.error("usage: bun run renovate APP_NAME"); + process.exit(1); +} + +const configFile = resolve(import.meta.dir, "..", app, "renovate.ts"); +if (!existsSync(configFile)) { + console.error(`✗ ${configFile} does not exist`); + process.exit(1); +} + +process.env.RENOVATE_CONFIG_FILE = configFile; +const result = await $`bunx renovate@${RENOVATE_VERSION}`.nothrow(); +process.exit(result.exitCode ?? 1); diff --git a/.dev/shell-utils.ts b/.dev/shell-utils.ts new file mode 100644 index 0000000..dc9c5f2 --- /dev/null +++ b/.dev/shell-utils.ts @@ -0,0 +1,38 @@ +import { $ } from "bun"; +import { createInterface } from "node:readline/promises"; + +export type ShellCmd = ReturnType; + +export async function check(cmd: ShellCmd): Promise { + const result = await cmd.nothrow(); + if (result.exitCode !== 0) { + console.error("✗ command failed"); + process.exit(1); + } +} + +export async function ok(cmd: ShellCmd): Promise { + return (await cmd.quiet().nothrow()).exitCode === 0; +} + +export async function text(cmd: ShellCmd): Promise { + return (await cmd.quiet().nothrow().text()).trim(); +} + +export function have(cmd: string): Promise { + return ok($`which ${cmd}`); +} + +const yes = process.argv.includes("--yes"); + +export async function confirm(question: string): Promise { + if (yes) return true; + if (!process.stdin.isTTY) { + console.error("✗ not a TTY; re-run with --yes to proceed"); + return false; + } + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase(); + rl.close(); + return answer === "y" || answer === "yes"; +} diff --git a/.dev/types.d.ts b/.dev/types.d.ts new file mode 100644 index 0000000..0d9b2a3 --- /dev/null +++ b/.dev/types.d.ts @@ -0,0 +1,16 @@ +/** + * Renovate is run via `bunx renovate@` (see .dev/renovate.ts) instead of + * being installed, so its real types aren't on disk. This declares just the + * shape `.dev/renovate-config.ts` imports. + */ +declare module "renovate/dist/config/types" { + export interface AllConfig { + platform?: string; + token?: string; + onboarding?: boolean; + requireConfig?: string; + gitAuthor?: string; + repositories?: unknown[]; + [key: string]: unknown; + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b2fd6cf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +trim_trailing_whitespace = true + +[*.yaml] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 1b7532e..1ab952a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea -**/charts \ No newline at end of file +**/charts +.junie + +imports +dist/ +node_modules diff --git a/.vclusters/vc1/vcluster.yaml b/.vclusters/vc1/vcluster.yaml index 91ac5fd..ec85bbf 100644 --- a/.vclusters/vc1/vcluster.yaml +++ b/.vclusters/vc1/vcluster.yaml @@ -37,14 +37,14 @@ controlPlane: k8s: image: tag: v1.35.0 -# statefulSet: -# persistence: -# volumeClaim: +# statefulSet: +# persistence: +# volumeClaim: # storageClass: memory-ssd policies: podSecurityStandard: baseline - + resourceQuota: enabled: true quota: @@ -59,7 +59,7 @@ policies: requests.storage: 600Gi memory-ssd.storageclass.storage.k8s.io/requests.storage: 100Gi memory-ssd-rwx.storageclass.storage.k8s.io/requests.storage: 100Gi - node-local-zfs.storageclass.storage.k8s.io/requests.storage: 0Gi + node-local-zfs.storageclass.storage.k8s.io/requests.storage: 100Gi # might be wrong (probably is wrong) memory-hdd.storageclass.storage.k8s.io/requests.storage: 500Gi services.nodeports: 17 diff --git a/.vclusters/vc2/vcluster.yaml b/.vclusters/vc2/vcluster.yaml index 8e26496..cd03e7d 100644 --- a/.vclusters/vc2/vcluster.yaml +++ b/.vclusters/vc2/vcluster.yaml @@ -70,7 +70,7 @@ policies: requests.storage: 600Gi memory-ssd.storageclass.storage.k8s.io/requests.storage: 100Gi memory-ssd-rwx.storageclass.storage.k8s.io/requests.storage: 100Gi - node-local-zfs.storageclass.storage.k8s.io/requests.storage: 0Gi + node-local-zfs.storageclass.storage.k8s.io/requests.storage: 100Gi # might be wrong (probably is wrong) memory-hdd.storageclass.storage.k8s.io/requests.storage: 500Gi services.nodeports: 17 diff --git a/README.md b/README.md index b7c17ea..73cccfa 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,115 @@ # Kir-Dev Kubernetes configuration -## Bootstrapping +This repo contains our Kubernetes configuration, +deployed following GitOps principles using Argo CD. + +## Running locally Install +[docker](https://docs.docker.com/engine/install/), [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), -[k3d](https://k3d.io), and -the [vCluster CLI](https://www.vcluster.com/install) -(`nix-shell -p kubectl k3d vcluster` if you have Nix), -then: +[bun](https://bun.com/docs/installation), +[k3d](https://k3d.io/stable/#installation), +[helm](https://helm.sh/docs/intro/install), +and [vcluster](https://www.vcluster.com/install): +- Nix: + - install Docker (`virtualisation.docker.enable = true` on NixOS), + - then `nix-shell -p kubectl bun k3d helm vcluster` +- Homebrew (untested): + - install Docker, + - then `brew install kubernetes-cli bun k3d helm vcluster` +- Linux, WSL (untested): + ```bash + # Docker Engine (on WSL you can instead enable Docker Desktop's WSL integration) + curl -fsSL https://get.docker.com | sudo sh + sudo usermod -aG docker "$USER" # then re-login -```bash -# Create a cluster -k3d cluster create mycluster --image rancher/k3s:v1.35.0-k3s1 + curl -fsSL https://bun.sh/install | bash + curl -fsSL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + case "$(uname -m)" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; *) echo "unsupported arch" >&2; exit 1 ;; esac + kubectl_ver=$(curl -fsSL https://dl.k8s.io/release/stable.txt) + sudo curl -fsSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/${kubectl_ver}/bin/linux/${arch}/kubectl" + sudo chmod +x /usr/local/bin/kubectl + sudo curl -fsSL -o /usr/local/bin/vcluster "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-${arch}" + sudo chmod +x /usr/local/bin/vcluster + ``` + +Once you have all the tools needed, deploy the cluster on your local computer using k3d: +```bash git clone https://github.com/kir-dev/k8s cd k8s -# Create nested vClusters -# Outer vCluster, should be identical to vc-kirdev -vcluster create vc1 -n vc1 -f .vclusters/vc1/vcluster.yaml -# Inner vCluster with workarounds for nested vCluster stuff. -# ⚠️ Comment out the memory-ssd section in .vclusters/vc2/vcluster.yaml when deploying locally -vcluster create vc2 -n vc2 -f .vclusters/vc2/vcluster.yaml +# install JS dependencies, +# rerun anytime dependencies in package.json change! +bun install + +# install Kubernetes Custom Resource Definitions so that they are usable through TypeScript, +# rerun anytime cdk8s.yaml changes! +bun run cdk8s:import -# Install ArgoCD -kubectl kustomize --enable-helm argocd/ | kubectl apply -f - +# creates a k3d cluster very similar to prod +bun run local-cluster:up -# Install an ArgoCD ApplicationSet for this repository -kubectl apply -f application-set/ +# update the local config, commit!, +# then run this to push your changes to the local cluster +bun run local-cluster:sync + +# delete the local k3d cluster +bun run local-cluster:down ``` +If you run into any issues [open an issue](https://github.com/kir-dev/k8s/issues/new), +so that others won't have to run into it again. + ## Adding a new app Create a new directory containing -- `.yaml` files defining Kubernetes resources, or -- a `kustomization.yaml`. - - You can use - [`helmCharts:`](https://kubectl.docs.kubernetes.io/references/kustomize/builtins/#_helmchartinflationgenerator_) - to install Helm charts. Set values either using `valuesInline:` or by creating a `values.yaml` and - referencing it using `valuesFile:`. -ArgoCD checks each directory (except the ones starting with a `.`). If it sees `kustomization.yaml`, it `kubectl apply --kustomize`s it, otherwise it applies -`.yaml` files using `kubectl apply`. +- a cdk8s app: + - `app.ts`: + ```ts + import { versions } from "./versions.ts"; + import { IntOrString, KubeDeployment, KubeNamespace, KubeService, Quantity } from "../imports/k8s"; + import * as environment from "../.dev/environment.ts"; + import * as cnpg from "../imports/postgresql.cnpg.io.ts"; + class MyApp extends Chart { + constructor(scope: Construct, id: string) { + super(scope, id); + new cnpg.Cluster(this, /*...*/); + new KubeDeployment(this, /*...*/); + /*...*/ + } + } + const app = new App(); + new MyApp(app, "myapp"); + export default app; + ``` + - `renovate.ts`: + ```ts + import { appConfig } from "../.dev/renovate-config.ts"; + export default appConfig("myapp"); + ``` + - `versions.ts`: + ```ts + export const versions = { + image: "ghcr.io/kir-dev/myapp:0.0.1@sha256:aaaaaaaaaaaaaa", + }; + ``` +- or `.yaml` files defining Kubernetes resources, +- or a `kustomization.yaml`. + - You can use + [`helmCharts:`](https://kubectl.docs.kubernetes.io/references/kustomize/builtins/#_helmchartinflationgenerator_) + to install Helm charts. Set values either using `valuesInline:` or by creating a `values.yaml` and referencing it + using `valuesFile:`. -## Documentation +ArgoCD checks each top-level directory except the ones starting with a `.`. If it sees `kustomization.yaml`, it +`kubectl apply --kustomize`s it, otherwise it applies `.yaml` files using `kubectl apply`. -- https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/ -- ArgoCD `Application` reference: https://argo-cd.readthedocs.io/en/stable/user-guide/application-specification/ -- Manage Argo CD Using Argo CD: - https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#manage-argo-cd-using-argo-cd -- Kustomization file documentation: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/ +> [!IMPORTANT] +> Ensure that every single deployment/pod has CPU/memory/ephemeral-storage requests/limits specified (even for resources created by a Helm chart!). +> Missing it anywhere causes ArgoCD to crash due to a bug with nested vClusters. ## Notes @@ -60,10 +120,25 @@ ArgoCD checks each directory (except the ones starting with a `.`). If it sees ` ```yaml # yaml-language-server: $schema=https://.../values.schema.json ``` - at the top of the `values.yaml`. Find the `values.schema.json` file in the chart's GitHub repository, - then press the *Raw* button to get a link. -- Set `resources.{limits,requests}.ephemeral-storage`, as the default (1GiB) uses more than allowed by the quota - (especially for the limit) -- Always specify the Postgres image version for CNPG `Cluster`s, otherwise backups can't be restored - due to the version mismatch -- Don't forget `database`/`owner` fields when restoring a CNPG DB from a backup \ No newline at end of file + at the top of the `values.yaml`. Find the `values.schema.json` file in the chart's GitHub repository, then press + the *Raw* button to get a link. +- Set `resources.{limits,requests}.ephemeral-storage`, as the default (1GiB) uses up too much of our quota. +- Always specify the Postgres image version for CNPG `Cluster`s, otherwise backups can't be restored due to the version + mismatch +- Don't forget `database`/`owner` fields when restoring a CNPG DB from a backup + +## Documentation links + +- ArgoCD `Application` resource reference: https://argo-cd.readthedocs.io/en/stable/user-guide/application-specification/ +- Manage Argo CD Using Argo CD: + https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#manage-argo-cd-using-argo-cd +- `kustomization.yaml` documentation: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/ + +## Bootstrapping the production cluster + +Given `kubectl config current-context` == `vc-kirdev`, installs the inner vCluster, Argo CD and the ApplicationSet: + +```bash +bun install +bun run bootstrap-prod +``` diff --git a/application-set/app.ts b/application-set/app.ts new file mode 100644 index 0000000..e653737 --- /dev/null +++ b/application-set/app.ts @@ -0,0 +1,56 @@ +import { ApplicationSet } from "../imports/argoproj.io"; +import * as environment from "../.dev/environment.ts"; +import { singletonApp } from "../.dev/cdk8s-utils.ts"; + +export default singletonApp({ namespace: "argocd" }, (scope) => { + new ApplicationSet(scope, "application-set", { + metadata: { + name: "application-set", + }, + spec: { + goTemplate: true, + goTemplateOptions: ["missingkey=error"], + generators: [ + { + git: { + repoUrl: environment.k8sRepoUrl, + revision: environment.k8sRepoRevision ?? "HEAD", + directories: [ + // include all directories + { + path: "*", + }, + // exclude .directories + { + path: ".*", + exclude: true, + }, + ], + }, + }, + ], + template: { + metadata: { + name: "{{.path.basename}}", + finalizers: ["resources-finalizer.argocd.argoproj.io"], + }, + spec: { + project: "default", + source: { + repoUrl: environment.k8sRepoUrl, + targetRevision: environment.k8sRepoRevision, + path: "{{.path.path}}", + }, + destination: { name: "in-cluster" }, + syncPolicy: { + automated: { + prune: true, + selfHeal: true, + }, + syncOptions: ["ServerSideApply=true", "CreateNamespace=true"], + }, + }, + }, + }, + }); +}); diff --git a/application-set/application-set.yaml b/application-set/application-set.yaml deleted file mode 100644 index d798bd1..0000000 --- a/application-set/application-set.yaml +++ /dev/null @@ -1,50 +0,0 @@ -kind: ApplicationSet -apiVersion: argoproj.io/v1alpha1 -metadata: - name: apps - namespace: argocd -spec: - goTemplate: true - goTemplateOptions: [ "missingkey=error" ] - generators: - - git: - repoURL: https://github.com/kir-dev/k8s - revision: HEAD - directories: - - path: "*" -# - path: startsch -# exclude: true - # - path: argocd - # exclude: true - # - path: application-set - # exclude: true - # - path: tempo - # exclude: true - # - path: opentelemetry-collector - # exclude: true - # - path: loki - # exclude: true - # - path: kube-prometheus-stack - # exclude: true - # - path: cnpg - # exclude: true - # - path: cert-manager - # exclude: true - template: - metadata: - name: "{{.path.basename}}" - finalizers: - - resources-finalizer.argocd.argoproj.io - spec: - project: default - source: - repoURL: https://github.com/kir-dev/k8s - path: "{{.path.path}}" - destination: - name: in-cluster - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - ServerSideApply=true diff --git a/argocd/kustomization.yaml b/argocd/kustomization.yaml index dc416e7..423e281 100644 --- a/argocd/kustomization.yaml +++ b/argocd/kustomization.yaml @@ -22,10 +22,50 @@ helmCharts: # https://argo-cd.readthedocs.io/en/stable/user-guide/kustomize/#kustomizing-helm-charts kustomize.buildOptions: "--enable-helm" + params: + # Serialize manifest generation. cdk8s apps are allowed to be + # processed concurrently (ArgoCD does so for all non-kustomize + # sources), but they share the bun/cdk8s caches and each + # `cdk8s:import` fans out to 16 processes — concurrent generations + # race on the cache and can OOM the sidecar. + # https://argo-cd.readthedocs.io/en/stable/operator-manual/argocd-cmd-params-cm.yaml + reposerver.parallelism.limit: "1" + + # Configure our Config Management Plugin for cdk8s apps + # https://argo-cd.readthedocs.io/en/stable/operator-manual/config-management-plugins/ + cmp: + create: true + plugins: + cdk8s: + discover: + fileName: "app.ts" + init: + command: [ bash, -c ] + args: + - | + set -e + # Use the same volume as where the git repo is, so that node_modules can be hard-linked instead of copied. + export BUN_INSTALL_CACHE_DIR=/tmp/bun + export CDK8S_IMPORT_CACHE=/tmp/cdk8s-imports + + cd .. + bun install --frozen-lockfile + bun run cdk8s:import + bun run cdk8s:synth $ARGOCD_APP_NAME + generate: + command: [ bash, -c ] + args: + - | + # put a --- between files + for f in ../dist/$ARGOCD_APP_NAME/*.k8s.yaml; do + printf -- '---\n' + cat "$f" + done + global: networkPolicy: create: true - + # resource requests/limits are from https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/values.yaml # nvm they are useless controller: @@ -109,6 +149,51 @@ helmCharts: cpu: 50m memory: 32Mi ephemeral-storage: 50Mi + + # Sidecar running the `cdk8s` CMP + extraContainers: + - name: cmp-cdk8s + command: [ "/var/run/argocd/argocd-cmp-server" ] + image: nixery.dev/shell/bun/kubernetes-helm/nodejs + imagePullPolicy: IfNotPresent + env: + # bun needs a writable HOME for its install cache; /tmp is the cmp-tmp emptyDir. + - name: HOME + value: /tmp + securityContext: + runAsNonRoot: true + runAsUser: 999 + # Explicit requests/limits are required: the local vCluster doesn't + # reflect injected defaults, and an ArgoCD controller panics + # ("assignment to entry in nil map") on containers with none. + resources: + requests: + cpu: 100m + memory: 256Mi + ephemeral-storage: 0 + limits: + cpu: 2000m + memory: 4Gi + ephemeral-storage: 2Gi + volumeMounts: + - mountPath: /var/run/argocd + name: var-files + - mountPath: /home/argocd/cmp-server/plugins + name: plugins + - mountPath: /home/argocd/cmp-server/config/plugin.yaml + subPath: cdk8s.yaml + name: argocd-cmp-cm + # Do not share the repo-server's /tmp volume (path traversal mitigation). + - mountPath: /tmp + name: cmp-tmp + + volumes: + - name: argocd-cmp-cm + configMap: + name: argocd-cmp-cm + - name: cmp-tmp + emptyDir: { } + resources: limits: cpu: 700m diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c120308 --- /dev/null +++ b/bun.lock @@ -0,0 +1,350 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "dependencies": { + "cdk8s": "2.70.93", + "cdk8s-cli": "2.207.53", + "constructs": "10.8.1", + "yaml": "2.9.0", + }, + "devDependencies": { + "@types/bun": "1.3.14", + "@types/node": "22.20.4", + "prettier": "3.9.8", + }, + }, + }, + "packages": { + "@jsii/check-node": ["@jsii/check-node@1.139.0", "", { "dependencies": { "chalk": "^4.1.2", "semver": "^7.8.5" } }, "sha512-tLS0H2XPN3hWVO9OtVB2DDllaxz3M6Jz8Zk8ByV1pl2h5b6LayJg8lHYvRMf0b3P/82xqWat7CpfLeqU96OwVw=="], + + "@jsii/spec": ["@jsii/spec@1.139.0", "", {}, "sha512-+l1B0h4WAg6KOQ7PGykW/FvxeFmyt74U0/n41cus9Jdjv3VfQJKynsv5r9Ng23B1KwWqJkS5YvJuv2yGYoULdw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@22.20.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.11", "", {}, "sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "case": ["case@1.6.3", "", {}, "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ=="], + + "cdk8s": ["cdk8s@2.70.93", "", { "dependencies": { "fast-json-patch": "^3.1.1", "follow-redirects": "^1.16.0", "yaml": "2.9.0" }, "peerDependencies": { "constructs": "^10" } }, "sha512-MKla1n0DkrvxANYizpsC7FoylpGv2W5dn2SMsfKFkCVBm3QTQd9QyWnRS5ldsv+C6fj5mbeID/i/D/aYzeRJpA=="], + + "cdk8s-cli": ["cdk8s-cli@2.207.53", "", { "dependencies": { "@types/node": "^16", "ajv": "^8.20.0", "cdk8s": "^2.70.93", "cdk8s-plus-33": "^2.5.49", "codemaker": "^1.139.0", "colors": "1.4.0", "constructs": "^10.8.1", "fs-extra": "^8", "jsii-pacmak": "^1.139.0", "jsii-rosetta": "^5.9.62", "jsii-srcmak": "0.1.1319", "json2jsii": "0.5.19", "semver": "^7.8.5", "sscaff": "^1.2.274", "table": "^6.9.0", "yaml": "2.9.0", "yargs": "^15" }, "bin": { "cdk8s": "bin/cdk8s" } }, "sha512-gCb91zgMRp1fI0fOo+G1gFkPqKT7+52Cx0OGa2L5Usjb1VPBvaae72TiR0d1oqXff0aTDqJzbCz9/+R+TFyNgA=="], + + "cdk8s-plus-33": ["cdk8s-plus-33@2.5.49", "", { "dependencies": { "minimatch": "^9.0.9" }, "peerDependencies": { "cdk8s": "^2.68.11", "constructs": "^10.3.0" } }, "sha512-DUjrSCjPhA/aolVyYd5do+7SNSilym2xEWOEfcs1QaSXKP2mut1jbaNdqpVDgIyqgoIIdxKCLplQlL8rKV7IoA=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "codemaker": ["codemaker@1.139.0", "", { "dependencies": { "camelcase": "^6.3.0", "decamelize": "^5.0.1", "fs-extra": "^10.1.0" } }, "sha512-v93CwWcepdmHDENEJuPepaOzo2J8SZ5HKNVgH/+6vPrvLOoDDfVXEkBMr6KUu4trHN7QvZjFwxJBpjCYCBG9+g=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], + + "commonmark": ["commonmark@0.31.2", "", { "dependencies": { "entities": "~3.0.1", "mdurl": "~1.0.1", "minimist": "~1.2.8" }, "bin": { "commonmark": "bin/commonmark" } }, "sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg=="], + + "constructs": ["constructs@10.8.1", "", {}, "sha512-98yGXYyhePqPYh3cYu8nzBERmAhC0DONe3UD03okK0nehZ7hYP4wgZuf02a04+uOWxnTJ5Rpp5m0GRNpwyLGGA=="], + + "date-format": ["date-format@4.0.14", "", {}, "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decamelize": ["decamelize@5.0.1", "", {}, "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA=="], + + "detect-indent": ["detect-indent@5.0.0", "", {}, "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g=="], + + "detect-newline": ["detect-newline@2.1.0", "", {}, "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-patch": ["fast-json-patch@3.1.1", "", {}, "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="], + + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jsii": ["jsii@5.9.52", "", { "dependencies": { "@jsii/check-node": "1.139.0", "@jsii/spec": "1.139.0", "case": "^1.6.3", "chalk": "^4", "fast-deep-equal": "^3.1.3", "log4js": "^6.9.1", "semver": "^7.8.5", "semver-intersect": "^1.5.0", "sort-json": "^2.0.1", "spdx-license-list": "^6.12.0", "typescript": "~5.9", "yargs": "^17.7.3" }, "bin": { "jsii": "bin/jsii" } }, "sha512-iOdM2f2iA6Wwr4cIapdBmmBMzE8lghZLIyvRsBT/dWBxgqWoOI0D4O/o2xJETOJ37bNdDv1dZZe9W20r+r7gjQ=="], + + "jsii-pacmak": ["jsii-pacmak@1.139.0", "", { "dependencies": { "@jsii/check-node": "1.139.0", "@jsii/spec": "1.139.0", "clone": "^2.1.2", "codemaker": "^1.139.0", "commonmark": "^0.31.2", "escape-string-regexp": "^4.0.0", "fs-extra": "^10.1.0", "jsii-reflect": "^1.139.0", "semver": "^7.8.5", "spdx-license-list": "^6.11.0", "xmlbuilder": "^15.1.1", "yargs": "^17.7.3" }, "peerDependencies": { "jsii-rosetta": ">=5.9.0" }, "bin": { "jsii-pacmak": "bin/jsii-pacmak" } }, "sha512-hE+rc+RS4Qwe+MzGqG515tXueOhn6r++/L8kpX5wT18HqUoBJdOdqFOv90PYitDgNywovOE0kEd8YyV1S5Mh6Q=="], + + "jsii-reflect": ["jsii-reflect@1.139.0", "", { "dependencies": { "@jsii/check-node": "1.139.0", "@jsii/spec": "1.139.0", "chalk": "^4", "fs-extra": "^10.1.0", "oo-ascii-tree": "^1.139.0", "yargs": "^17.7.3" }, "bin": { "jsii-query": "bin/jsii-query", "jsii-tree": "bin/jsii-tree" } }, "sha512-Z8rGDzykcg6REfFV8HJZ0oms5zi8wBO4T7B5thSS4iME0osbWJP62pFn2yT6VivOESKnkFZOI3RkIKYkynPbGg=="], + + "jsii-rosetta": ["jsii-rosetta@5.9.62", "", { "dependencies": { "@jsii/check-node": "^1.139.0", "@jsii/spec": "^1.139.0", "@xmldom/xmldom": "^0.9.11", "chalk": "^4", "commonmark": "^0.31.2", "fast-glob": "^3.3.3", "jsii": "~5.9.1", "semver": "^7.8.5", "semver-intersect": "^1.5.0", "stream-json": "^1.9.1", "typescript": "~5.9", "workerpool": "^6.5.1", "yargs": "^17.7.3" }, "bin": { "jsii-rosetta": "bin/jsii-rosetta" } }, "sha512-Zvg/aZnPna8vfn00WX/V1PhmSvehonNxwb6WxwSNK2cyzehw8XmiIRiAszyKRc5NhBTk5XWvnZroBZ0EXDeEZw=="], + + "jsii-srcmak": ["jsii-srcmak@0.1.1319", "", { "dependencies": { "fs-extra": "^9.1.0", "jsii": "~5.9.3", "jsii-pacmak": "^1.113.0", "jsii-rosetta": "^5.9.3", "ncp": "^2.0.0", "yargs": "^17.7.2" }, "bin": { "jsii-srcmak": "bin/jsii-srcmak" } }, "sha512-gF/3PZX+iQDyZ2aybbWOmQR3UiYAKLWUzIuuHV/0hprctiBlbQMmZ+PPr5Ix7HuRYqT7UdlIAt06rySrqhefzg=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json2jsii": ["json2jsii@0.5.19", "", { "dependencies": { "camelcase": "^6.3.0", "json-schema": "^0.4.0", "snake-case": "^3.0.4" } }, "sha512-MmLACPSLsINCoQETUt7/cX/jPI3jiY7waum2I+bbv9vNxR440q66FfCMlWN/8+71InA5gjzOttx+uJEef9ispg=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + + "log4js": ["log4js@6.9.1", "", { "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", "flatted": "^3.2.7", "rfdc": "^1.3.0", "streamroller": "^3.1.5" } }, "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "mdurl": ["mdurl@1.0.1", "", {}, "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "ncp": ["ncp@2.0.0", "", { "bin": { "ncp": "./bin/ncp" } }, "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "oo-ascii-tree": ["oo-ascii-tree@1.139.0", "", {}, "sha512-Sv9334UvsBcvYh9xkcBtheWjH+mkhXLmuhOm+/OTGVFUCsvdVdXwDMTbYZHpJaTSbhaHScPbHqjAPECAFwlgtg=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "prettier": ["prettier@3.9.8", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "semver-intersect": ["semver-intersect@1.5.0", "", { "dependencies": { "semver": "^6.3.0" } }, "sha512-BDjWX7yCC0haX4W/zrnV2JaMpVirwaEkGOBmgRQtH++F1N3xl9v7k9H44xfTqwl+yLNNSbMKosoVSTIiJVQ2Pw=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "sort-json": ["sort-json@2.0.1", "", { "dependencies": { "detect-indent": "^5.0.0", "detect-newline": "^2.1.0", "minimist": "^1.2.0" }, "bin": { "sort-json": "app/cmd.js" } }, "sha512-s8cs2bcsQCzo/P2T/uoU6Js4dS/jnX8+4xunziNoq9qmSpZNCrRIAIvp4avsz0ST18HycV4z/7myJ7jsHWB2XQ=="], + + "spdx-license-list": ["spdx-license-list@6.12.0", "", {}, "sha512-+nUYqm3aZMSHbjsthK+i/HHI2okTElCvqwUd4k8QcSk+FTGgjq+fsdFj2wZOaX6XmR2JdWhf/NeflNnvKOjcnQ=="], + + "sscaff": ["sscaff@1.2.274", "", {}, "sha512-sztRa50SL1LVxZnF1au6QT1SC2z0S1oEOyi2Kpnlg6urDns93aL32YxiJcNkLcY+VHFtVqm/SRv4cb+6LeoBQA=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "streamroller": ["streamroller@3.1.5", "", { "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", "fs-extra": "^8.1.0" } }, "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + + "workerpool": ["workerpool@6.5.1", "", {}, "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + + "bun-types/@types/node": ["@types/node@16.18.126", "", {}, "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw=="], + + "cdk8s-cli/@types/node": ["@types/node@16.18.126", "", {}, "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw=="], + + "codemaker/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "jsii/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "jsii-pacmak/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "jsii-pacmak/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "jsii-reflect/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "jsii-reflect/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "jsii-rosetta/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "jsii-srcmak/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "jsii-srcmak/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "semver-intersect/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "yargs/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "yargs-parser/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "codemaker/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "codemaker/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "jsii-pacmak/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsii-pacmak/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "jsii-pacmak/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jsii-pacmak/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jsii-pacmak/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jsii-reflect/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsii-reflect/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "jsii-reflect/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jsii-reflect/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jsii-reflect/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jsii-rosetta/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jsii-rosetta/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jsii-rosetta/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jsii-srcmak/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsii-srcmak/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "jsii-srcmak/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jsii-srcmak/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jsii-srcmak/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jsii/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jsii/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jsii/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jsii-pacmak/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jsii-reflect/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jsii-rosetta/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jsii-srcmak/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jsii/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..9897533 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +[install] +# https://bun.com/docs/runtime/bunfig#install-auto +# Disable automatically installing dependencies when a .ts script does not have a node_modules. +# The default made debugging an issue with symlinked .ts files *very* painful. +auto = "disable" diff --git a/cdk8s.yaml b/cdk8s.yaml new file mode 100644 index 0000000..ef24204 --- /dev/null +++ b/cdk8s.yaml @@ -0,0 +1,26 @@ +language: typescript + +# https://cdk8s.io/docs/latest/cli/import/#import-types +imports: + - k8s@1.36.0 # version from https://github.com/cdk8s-team/cdk8s/tree/master/kubernetes-schemas + + - helm:oci://ghcr.io/argoproj/argo-helm/argo-cd@9.3.4 + - helm:oci://quay.io/jetstack/charts/cert-manager@v1.19.2 + - helm:https://cloudnative-pg.github.io/charts/cloudnative-pg@0.27.0 + - helm:https://cloudnative-pg.io/charts/plugin-barman-cloud@0.4.0 + - helm:https://prometheus-community.github.io/helm-charts/kube-prometheus-stack@81.2.0 + - helm:https://grafana.github.io/helm-charts/loki@6.49.0 + - helm:https://grafana.github.io/helm-charts/tempo@1.24.3 + - helm:https://traefik.github.io/charts/traefik@39.0.0 + - helm:https://open-telemetry.github.io/opentelemetry-helm-charts/opentelemetry-collector@0.143.0 + + # NOTE: You can only have one .yaml CRD file per API group (like networking.k8s.io), + # as they don't get merged, instead the new one replaces the old one. + # https://github.com/cdk8s-team/cdk8s-cli/issues/3797 + - https://raw.githubusercontent.com/argoproj/argo-cd/v3.2.5/manifests/install.yaml + - https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.crds.yaml + - https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.28.0/releases/cnpg-1.28.0.yaml + - https://raw.githubusercontent.com/cloudnative-pg/plugin-barman-cloud/v0.10.0/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml + - https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.88.0/bundle.yaml + - https://raw.githubusercontent.com/traefik/traefik/v3.6.7/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml + diff --git a/cert-manager/values.yaml b/cert-manager/values.yaml index 792d081..eec3f48 100644 --- a/cert-manager/values.yaml +++ b/cert-manager/values.yaml @@ -18,7 +18,7 @@ resources: ephemeral-storage: 200Mi cainjector: - resources: + resources: requests: cpu: 100m memory: 128Mi @@ -27,9 +27,9 @@ cainjector: cpu: 300m memory: 256Mi ephemeral-storage: 200Mi - + webhook: - resources: + resources: requests: cpu: 100m memory: 128Mi @@ -40,7 +40,7 @@ webhook: ephemeral-storage: 200Mi startupapicheck: - resources: + resources: requests: cpu: 100m memory: 128Mi @@ -49,4 +49,4 @@ startupapicheck: cpu: 300m memory: 256Mi ephemeral-storage: 200Mi - + diff --git a/demo/app.ts b/demo/app.ts new file mode 100644 index 0000000..2753af6 --- /dev/null +++ b/demo/app.ts @@ -0,0 +1,64 @@ +import { Construct } from "constructs"; +import { App, Chart } from "cdk8s"; +import { IntOrString, KubeDeployment, KubeNamespace, KubeService, Quantity } from "../imports/k8s"; +import { versions } from "./versions.ts"; + +/** + * A trivial cdk8s app that renders a Deployment + Service, proving that + * ArgoCD can synthesize and apply cdk8s manifests through the `cdk8s` + * Config Management Plugin. + */ +class DemoChart extends Chart { + constructor(scope: Construct, ns: string) { + super(scope, ns); + + new KubeNamespace(this, "demo-namespace", { metadata: { name: "demo" } }); + + const labels = { app: "demo" }; + const metadata = { name: "demo", namespace: "demo", labels }; + + new KubeDeployment(this, "demo-deployment", { + metadata, + spec: { + replicas: 2, + selector: { matchLabels: labels }, + template: { + metadata: { labels }, + spec: { + containers: [ + { + name: "demo", + image: versions.image, + ports: [{ containerPort: 80 }], + resources: { + requests: { + cpu: Quantity.fromString("10m"), + memory: Quantity.fromString("32Mi"), + }, + limits: { + cpu: Quantity.fromString("100m"), + memory: Quantity.fromString("128Mi"), + "ephemeral-storage": Quantity.fromString("50Mi"), + }, + }, + }, + ], + }, + }, + }, + }); + + new KubeService(this, "demo-service", { + metadata, + spec: { + selector: labels, + ports: [{ port: 80, targetPort: IntOrString.fromNumber(80) }], + }, + }); + } +} + +const app = new App(); +new DemoChart(app, "demo"); + +export default app; diff --git a/demo/renovate.ts b/demo/renovate.ts new file mode 100644 index 0000000..d08e1cd --- /dev/null +++ b/demo/renovate.ts @@ -0,0 +1,3 @@ +import { appConfig } from "../.dev/renovate-config.ts"; + +export default appConfig("demo"); diff --git a/demo/versions.ts b/demo/versions.ts new file mode 100644 index 0000000..ae3b913 --- /dev/null +++ b/demo/versions.ts @@ -0,0 +1,3 @@ +export const versions = { + image: "nginx:1.27.4", +}; diff --git a/ehk/app.ts b/ehk/app.ts new file mode 100644 index 0000000..23dcd09 --- /dev/null +++ b/ehk/app.ts @@ -0,0 +1,419 @@ +// https://github.com/kir-dev/ehk +// +// https://ehk.kir-dev.hu +// +// Next.js + Payload CMS (Postgres) application. + +import * as kube from "../imports/k8s"; +import * as environment from "../.dev/environment.ts"; +import * as cnpg from "../imports/postgresql.cnpg.io.ts"; +import { versions } from "./versions.ts"; +import { singletonApp } from "../.dev/cdk8s-utils.ts"; + +export default singletonApp({ namespace: "ehk", createNamespace: true }, (scope) => { + const labels = { + "app.kubernetes.io/name": "ehk", + "app.kubernetes.io/instance": "ehk", + "app.kubernetes.io/component": "server", + "app.kubernetes.io/part-of": "ehk", + }; + + new kube.KubeConfigMap(scope, "ehk-config", { + metadata: { + name: "ehk-config", + annotations: { "argocd.argoproj.io/sync-wave": "-25" }, + }, + data: { + NODE_ENV: "production", + NEXT_TELEMETRY_DISABLED: "1", + }, + }); + + // Set manually in production: + // PAYLOAD_SECRET: + // S3_BUCKET: + // S3_ACCESS_KEY_ID: + // S3_SECRET_ACCESS_KEY: + // S3_REGION: + // S3_ENDPOINT: + new kube.KubeSecret(scope, "ehk-secrets", { + metadata: { + name: "ehk-secrets", + annotations: { "argocd.argoproj.io/sync-wave": "-25" }, + }, + ...(environment.environment != "Production" + ? { + stringData: { + PAYLOAD_SECRET: "local-development-secret", + S3_BUCKET: "ehk-media", + S3_ACCESS_KEY_ID: "local", + S3_SECRET_ACCESS_KEY: "localpass", + S3_REGION: "us-east-1", + S3_ENDPOINT: "http://ehk-minio:9000", + }, + } + : {}), + }); + + new cnpg.Cluster(scope, "ehk-db", { + metadata: { + name: "ehk-db", + labels: { + "app.kubernetes.io/name": "postgres", + "app.kubernetes.io/instance": "postgres-ehk", + "app.kubernetes.io/component": "database", + "app.kubernetes.io/part-of": "ehk", + }, + annotations: { "argocd.argoproj.io/sync-wave": "-20" }, + }, + spec: { + primaryUpdateStrategy: cnpg.ClusterSpecPrimaryUpdateStrategy.UNSUPERVISED, + primaryUpdateMethod: cnpg.ClusterSpecPrimaryUpdateMethod.SWITCHOVER, + instances: 2, + imageName: "ghcr.io/cloudnative-pg/postgresql:17.5", + imagePullPolicy: "IfNotPresent", + monitoring: { enablePodMonitor: true }, + postgresql: { + parameters: { + wal_level: "replica", + shared_buffers: "128MB", + }, + }, + resources: { + limits: { + cpu: cnpg.ClusterSpecResourcesLimits.fromString("500m"), + memory: cnpg.ClusterSpecResourcesLimits.fromString("512Mi"), + "ephemeral-storage": cnpg.ClusterSpecResourcesLimits.fromString("500Mi"), + }, + requests: { + cpu: cnpg.ClusterSpecResourcesRequests.fromString("100m"), + memory: cnpg.ClusterSpecResourcesRequests.fromString("128Mi"), + "ephemeral-storage": cnpg.ClusterSpecResourcesRequests.fromString("100Mi"), + }, + }, + storage: { + size: "1.5Gi", + storageClass: "node-local-zfs", + }, + bootstrap: { + initdb: { + database: "ehk", + owner: "ehk", + }, + }, + }, + }); + + // Apply Payload migrations before the new image starts serving traffic. + // A Sync hook (not PreSync) is required because CNPG lives in this same + // Application and is only created in the previous sync wave. + new kube.KubeJob(scope, "ehk-migrate", { + metadata: { + name: "ehk-migrate", + labels, + annotations: { + "argocd.argoproj.io/hook": "Sync", + "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded", + "argocd.argoproj.io/sync-wave": "-10", + }, + }, + spec: { + backoffLimit: 1, + activeDeadlineSeconds: 600, + template: { + metadata: { labels }, + spec: { + restartPolicy: "Never", + automountServiceAccountToken: false, + initContainers: [ + { + name: "wait-for-database", + image: "ghcr.io/cloudnative-pg/postgresql:17.5", + imagePullPolicy: "IfNotPresent", + command: ["/bin/sh", "-ec"], + args: ["until pg_isready -h ehk-db-rw -p 5432 -U postgres; do sleep 2; done"], + resources: { + requests: { + cpu: kube.Quantity.fromString("10m"), + memory: kube.Quantity.fromString("16Mi"), + "ephemeral-storage": kube.Quantity.fromString("5Mi"), + }, + limits: { + cpu: kube.Quantity.fromString("50m"), + memory: kube.Quantity.fromString("32Mi"), + "ephemeral-storage": kube.Quantity.fromString("20Mi"), + }, + }, + }, + ], + containers: [ + { + name: "migrate", + image: versions.image, + imagePullPolicy: "IfNotPresent", + command: ["yarn", "migrate"], + env: [ + { + name: "DATABASE_URI", + valueFrom: { secretKeyRef: { name: "ehk-db-app", key: "uri" } }, + }, + { + name: "PAYLOAD_SECRET", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "PAYLOAD_SECRET" } }, + }, + ], + resources: { + requests: { + cpu: kube.Quantity.fromString("50m"), + memory: kube.Quantity.fromString("128Mi"), + "ephemeral-storage": kube.Quantity.fromString("20Mi"), + }, + limits: { + cpu: kube.Quantity.fromString("250m"), + memory: kube.Quantity.fromString("512Mi"), + "ephemeral-storage": kube.Quantity.fromString("100Mi"), + }, + }, + }, + ], + }, + }, + }, + }); + + if (environment.environment != "Production") { + // Local S3-compatible object storage for the `media` collection. + // Production uses an external bucket configured via `ehk-secrets`. + const minioLabels = { "app.kubernetes.io/name": "ehk-minio", "app.kubernetes.io/part-of": "ehk" }; + new kube.KubeDeployment(scope, "ehk-minio", { + metadata: { name: "ehk-minio", labels: minioLabels }, + spec: { + replicas: 1, + selector: { matchLabels: { "app.kubernetes.io/name": "ehk-minio" } }, + template: { + metadata: { labels: minioLabels }, + spec: { + containers: [ + { + name: "minio", + image: versions.minio, + imagePullPolicy: "IfNotPresent", + args: ["server", "/data", "--console-address", ":9001"], + env: [ + { + name: "MINIO_ROOT_USER", + valueFrom: { + secretKeyRef: { name: "ehk-secrets", key: "S3_ACCESS_KEY_ID" }, + }, + }, + { + name: "MINIO_ROOT_PASSWORD", + valueFrom: { + secretKeyRef: { name: "ehk-secrets", key: "S3_SECRET_ACCESS_KEY" }, + }, + }, + ], + ports: [ + { name: "api", containerPort: 9000 }, + { name: "console", containerPort: 9001 }, + ], + resources: { + requests: { + cpu: kube.Quantity.fromString("50m"), + memory: kube.Quantity.fromString("128Mi"), + "ephemeral-storage": kube.Quantity.fromString("0"), + }, + limits: { + cpu: kube.Quantity.fromString("500m"), + memory: kube.Quantity.fromString("512Mi"), + "ephemeral-storage": kube.Quantity.fromString("500Mi"), + }, + }, + volumeMounts: [{ name: "data", mountPath: "/data" }], + }, + ], + volumes: [{ name: "data", emptyDir: {} }], + }, + }, + }, + }); + + new kube.KubeService(scope, "ehk-minio-service", { + metadata: { name: "ehk-minio", labels: minioLabels }, + spec: { + selector: { "app.kubernetes.io/name": "ehk-minio" }, + ports: [{ name: "http", port: 9000, targetPort: kube.IntOrString.fromNumber(9000) }], + }, + }); + + new kube.KubeJob(scope, "ehk-minio-bucket", { + metadata: { name: "ehk-minio-bucket", labels: minioLabels }, + spec: { + backoffLimit: 5, + template: { + metadata: { labels: minioLabels }, + spec: { + restartPolicy: "Never", + containers: [ + { + name: "create-bucket", + image: versions.minioClient, + imagePullPolicy: "IfNotPresent", + command: ["/bin/sh", "-ec"], + args: [ + 'until mc alias set local http://ehk-minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do sleep 2; done; mc mb --ignore-existing local/ehk-media', + ], + env: [ + { + name: "MINIO_ROOT_USER", + valueFrom: { + secretKeyRef: { name: "ehk-secrets", key: "S3_ACCESS_KEY_ID" }, + }, + }, + { + name: "MINIO_ROOT_PASSWORD", + valueFrom: { + secretKeyRef: { name: "ehk-secrets", key: "S3_SECRET_ACCESS_KEY" }, + }, + }, + ], + resources: { + requests: { + cpu: kube.Quantity.fromString("10m"), + memory: kube.Quantity.fromString("16Mi"), + "ephemeral-storage": kube.Quantity.fromString("0"), + }, + limits: { + cpu: kube.Quantity.fromString("100m"), + memory: kube.Quantity.fromString("64Mi"), + "ephemeral-storage": kube.Quantity.fromString("50Mi"), + }, + }, + }, + ], + }, + }, + }, + }); + } + + new kube.KubeService(scope, "ehk-service", { + metadata: { name: "ehk", labels }, + spec: { + selector: labels, + ports: [{ name: "http", port: 80, targetPort: kube.IntOrString.fromString("http") }], + }, + }); + + new kube.KubeDeployment(scope, "ehk-deployment", { + metadata: { name: "ehk", labels }, + spec: { + replicas: 1, + selector: { matchLabels: labels }, + template: { + metadata: { labels }, + spec: { + automountServiceAccountToken: false, + containers: [ + { + name: "ehk", + image: versions.image, + imagePullPolicy: "IfNotPresent", + ports: [{ containerPort: 3000, protocol: "TCP", name: "http" }], + env: [ + { + name: "DATABASE_URI", + valueFrom: { secretKeyRef: { name: "ehk-db-app", key: "uri" } }, + }, + { + name: "PAYLOAD_SECRET", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "PAYLOAD_SECRET" } }, + }, + { + name: "S3_BUCKET", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "S3_BUCKET" } }, + }, + { + name: "S3_ACCESS_KEY_ID", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "S3_ACCESS_KEY_ID" } }, + }, + { + name: "S3_SECRET_ACCESS_KEY", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "S3_SECRET_ACCESS_KEY" } }, + }, + { + name: "S3_REGION", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "S3_REGION" } }, + }, + { + name: "S3_ENDPOINT", + valueFrom: { secretKeyRef: { name: "ehk-secrets", key: "S3_ENDPOINT" } }, + }, + ], + envFrom: [{ configMapRef: { name: "ehk-config" } }], + startupProbe: { + tcpSocket: { port: kube.IntOrString.fromString("http") }, + periodSeconds: 5, + failureThreshold: 60, + }, + readinessProbe: { + tcpSocket: { port: kube.IntOrString.fromString("http") }, + periodSeconds: 10, + timeoutSeconds: 3, + failureThreshold: 3, + }, + livenessProbe: { + tcpSocket: { port: kube.IntOrString.fromString("http") }, + periodSeconds: 20, + timeoutSeconds: 3, + failureThreshold: 6, + }, + resources: { + limits: { + cpu: kube.Quantity.fromString("1000m"), + memory: kube.Quantity.fromString("1Gi"), + "ephemeral-storage": kube.Quantity.fromString("500Mi"), + }, + requests: { + cpu: kube.Quantity.fromString("100m"), + memory: kube.Quantity.fromString("256Mi"), + "ephemeral-storage": kube.Quantity.fromString("0"), + }, + }, + }, + ], + restartPolicy: "Always", + }, + }, + }, + }); + + new kube.KubeIngress(scope, "ehk-ingress", { + metadata: { + name: "ehk", + labels, + annotations: { + "cert-manager.io/cluster-issuer": "letsencrypt", + "acme.cert-manager.io/http01-ingress-class": "traefik", + }, + }, + spec: { + ingressClassName: "traefik", + tls: [{ hosts: ["ehk.kir-dev.hu"], secretName: "ehk-tls-cert" }], + rules: [ + { + host: "ehk.kir-dev.hu", + http: { + paths: [ + { + path: "/", + pathType: "Prefix", + backend: { service: { name: "ehk", port: { name: "http" } } }, + }, + ], + }, + }, + ], + }, + }); +}); diff --git a/ehk/renovate.ts b/ehk/renovate.ts new file mode 100644 index 0000000..1b0d11a --- /dev/null +++ b/ehk/renovate.ts @@ -0,0 +1,3 @@ +import { appConfig } from "../.dev/renovate-config.ts"; + +export default appConfig("ehk"); diff --git a/ehk/versions.ts b/ehk/versions.ts new file mode 100644 index 0000000..1ebc099 --- /dev/null +++ b/ehk/versions.ts @@ -0,0 +1,5 @@ +export const versions = { + image: "ghcr.io/kir-dev/ehk:local", + minio: "quay.io/minio/minio:latest", + minioClient: "quay.io/minio/mc:latest", +}; diff --git a/loki/values.yaml b/loki/values.yaml index 07ba697..0e248df 100644 --- a/loki/values.yaml +++ b/loki/values.yaml @@ -93,7 +93,7 @@ minio: persistence: storageClass: memory-hdd gateway: - affinity: {} # allow scheduling 2 gateways on the same node, needed when updating + affinity: { } # allow scheduling 2 gateways on the same node, needed when updating resources: requests: cpu: 100m diff --git a/package.json b/package.json new file mode 100644 index 0000000..13e26d3 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "scripts": { + "cdk8s:import": "bun .dev/cdk8s-import.ts", + "cdk8s:synth": "bun .dev/cdk8s-synth.ts", + "local-cluster:up": "bun .dev/local-cluster.ts up", + "local-cluster:sync": "bun .dev/local-cluster.ts sync", + "local-cluster:down": "bun .dev/local-cluster.ts down", + "bootstrap-prod": "KIRDEV_ENVIRONMENT=Production KIRDEV_K8S_REPO_URL=https://github.com/kir-dev/k8s bun .dev/bootstrap-prod.ts", + "renovate": "bun .dev/renovate.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "cdk8s": "2.70.93", + "cdk8s-cli": "2.207.53", + "constructs": "10.8.1", + "yaml": "2.9.0" + }, + "devDependencies": { + "@types/bun": "1.3.14", + "@types/node": "22.20.4", + "prettier": "3.9.8" + }, + "prettier": { + "trailingComma": "all" + } +} diff --git a/place/place.yaml b/place/place.yaml index 9d62b71..30fdf6b 100644 --- a/place/place.yaml +++ b/place/place.yaml @@ -1,4 +1,3 @@ - # https://github.com/kir-dev/blace # # https://place.kir-dev.hu diff --git a/sprint-review-ha5kfu/README.md b/sprint-review-ha5kfu/README.md index 14549e7..06a8c6b 100644 --- a/sprint-review-ha5kfu/README.md +++ b/sprint-review-ha5kfu/README.md @@ -1,25 +1,24 @@ # Sprint Review – HA5KFU instance -This directory defines an isolated Sprint Review App v2 instance. ArgoCD's -top-level `ApplicationSet` discovers it as the `sprint-review-ha5kfu` +This directory defines an isolated Sprint Review App v2 instance. ArgoCD's top-level `ApplicationSet` discovers it as +the `sprint-review-ha5kfu` application and renders the listed resources through Kustomize. ## Instance values -| Setting | Current value | -| --- | --- | -| Namespace / instance label | `sprint-review-ha5kfu` | -| Frontend host | `ha5kfu.sprint-review.kir-dev.hu` | -| Backend host | `api.ha5kfu.sprint-review.kir-dev.hu` | -| Internal backend URL | `http://sprint-review-ha5kfu-backend` | -| AuthSCH callback | `https://api.ha5kfu.sprint-review.kir-dev.hu/auth/callback` | -| Database cluster | `sprint-review-ha5kfu-db` | -| Harbor pull Secret | `harbor-secret` | +| Setting | Current value | +|----------------------------|-------------------------------------------------------------| +| Namespace / instance label | `sprint-review-ha5kfu` | +| Frontend host | `ha5kfu.sprint-review.kir-dev.hu` | +| Backend host | `api.ha5kfu.sprint-review.kir-dev.hu` | +| Internal backend URL | `http://sprint-review-ha5kfu-backend` | +| AuthSCH callback | `https://api.ha5kfu.sprint-review.kir-dev.hu/auth/callback` | +| Database cluster | `sprint-review-ha5kfu-db` | +| Harbor pull Secret | `harbor-secret` | ## ArgoCD ordering and migrations -All resources belong to one ArgoCD Application, including the CNPG database. -The sync waves enforce this order: +All resources belong to one ArgoCD Application, including the CNPG database. The sync waves enforce this order: 1. namespace (`-30`); 2. CNPG cluster (`-20`); @@ -27,63 +26,53 @@ The sync waves enforce this order: 4. application Deployments, Services and Ingresses (`0`). The migration is a blocking ArgoCD `Sync` hook instead of a literal `PreSync` -hook. This is intentional: on the first deployment, a `PreSync` Job would run -before the CNPG resource exists and would deadlock the sync. ArgoCD waits for -the CNPG cluster to become healthy, then runs: +hook. This is intentional: on the first deployment, a `PreSync` Job would run before the CNPG resource exists and would +deadlock the sync. ArgoCD waits for the CNPG cluster to become healthy, then runs: ```text yarn workspace backend prisma:migrate:deploy ``` -The backend Deployment cannot roll out if this Job fails. The backend runtime -image must therefore contain `apps/backend/prisma`, all migrations, the Prisma -CLI/runtime, and the generated client. The migration and backend Deployment -image tags must always be identical. An init container additionally waits for -the CNPG read-write Service to accept connections, so the migration does not -depend on ArgoCD having a custom CNPG health assessment. +The backend Deployment cannot roll out if this Job fails. The backend runtime image must therefore contain +`apps/backend/prisma`, all migrations, the Prisma CLI/runtime, and the generated client. The migration and backend +Deployment image tags must always be identical. An init container additionally waits for the CNPG read-write Service to +accept connections, so the migration does not depend on ArgoCD having a custom CNPG health assessment. ## Externally managed Secrets Following the existing StartSCH pattern, ArgoCD creates the metadata-only -`sprint-review-ha5kfu-backend-secrets` resource, while its values are populated -outside Git with exactly: +`sprint-review-ha5kfu-backend-secrets` resource, while its values are populated outside Git with exactly: - `AUTHSCH_CLIENT_ID`; - `AUTHSCH_CLIENT_SECRET`; - `JWT_SECRET`; - `SESSION_SECRET`. -`JWT_SECRET` and `SESSION_SECRET` must be strong, instance-specific and -different. CNPG creates `sprint-review-ha5kfu-db-app`; the backend and migration -Job read its `uri` key. The namespace-local `harbor-secret` must also exist -before image pull. +`JWT_SECRET` and `SESSION_SECRET` must be strong, instance-specific and different. CNPG creates +`sprint-review-ha5kfu-db-app`; the backend and migration Job read its `uri` key. The namespace-local `harbor-secret` +must also exist before image pull. Because the ApplicationSet uses server-side apply and the manifest omits -`.data`, ArgoCD owns the Secret metadata but not the externally populated data -fields. Kubernetes injects `secretKeyRef` environment variables when a -container starts; it does not hot-reload them in an already running process. -Therefore: +`.data`, ArgoCD owns the Secret metadata but not the externally populated data fields. Kubernetes injects `secretKeyRef` +environment variables when a container starts; it does not hot-reload them in an already running process. Therefore: - populate the backend credential Secret before the backend container starts; - create `harbor-secret` before the first private image pull; - rotate backend values by updating the Secret and then rolling the backend pod; - leave the CNPG-generated database Secret to the operator. -The cluster currently exposes no External Secrets or Sealed Secrets CRD, so -the values and the Harbor pull Secret must be created by the platform or another -out-of-band secure process. A metadata-only Docker registry Secret is invalid, -so `harbor-secret` cannot use the same placeholder pattern. Raw values must not -be committed to this repository. +The cluster currently exposes no External Secrets or Sealed Secrets CRD, so the values and the Harbor pull Secret must +be created by the platform or another out-of-band secure process. A metadata-only Docker registry Secret is invalid, so +`harbor-secret` cannot use the same placeholder pattern. Raw values must not be committed to this repository. ## CNPG backups -The repository's StartSCH deployment uses the Barman Cloud plugin with a -dedicated Backblaze B2 bucket, an `ObjectStore`, a `ScheduledBackup`, and WAL -archiving. HA5KFU must use its own bucket and credentials; it must not reuse the -StartSCH bucket. +The repository's StartSCH deployment uses the Barman Cloud plugin with a dedicated Backblaze B2 bucket, an +`ObjectStore`, a `ScheduledBackup`, and WAL archiving. HA5KFU must use its own bucket and credentials; it must not reuse +the StartSCH bucket. -`cnpg-backup-resources.example.yaml` contains the matching ObjectStore and daily -backup schedule, but it is intentionally excluded from Kustomize. To enable it: +`cnpg-backup-resources.example.yaml` contains the matching ObjectStore and daily backup schedule, but it is +intentionally excluded from Kustomize. To enable it: 1. create a dedicated S3-compatible bucket and record its exact endpoint; 2. securely create `sprint-review-ha5kfu-backups-secrets` with @@ -106,30 +95,24 @@ spec: barmanObjectName: sprint-review-ha5kfu-backups ``` -Enable the plugin only after the ObjectStore credential works. Otherwise failed -WAL uploads can accumulate on the 1.5 GiB database volume and eventually stall -PostgreSQL. After enabling it, verify ObjectStore health and run a manual Backup -before relying on the schedule. +Enable the plugin only after the ObjectStore credential works. Otherwise failed WAL uploads can accumulate on the 1.5 +GiB database volume and eventually stall PostgreSQL. After enabling it, verify ObjectStore health and run a manual +Backup before relying on the schedule. ## Network boundaries -This instance ships no `NetworkPolicy`, matching the other applications in this -repository (`startsch`, `place`). +This instance ships no `NetworkPolicy`, matching the other applications in this repository (`startsch`, `place`). -An earlier revision did define a default-deny policy set with explicit allow -rules. It could not work in this cluster: the vClusters are nested -(`-x-vc2-x-vc2-x-vc-kirdev`), and with `sync.toHost.networkPolicies` only -egress rules whose peer is a `podSelector` in the *same* namespace survive the -translation to the host cluster. Rules pointing outside the namespace are lost, -so DNS to `kube-system` was denied no matter how the rule was written — -verified with a selector peer, a peer-less port-only rule, and `ipBlock` peers -covering both the Service and Pod CIDRs. The same class of failure would have -hit the AuthSCH egress rule and the Traefik ingress rules. Reintroduce -policies only once the platform supports cross-namespace peers here. +An earlier revision did define a default-deny policy set with explicit allow rules. It could not work in this cluster: +the vClusters are nested (`-x-vc2-x-vc2-x-vc-kirdev`), and with `sync.toHost.networkPolicies` only egress rules whose +peer is a `podSelector` in the *same* namespace survive the translation to the host cluster. Rules pointing outside the +namespace are lost, so DNS to `kube-system` was denied no matter how the rule was written — verified with a selector +peer, a peer-less port-only rule, and `ipBlock` peers covering both the Service and Pod CIDRs. The same class of failure +would have hit the AuthSCH egress rule and the Traefik ingress rules. Reintroduce policies only once the platform +supports cross-namespace peers here. The backend Ingress publishes only the exact `/auth/login` and `/auth/callback` -paths. Normal API calls use the frontend's same-origin `/api/*` proxy and the -cluster-local backend Service. +paths. Normal API calls use the frontend's same-origin `/api/*` proxy and the cluster-local backend Service. ## Required before ArgoCD sync @@ -138,9 +121,8 @@ cluster-local backend Service. 3. Point both DNS names at the cluster ingress. 4. Configure the instance-specific object store before enabling CNPG backups. -Both immutable images are already digest-pinned in `kustomization.yaml` and were -successfully pulled during manifest verification. The backend image is amd64, -runs as UID 1000, uses the expected direct Node entrypoint, and contains the +Both immutable images are already digest-pinned in `kustomization.yaml` and were successfully pulled during manifest +verification. The backend image is amd64, runs as UID 1000, uses the expected direct Node entrypoint, and contains the Prisma schema, migrations, and `prisma:migrate:deploy` script. ## Verification @@ -153,6 +135,5 @@ kubectl kustomize sprint-review-ha5kfu \ | kubectl apply --dry-run=client --validate=false -f - ``` -After sync, verify frontend health, backend live/ready behavior during a database -failure, AuthSCH login/callback, `/api/auth/me`, image upload, migration failure -blocking, and denied undeclared network paths. +After sync, verify frontend health, backend live/ready behavior during a database failure, AuthSCH login/callback, +`/api/auth/me`, image upload, migration failure blocking, and denied undeclared network paths. diff --git a/sprint-review-ha5kfu/frontend-deployment.yaml b/sprint-review-ha5kfu/frontend-deployment.yaml index 98d0ef3..d285420 100644 --- a/sprint-review-ha5kfu/frontend-deployment.yaml +++ b/sprint-review-ha5kfu/frontend-deployment.yaml @@ -94,6 +94,6 @@ spec: mountPath: /tmp volumes: - name: next-cache - emptyDir: {} + emptyDir: { } - name: tmp - emptyDir: {} + emptyDir: { } diff --git a/sprint-review-ha5kfu/migration-job.yaml b/sprint-review-ha5kfu/migration-job.yaml index 8052e08..ea2b63c 100644 --- a/sprint-review-ha5kfu/migration-job.yaml +++ b/sprint-review-ha5kfu/migration-job.yaml @@ -99,4 +99,4 @@ spec: mountPath: /tmp volumes: - name: tmp - emptyDir: {} + emptyDir: { } diff --git a/startsch/app.ts b/startsch/app.ts new file mode 100644 index 0000000..0555454 --- /dev/null +++ b/startsch/app.ts @@ -0,0 +1,313 @@ +// https://github.com/kir-dev/StartSCH +// +// https://start.sch.bme.hu + +import * as kube from "../imports/k8s"; +import * as environment from "../.dev/environment.ts"; +import * as cnpg from "../imports/postgresql.cnpg.io.ts"; +import * as barman from "../imports/barmancloud.cnpg.io.ts"; +import { versions } from "./versions.ts"; +import { singletonApp } from "../.dev/cdk8s-utils.ts"; + +export default singletonApp({ namespace: "startsch", createNamespace: true }, (scope) => { + new kube.KubeConfigMap(scope, "startsch-config", { + metadata: { name: "startsch-config" }, + data: { + "Logging__LogLevel__Microsoft.AspNetCore.Authentication": "Warning", + "Logging__LogLevel__Microsoft.AspNetCore.Authorization": "Warning", + "Logging__LogLevel__Microsoft.AspNetCore.Components": "Warning", + "Logging__LogLevel__Microsoft.AspNetCore.Hosting.Diagnostics": "Warning", + "Logging__LogLevel__Microsoft.AspNetCore.Hosting": "Information", + "Logging__LogLevel__Microsoft.AspNetCore.HttpOverrides": "Warning", + "Logging__LogLevel__Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information", + "Logging__LogLevel__Microsoft.AspNetCore.Server": "Information", + "Logging__LogLevel__Microsoft.AspNetCore": "Warning", + "Logging__LogLevel__Microsoft.EntityFrameworkCore.Migrations": "Information", + "Logging__LogLevel__Microsoft.EntityFrameworkCore": "Information", + "Logging__LogLevel__Microsoft.Hosting": "Information", + Logging__LogLevel__StartSch: "Trace", + OTEL_EXPORTER_OTLP_ENDPOINT: "http://opentelemetry-collector.monitoring.svc.cluster.local:4317", + StartSch__PublicUrl: "https://start.sch.bme.hu", + StartSch__EnabledModules__All: environment.environment == "Production" ? "true" : "false", + }, + }); + + new kube.KubeSecret(scope, "startsch-secrets", { + metadata: { name: "startsch-secrets" }, + // Set manually: + // stringData: + // AuthSch__ClientId: + // AuthSch__ClientSecret: + // KirMail__ApiKey: + // Push__PrivateKey: + // Push__PublicKey: + // Push__Subject: mailto: + }); + + const labels = { + "app.kubernetes.io/name": "startsch", + "app.kubernetes.io/component": "server", + }; + + new kube.KubeService(scope, "startsch-service", { + metadata: { name: "startsch" }, + spec: { + selector: labels, + ports: [{ name: "http", port: 80, targetPort: kube.IntOrString.fromString("http") }], + }, + }); + + new kube.KubeSecret(scope, "startsch-backups-secrets", { + metadata: { name: "startsch-backups-secrets" }, + // Set manually: + // stringData: + // ACCESS_KEY_ID: + // ACCESS_SECRET_KEY: + }); + + const bootstrapMode: "initdb" | "recovery" = environment.environment == "Production" ? "recovery" : "initdb"; + // Set to false while restoring from a backup + const enableBackup = environment.environment == "Production"; + + // Configure where the backups are + if (enableBackup) { + new barman.ObjectStore(scope, "startsch-backups", { + metadata: { name: "startsch-backups" }, + spec: { + instanceSidecarConfiguration: { + resources: { + limits: { + cpu: barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesLimits.fromString("1"), + memory: barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesLimits.fromString( + "512Mi", + ), + "ephemeral-storage": + barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesLimits.fromString("500Mi"), + }, + requests: { + cpu: barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesRequests.fromString("100m"), + memory: barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesRequests.fromString( + "128Mi", + ), + "ephemeral-storage": + barman.ObjectStoreSpecInstanceSidecarConfigurationResourcesRequests.fromString("100Mi"), + }, + }, + }, + configuration: { + destinationPath: "s3://startsch-backups/", + endpointUrl: "https://s3.eu-central-003.backblazeb2.com", + s3Credentials: { + accessKeyId: { name: "startsch-backups-secrets", key: "ACCESS_KEY_ID" }, + secretAccessKey: { name: "startsch-backups-secrets", key: "ACCESS_SECRET_KEY" }, + }, + wal: { + compression: barman.ObjectStoreSpecConfigurationWalCompression.GZIP, + maxParallel: 8, + }, + }, + }, + }); + } + + const clusterProps: cnpg.ClusterProps = { + metadata: { + name: "startsch-db", + labels: { + "app.kubernetes.io/name": "postgres", + "app.kubernetes.io/instance": "postgres-startsch", + "app.kubernetes.io/component": "database", + "app.kubernetes.io/part-of": "startsch", + }, + }, + spec: { + primaryUpdateStrategy: cnpg.ClusterSpecPrimaryUpdateStrategy.UNSUPERVISED, + primaryUpdateMethod: cnpg.ClusterSpecPrimaryUpdateMethod.SWITCHOVER, + instances: 2, + imageName: "ghcr.io/cloudnative-pg/postgresql:17.5", + imagePullPolicy: "Always", + monitoring: { enablePodMonitor: true }, + postgresql: { + parameters: { + wal_level: "replica", + shared_buffers: "128MB", + // only force wal archiving every 20 minutes if the current segment is not full + // (only 2500 requests/day are free in backblaze, the original value of 5 minutes ran out of requests just before the reset) + archive_timeout: "1200", + }, + }, + resources: { + limits: { + cpu: cnpg.ClusterSpecResourcesLimits.fromString("500m"), + memory: cnpg.ClusterSpecResourcesLimits.fromString("512Mi"), + "ephemeral-storage": cnpg.ClusterSpecResourcesLimits.fromString("500Mi"), + }, + requests: { + cpu: cnpg.ClusterSpecResourcesRequests.fromString("100m"), + memory: cnpg.ClusterSpecResourcesRequests.fromString("128Mi"), + "ephemeral-storage": cnpg.ClusterSpecResourcesRequests.fromString("100Mi"), + }, + }, + storage: { + size: "1.5Gi", + storageClass: "node-local-zfs", + }, + }, + }; + + if (environment.environment == "Production") { + (clusterProps.spec.externalClusters ??= []).push( + { + name: "backblaze-backup", + plugin: { + name: "barman-cloud.cloudnative-pg.io", + enabled: true, // needed otherwise ArgoCD complains about being OutOfSync + isWalArchiver: false, // needed otherwise ArgoCD complains about being OutOfSync + parameters: { barmanObjectName: "startsch-backups", serverName: "startsch-db" }, + }, + }, + ); + } + + switch (bootstrapMode) { + case "initdb": + clusterProps.spec.bootstrap = { + initdb: { + database: "startsch", + owner: "startsch", + }, + }; + break; + case "recovery": + clusterProps.spec.bootstrap = + { + recovery: { + source: "backblaze-backup", + database: "startsch", + owner: "startsch", + }, + }; + break; + default: + throw new Error(); + } + + if (enableBackup) { + (clusterProps.spec.plugins ??= []).push( + { + name: "barman-cloud.cloudnative-pg.io", + enabled: true, // needed otherwise ArgoCD complains + isWalArchiver: true, + parameters: { barmanObjectName: "startsch-backups" }, + }, + ); + } + + new cnpg.Cluster(scope, "startsch-db", clusterProps); + + if (enableBackup) { + new cnpg.ScheduledBackup(scope, "startsch-backup", { + metadata: { name: "startsch-backup" }, + spec: { + cluster: { name: "startsch-db" }, + schedule: "0 24 3 * * *", // At 3:24 every day + backupOwnerReference: cnpg.ScheduledBackupSpecBackupOwnerReference.SELF, + method: cnpg.ScheduledBackupSpecMethod.PLUGIN, + pluginConfiguration: { name: "barman-cloud.cloudnative-pg.io" }, + }, + }); + } + + new kube.KubeDeployment(scope, "startsch-deployment", { + metadata: { name: "startsch", labels }, + spec: { + replicas: 1, + selector: { matchLabels: labels }, + template: { + metadata: { labels }, + spec: { + containers: [ + { + name: "startsch", + image: versions.image, + imagePullPolicy: "Always", + ports: [{ containerPort: 8080, protocol: "TCP", name: "http" }], + env: [ + { + name: "DBHOST", + valueFrom: { secretKeyRef: { name: "startsch-db-app", key: "host" } }, + }, + { + name: "DBNAME", + valueFrom: { secretKeyRef: { name: "startsch-db-app", key: "dbname" } }, + }, + { + name: "DBUSER", + valueFrom: { secretKeyRef: { name: "startsch-db-app", key: "user" } }, + }, + { + name: "DBPASSWORD", + valueFrom: { secretKeyRef: { name: "startsch-db-app", key: "password" } }, + }, + { + name: "ConnectionStrings__Postgres", + value: "Host=$(DBHOST); Database=$(DBNAME); Username=$(DBUSER); Password=$(DBPASSWORD);", + }, + ], + envFrom: [ + { configMapRef: { name: "startsch-config" } }, + { secretRef: { name: "startsch-secrets" } }, + ], + resources: { + limits: { + cpu: kube.Quantity.fromString("500m"), + memory: kube.Quantity.fromString("500Mi"), + "ephemeral-storage": kube.Quantity.fromString("200Mi"), + }, + requests: { + cpu: kube.Quantity.fromString("100m"), + memory: kube.Quantity.fromString("256Mi"), + "ephemeral-storage": kube.Quantity.fromString("0"), + }, + }, + }, + ], + restartPolicy: "Always", + }, + }, + }, + }); + + new kube.KubeIngress(scope, "startsch-ingress", { + metadata: { + name: "startsch", + annotations: { + "cert-manager.io/cluster-issuer": "letsencrypt", + "acme.cert-manager.io/http01-ingress-class": "traefik", + }, + }, + spec: { + ingressClassName: "traefik", + tls: [{ hosts: ["start.sch.bme.hu"], secretName: "startsch-tls-cert" }], + rules: [ + { + host: "start.sch.bme.hu", + http: { + paths: [ + { + path: "/", + pathType: "Prefix", + backend: { + service: { + name: "startsch", + port: { name: "http" }, + }, + }, + }, + ], + }, + }, + ], + }, + }); +}); diff --git a/startsch/renovate.ts b/startsch/renovate.ts new file mode 100644 index 0000000..ed02a2a --- /dev/null +++ b/startsch/renovate.ts @@ -0,0 +1,3 @@ +import { appConfig } from "../.dev/renovate-config.ts"; + +export default appConfig("startsch"); diff --git a/startsch/startsch.yaml b/startsch/startsch.yaml deleted file mode 100644 index 6827399..0000000 --- a/startsch/startsch.yaml +++ /dev/null @@ -1,292 +0,0 @@ - -# https://github.com/kir-dev/StartSCH -# -# https://start.sch.bme.hu - -kind: Namespace -apiVersion: v1 -metadata: - name: startsch - ---- - -kind: ConfigMap -apiVersion: v1 -metadata: - namespace: startsch - name: startsch-config -data: - Logging__LogLevel__Microsoft.AspNetCore.Authentication: Warning - Logging__LogLevel__Microsoft.AspNetCore.Authorization: Warning - Logging__LogLevel__Microsoft.AspNetCore.Components: Warning - Logging__LogLevel__Microsoft.AspNetCore.Hosting.Diagnostics: Warning - Logging__LogLevel__Microsoft.AspNetCore.Hosting: Information - Logging__LogLevel__Microsoft.AspNetCore.HttpOverrides: Warning - Logging__LogLevel__Microsoft.AspNetCore.Routing.EndpointMiddleware: Information - Logging__LogLevel__Microsoft.AspNetCore.Server: Information - Logging__LogLevel__Microsoft.AspNetCore: Warning - Logging__LogLevel__Microsoft.EntityFrameworkCore.Migrations: Information - Logging__LogLevel__Microsoft.EntityFrameworkCore: Information - Logging__LogLevel__Microsoft.Hosting: Information - Logging__LogLevel__StartSch: Trace - OTEL_EXPORTER_OTLP_ENDPOINT: http://opentelemetry-collector.monitoring.svc.cluster.local:4317 - StartSch__PublicUrl: https://start.sch.bme.hu - StartSch__EnabledModules__All: "true" - ---- - -kind: Secret -apiVersion: v1 -metadata: - namespace: startsch - name: startsch-secrets -# Set manually -#stringData: -# AuthSch__ClientId: -# AuthSch__ClientSecret: -# KirMail__ApiKey: -# Push__PrivateKey: -# Push__PublicKey: -# Push__Subject: mailto: - ---- - -kind: Service -apiVersion: v1 -metadata: - namespace: startsch - name: startsch -spec: - selector: - app.kubernetes.io/name: startsch - app.kubernetes.io/component: server - ports: - - name: http - port: 80 - targetPort: http - ---- - -kind: Secret -apiVersion: v1 -metadata: - namespace: startsch - name: startsch-backups-secrets - ---- - -kind: ObjectStore -apiVersion: barmancloud.cnpg.io/v1 -metadata: - name: startsch-backups - namespace: startsch -spec: - instanceSidecarConfiguration: - resources: - limits: - cpu: '1' - memory: 512Mi - ephemeral-storage: 500Mi - requests: - cpu: 100m - memory: 128Mi - ephemeral-storage: 100Mi - configuration: - destinationPath: s3://startsch-backups/ - endpointURL: https://s3.eu-central-003.backblazeb2.com - s3Credentials: - accessKeyId: - name: startsch-backups-secrets - key: ACCESS_KEY_ID - secretAccessKey: - name: startsch-backups-secrets - key: ACCESS_SECRET_KEY - wal: - compression: gzip - maxParallel: 8 - ---- - -kind: Cluster -apiVersion: postgresql.cnpg.io/v1 -metadata: - namespace: startsch - name: startsch-db - labels: - app.kubernetes.io/name: postgres - app.kubernetes.io/instance: postgres-startsch - app.kubernetes.io/component: database - app.kubernetes.io/part-of: startsch - annotations: { } -spec: - primaryUpdateStrategy: unsupervised - primaryUpdateMethod: switchover - instances: 2 - imageName: ghcr.io/cloudnative-pg/postgresql:17.5 - imagePullPolicy: Always - monitoring: - enablePodMonitor: true - postgresql: - parameters: - wal_level: replica - shared_buffers: 128MB - # only force wal archiving every 20 minutes if the current segment is not full - # (only 2500 requests/day are free in backblaze, the original value of 5 minutes ran out of requests just before the reset) - archive_timeout: '1200' - resources: - limits: - cpu: 500m - memory: 512Mi - ephemeral-storage: 500Mi - requests: - cpu: 100m - memory: 128Mi - ephemeral-storage: 100Mi - storage: - size: 1.5Gi - storageClass: node-local-zfs - - # Enable automatic backups (disable while restoring) - plugins: - - name: barman-cloud.cloudnative-pg.io - enabled: true # needed otherwise ArgoCD complains - isWALArchiver: true - parameters: - barmanObjectName: startsch-backups - - # Create a new database -# bootstrap: -# initdb: -# database: startsch -# owner: startsch - - # Restore from a backup (leave after restoring finished) - bootstrap: - recovery: - source: backblaze-backup - database: startsch - owner: startsch - externalClusters: - - name: backblaze-backup - plugin: - name: barman-cloud.cloudnative-pg.io - enabled: true # needed otherwise ArgoCD complains about being OutOfSync - isWALArchiver: false # needed otherwise ArgoCD complains about being OutOfSync - parameters: - barmanObjectName: startsch-backups - serverName: startsch-db - ---- - -kind: ScheduledBackup -apiVersion: postgresql.cnpg.io/v1 -metadata: - name: startsch-backup - namespace: startsch -spec: - cluster: - name: startsch-db - schedule: "0 24 3 * * *" # At 3:24 every day - backupOwnerReference: self - method: plugin - pluginConfiguration: - name: barman-cloud.cloudnative-pg.io - ---- - -kind: Deployment -apiVersion: apps/v1 -metadata: - namespace: startsch - name: startsch - labels: - app.kubernetes.io/name: startsch - app.kubernetes.io/component: server -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: startsch - app.kubernetes.io/component: server - template: - metadata: - namespace: startsch - name: startsch - labels: - app.kubernetes.io/name: startsch - app.kubernetes.io/component: server - spec: - containers: - - name: startsch - image: ghcr.io/kir-dev/startsch:v0.0.131@sha256:ac9752f26d003fb1029e13d6338f5367f0ea45440a52176ba2341ed038620334 - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - name: http - env: - - name: DBHOST - valueFrom: - secretKeyRef: - name: startsch-db-app - key: host - - name: DBNAME - valueFrom: - secretKeyRef: - name: startsch-db-app - key: dbname - - name: DBUSER - valueFrom: - secretKeyRef: - name: startsch-db-app - key: user - - name: DBPASSWORD - valueFrom: - secretKeyRef: - name: startsch-db-app - key: password - - name: ConnectionStrings__Postgres - value: "Host=$(DBHOST); Database=$(DBNAME); Username=$(DBUSER); Password=$(DBPASSWORD);" - envFrom: - - configMapRef: - name: startsch-config - - secretRef: - name: startsch-secrets - resources: - limits: - cpu: 500m - memory: 500Mi - ephemeral-storage: 200Mi - requests: - cpu: 100m - memory: 256Mi - ephemeral-storage: 0 - restartPolicy: Always - ---- - -kind: Ingress -apiVersion: networking.k8s.io/v1 -metadata: - namespace: startsch - name: startsch - annotations: - cert-manager.io/cluster-issuer: letsencrypt - acme.cert-manager.io/http01-ingress-class: traefik -spec: - ingressClassName: traefik - tls: - - hosts: - - start.sch.bme.hu - secretName: startsch-tls-cert - rules: - - host: start.sch.bme.hu - http: - paths: - - path: "/" - pathType: Prefix - backend: - service: - name: startsch - port: - name: http diff --git a/startsch/versions.ts b/startsch/versions.ts new file mode 100644 index 0000000..8e02e6b --- /dev/null +++ b/startsch/versions.ts @@ -0,0 +1,3 @@ +export const versions = { + image: "ghcr.io/kir-dev/startsch:v0.0.131@sha256:ac9752f26d003fb1029e13d6338f5367f0ea45440a52176ba2341ed038620334", +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d75cb16 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "types": ["bun"], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["**/*.ts", ".dev/**/*.ts"], + "exclude": ["node_modules", "dist"] +}