feat: add garage tooling

This commit is contained in:
2026-08-30 18:51:19 +07:00 Unverified
parent 716246659c
commit 7f1d445981
7 changed files with 139 additions and 5 deletions
+18
View File
@@ -67,6 +67,9 @@ bun run index.ts start
bun run index.ts stop
bun run index.ts restart
bun run index.ts db ls
bun run index.ts s3 ls
bun run index.ts s3 creds app
bun run index.ts s3 ui app
```
All commands accept `--config` to use a configuration file other than
@@ -102,6 +105,9 @@ For a permanent setup, write the generated script to a file and source it from y
- `exec <deployment> <command...>`: execute a command inside a running deployment pod
- `db ls`: list managed Postgres claims declared in the current Compose file
- `db creds <service>`: print the generated connection details for a managed Postgres claim
- `s3 ls`: list managed S3 claims declared in the current Compose file
- `s3 creds <service>`: print all generated S3 environment variables for a service
- `s3 ui <service>`: print the Garage UI object-browser URL for a service bucket
## Configuration
@@ -255,6 +261,18 @@ The Garage operator generates the credentials. `kuber` reads its generated Secre
One service can declare both `postgresql:...` and `s3:...`; all generated values are merged into the same service Secret. Managed Garage buckets and keys are retained by normal `down` and deleted by `down -f`.
Inspect a claim, print its generated credentials, or get its Garage UI URL:
```bash
kuber s3 ls
kuber s3 creds app
kuber s3 ui app
```
`s3 creds` prints `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
`AWS_ENDPOINT_URL_S3`, `AWS_REGION`, and `S3_BUCKET` as shell-style environment
assignments. `s3 ui` only prints the URL; it does not open a browser.
### Environment Files
`env_file` entries are read locally and turned into a Kubernetes `Secret` named `<service>-env`. Deployments then consume that secret through `envFrom`.
+3 -1
View File
@@ -7,6 +7,7 @@ import { exportCommand } from "./export";
import { logs } from "./logs";
import { ps } from "./ps";
import { restart } from "./restart";
import { s3 } from "./s3";
import { start } from "./start";
import { stop } from "./stop";
import { up } from "./up";
@@ -14,7 +15,7 @@ import { up } from "./up";
export const main = defineCommand({
meta: {
name: "kuber",
version: "1.1.0",
version: "1.2.0",
description: "Docker Compose -> K8s translation layer",
},
args: {
@@ -31,6 +32,7 @@ export const main = defineCommand({
logs,
ps,
restart,
s3,
start,
stop,
up,
+89
View File
@@ -0,0 +1,89 @@
import { defineCommand } from "citty";
import { ctx } from "../lib/context";
import { toTable } from "../lib/format";
import {
getComposeS3Claims,
getS3Credentials,
type S3Claim,
} from "../lib/storage";
const GARAGE_UI_URL = "http://garage-ui.garage-system.svc.cluster.local";
export function getS3UiUrl(bucket: string): string {
return `${GARAGE_UI_URL}/buckets/${encodeURIComponent(bucket)}/objects`;
}
async function getServiceClaim(service: string): Promise<S3Claim> {
const claim = getComposeS3Claims(await ctx().compose()).find(
(entry) => entry.service === service,
);
if (!claim) {
throw new Error(`Service ${service} does not declare a managed S3 volume.`);
}
return claim;
}
const list = defineCommand({
meta: {
name: "ls",
description: "List managed S3 claims",
},
async run() {
const claims = getComposeS3Claims(await ctx().compose());
if (claims.length === 0) {
console.log("No managed S3 volumes declared");
return;
}
console.log(
toTable(
claims.map((claim) => ({
service: claim.service,
key: claim.key,
bucket: claim.bucket,
})),
),
);
},
});
const creds = defineCommand({
meta: {
name: "creds",
description: "Print S3 environment variables for a service",
},
async run({ args }) {
const service = args._[0];
if (!service) throw new Error("Service name is required");
const credentials = await getS3Credentials(await getServiceClaim(service));
for (const [name, value] of Object.entries(credentials)) {
console.log(`${name}=${value}`);
}
},
});
const ui = defineCommand({
meta: {
name: "ui",
description: "Print the Garage UI URL for a service bucket",
},
async run({ args }) {
const service = args._[0];
if (!service) throw new Error("Service name is required");
console.log(getS3UiUrl((await getServiceClaim(service)).bucket));
},
});
export const s3 = defineCommand({
meta: {
name: "s3",
description: "Inspect managed S3 storage",
},
subCommands: {
creds,
ls: list,
ui,
},
});
+2 -2
View File
@@ -225,7 +225,7 @@ async function reconcileKeys(
}
}
async function readKeyEnvironment(
export async function getS3Credentials(
claim: S3Claim,
): Promise<Record<string, string>> {
const deadline = Date.now() + SECRET_WAIT_TIMEOUT_MS;
@@ -297,7 +297,7 @@ export async function reconcileS3Claims(
const environmentsByKey = new Map<string, Record<string, string>>();
for (const claim of claims) {
if (environmentsByKey.has(claim.key)) continue;
environmentsByKey.set(claim.key, await readKeyEnvironment(claim));
environmentsByKey.set(claim.key, await getS3Credentials(claim));
}
return Object.fromEntries(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@dmgnr/kuber",
"version": "1.1.0",
"version": "1.2.0",
"description": "Docker Compose to Kubernetes translation layer",
"bin": {
"kuber": "dist/index.js"
+19 -1
View File
@@ -7,6 +7,7 @@ import { logs } from "../../command/logs";
import { initializeCompletion, main } from "../../command/main";
import { ps } from "../../command/ps";
import { restart } from "../../command/restart";
import { s3 } from "../../command/s3";
import { start } from "../../command/start";
import { stop } from "../../command/stop";
import { up } from "../../command/up";
@@ -24,6 +25,7 @@ describe("CLI command definitions", () => {
["start", "Start deployments without rebuilding images", start],
["stop", "Scale managed deployments to zero", stop],
["restart", "Roll out a restart for managed deployments", restart],
["s3", "Inspect managed S3 storage", s3],
["down", "Delete managed resources except ingress and PVCs", down],
["ps", "List deployments", ps],
["logs", "Show deployment logs", logs],
@@ -37,7 +39,8 @@ describe("CLI command definitions", () => {
(name, description, command) => {
const definition = command as Command;
expect(definition.meta).toEqual({ name, description });
if (name !== "db") expect(typeof definition.run).toBe("function");
if (name !== "db" && name !== "s3")
expect(typeof definition.run).toBe("function");
},
);
@@ -77,6 +80,21 @@ describe("CLI command definitions", () => {
expect(typeof subCommands?.creds?.run).toBe("function");
});
test("defines S3 inspection subcommands", () => {
const subCommands = (s3 as Command).subCommands;
expect(Object.keys(subCommands ?? {}).sort()).toEqual([
"creds",
"ls",
"ui",
]);
expect(subCommands?.ls?.meta?.name).toBe("ls");
expect(subCommands?.creds?.meta?.name).toBe("creds");
expect(subCommands?.ui?.meta?.name).toBe("ui");
expect(typeof subCommands?.ls?.run).toBe("function");
expect(typeof subCommands?.creds?.run).toBe("function");
expect(typeof subCommands?.ui?.run).toBe("function");
});
test("registers shell completion on the root command", async () => {
await initializeCompletion();
expect((await Promise.resolve(main.meta))?.name).toBe("kuber");
+7
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import type { ComposeSpecification, Service } from "../../schema/docker.d";
import { serviceToDeployment, volumesToPvc } from "../../lib/convert";
import { getS3UiUrl } from "../../command/s3";
import {
getComposeS3Claims,
getServiceS3Claim,
@@ -89,4 +90,10 @@ describe("managed S3 claims", () => {
{ secretRef: { name: "app-env" } },
]);
});
test("builds a Garage UI object URL for a bucket", () => {
expect(getS3UiUrl("shared assets")).toBe(
"http://garage-ui.garage-system.svc.cluster.local/buckets/shared%20assets/objects",
);
});
});