feat: add garage tooling
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user