Skip to content

Commit 250c247

Browse files
update apps
1 parent 840698b commit 250c247

7 files changed

Lines changed: 184 additions & 72 deletions

File tree

packages/cli/src/commands/apps/APPS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ For compose files that have just one service, the import overlaps with the Docke
9292
| `--dockerfile` | Build from a Dockerfile, then deploy. Pass a path or use the bare flag for `./Dockerfile`. |
9393
| `--context` | Docker build context directory. Defaults to the directory of the Dockerfile. |
9494
| `--tag` | Override the auto-generated `<sha>-<timestamp>` image tag. |
95-
| `--registry` | bunny.net registry ID to push to. Overrides the value stored in `bunny.jsonc`. |
95+
| `--registry` | Registry ID to push to, or `bunny` for the bunny.net registry (requires `BUNNYNET_REGISTRY_URL`). Overrides `bunny.jsonc`. |
9696
| `--container` | Name of the container to update. Required when `bunny.jsonc` has multiple containers and you pass `<image>`/`--dockerfile`. |
9797
| `--port` | Override the container port. Retargets any endpoints written to `bunny.jsonc`. |
9898
| `--command` | Override the container `CMD`. Passed as a single string, split on whitespace. |

packages/cli/src/commands/apps/deploy.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { dirname, isAbsolute, resolve } from "node:path";
33
import type { RegistryMap } from "@bunny.net/app-config";
44
import { createMcClient } from "@bunny.net/openapi-client";
55
import { resolveConfig } from "../../config/index.ts";
6+
import {
7+
REGISTRY_URL_ENV,
8+
REGISTRY_USERNAME,
9+
tryResolveRegistryEndpoint,
10+
} from "../../core/bunny-registry.ts";
611
import { clientOptions } from "../../core/client-options.ts";
712
import { bunny } from "../../core/colors.ts";
813
import { defineCommand } from "../../core/define-command.ts";
@@ -124,6 +129,7 @@ async function applyPostPushSuggestions(
124129
}
125130

126131
import {
132+
BUNNY_REGISTRY_ID,
127133
buildImage,
128134
buildImageRef,
129135
dockerLogin,
@@ -207,7 +213,8 @@ export const appsDeployCommand = defineCommand<DeployArgs>({
207213
})
208214
.option("registry", {
209215
type: "string",
210-
describe: "bunny.net registry ID to push to (overrides bunny.jsonc)",
216+
describe:
217+
'Registry ID to push to, or "bunny" for the bunny.net registry (overrides bunny.jsonc)',
211218
})
212219
.option("container", {
213220
type: "string",
@@ -459,17 +466,98 @@ export const appsDeployCommand = defineCommand<DeployArgs>({
459466
if (mode.kind === "build") {
460467
assertDockerfileExists(mode.dockerfile, targetName);
461468

469+
const bunnyEndpoint = tryResolveRegistryEndpoint();
470+
// `--registry bunny` is a friendly alias for the internal sentinel.
471+
if (registryId === "bunny") registryId = BUNNY_REGISTRY_ID;
472+
462473
// Ensure a registry is selected before we build (we need its hostname).
463474
if (!registryId) {
464-
const resolved = await promptRegistry(client);
475+
const resolved = await promptRegistry(client, {
476+
bunnyEndpoint: bunnyEndpoint ?? undefined,
477+
});
465478
if (!resolved) {
466479
throw new UserError(
467480
"A registry is required to build and push images.",
468481
);
469482
}
470483
registryId = resolved.id;
471484
freshCreds = resolved.freshCredentials;
472-
setContainerRegistry(targetName, registryId);
485+
// TODO: The bunny registry has no account record — don't persist a sentinel AT THE MOMENT
486+
if (!resolved.bunny) setContainerRegistry(targetName, registryId);
487+
}
488+
489+
// TODO: bunny.net registry (env stub): build + push with the API token, then skip deploy — MC can't pull from it until `/registries` exposes a `bunny` record we can deploy by id.
490+
if (registryId === BUNNY_REGISTRY_ID) {
491+
if (!bunnyEndpoint) {
492+
throw new UserError(
493+
"The bunny.net registry endpoint is not configured.",
494+
`Set ${REGISTRY_URL_ENV} to the registry URL and try again.`,
495+
);
496+
}
497+
if (!cfg.apiKey) {
498+
throw new UserError(
499+
"Not logged in.",
500+
'Run "bunny login" to authenticate.',
501+
);
502+
}
503+
504+
const tag = args.tag ?? (await generateTag());
505+
const imageRef = buildImageRef(
506+
bunnyEndpoint.host,
507+
undefined,
508+
toml.app.name,
509+
tag,
510+
);
511+
const buildCwd = resolveBuildContext(mode.dockerfile, args.context);
512+
513+
logger.info(`Building ${imageRef}...`);
514+
await buildImage(mode.dockerfile, imageRef, buildCwd);
515+
516+
if (noPush) {
517+
logger.success(`Image built: ${imageRef}`);
518+
logger.dim("Skipping push and deploy (--no-push).");
519+
if (output === "json") {
520+
logger.log(
521+
JSON.stringify({
522+
built: true,
523+
image: imageRef,
524+
pushed: false,
525+
deployed: false,
526+
}),
527+
);
528+
}
529+
return;
530+
}
531+
532+
const loginSpin = spinner(`Logging in to ${bunnyEndpoint.host}...`);
533+
loginSpin.start();
534+
try {
535+
await dockerLogin(bunnyEndpoint.host, REGISTRY_USERNAME, cfg.apiKey);
536+
} finally {
537+
loginSpin.stop();
538+
}
539+
540+
logger.info(`Pushing ${imageRef}...`);
541+
await pushImage(imageRef);
542+
543+
if (output === "json") {
544+
logger.log(
545+
JSON.stringify({
546+
built: true,
547+
image: imageRef,
548+
pushed: true,
549+
deployed: false,
550+
reason: "mc-pull-unsupported",
551+
}),
552+
);
553+
return;
554+
}
555+
556+
logger.success(`Pushed ${imageRef}`);
557+
logger.warn(
558+
"Magic Containers can't deploy from the bunny.net registry yet — image pushed, deploy skipped.",
559+
);
560+
return;
473561
}
474562

475563
const regSpin = spinner("Fetching registry...");

packages/cli/src/commands/apps/docker.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { UserError } from "../../core/errors.ts";
99
import { logger } from "../../core/logger.ts";
1010
import { spinner } from "../../core/ui.ts";
1111

12-
// Generic Docker primitives live in core/docker.ts; re-exported here so
13-
// existing app-command imports keep resolving from this module.
1412
export {
1513
dockerLogin,
1614
ensureDockerAvailable,
@@ -561,6 +559,9 @@ export function buildImageRef(
561559

562560
const ADD_NEW_REGISTRY = "__add_new__";
563561

562+
/** Stand-in id for the bunny.net registry; swap for the real id once `/registries` returns it as a `bunny` record. */
563+
export const BUNNY_REGISTRY_ID = "bunny";
564+
564565
/**
565566
* Result of resolving a registry — the ID plus, if the user just entered
566567
* credentials in this session, those credentials so the caller can run
@@ -673,6 +674,7 @@ export async function createRegistry(
673674
*/
674675
export async function promptRegistry(
675676
client: McClient,
677+
opts: { bunnyEndpoint?: { host: string } } = {},
676678
): Promise<ResolvedRegistry | null> {
677679
const regSpin = spinner("Fetching registries...");
678680
regSpin.start();
@@ -685,6 +687,14 @@ export async function promptRegistry(
685687
const pushable = registries.filter((r) => r.userName);
686688

687689
const choices = [
690+
...(opts.bunnyEndpoint
691+
? [
692+
{
693+
title: `bunny.net registry (${opts.bunnyEndpoint.host})`,
694+
value: BUNNY_REGISTRY_ID,
695+
},
696+
]
697+
: []),
688698
...pushable.map((r) => ({
689699
title: `${r.displayName} (${r.hostName}${r.userName})`,
690700
value: String(r.id ?? ""),
@@ -700,6 +710,9 @@ export async function promptRegistry(
700710
});
701711

702712
if (choice === undefined) return null;
713+
if (choice === BUNNY_REGISTRY_ID) {
714+
return { id: BUNNY_REGISTRY_ID, hostName: opts.bunnyEndpoint?.host };
715+
}
703716
if (choice !== ADD_NEW_REGISTRY) {
704717
const existing = pushable.find((r) => String(r.id) === String(choice));
705718
return { id: String(choice), hostName: existing?.hostName };

packages/cli/src/commands/registry/client.ts

Lines changed: 10 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,15 @@
1+
import {
2+
basicAuthHeader,
3+
type RegistryEndpoint,
4+
} from "../../core/bunny-registry.ts";
15
import { UserError } from "../../core/errors.ts";
26

3-
/** Env var holding the OCI registry endpoint. Intentionally undocumented. */
4-
export const REGISTRY_URL_ENV = "BUNNYNET_REGISTRY_URL";
5-
6-
/** Basic-auth username for the registry. The API token is the password. */
7-
export const REGISTRY_USERNAME = "token";
8-
9-
export interface RegistryEndpoint {
10-
/** Normalised base URL with no trailing slash (e.g. `https://host`). */
11-
baseUrl: string;
12-
/** Host[:port] used for `docker login` / image refs. */
13-
host: string;
14-
}
15-
16-
/**
17-
* Normalise a raw registry URL into a base URL and host, defaulting the
18-
* scheme to https when omitted. Pure so it's testable without env access.
19-
*/
20-
export function parseRegistryUrl(raw: string): RegistryEndpoint {
21-
const trimmed = raw.trim();
22-
if (!trimmed) throw new UserError("Registry URL is empty.");
23-
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
24-
? trimmed
25-
: `https://${trimmed}`;
26-
27-
let url: URL;
28-
try {
29-
url = new URL(withScheme);
30-
} catch {
31-
throw new UserError(`Invalid registry URL: ${raw}`);
32-
}
33-
34-
return { baseUrl: url.origin, host: url.host };
35-
}
36-
37-
/**
38-
* Resolve the registry endpoint from the environment. The URL is kept out
39-
* of help output and config so it isn't casually discoverable; an unset
40-
* env var is an expected, friendly error rather than a crash.
41-
*/
42-
export function resolveRegistryEndpoint(): RegistryEndpoint {
43-
const raw = process.env[REGISTRY_URL_ENV];
44-
if (!raw) {
45-
throw new UserError(
46-
"Registry endpoint is not configured.",
47-
`Set ${REGISTRY_URL_ENV} to the registry URL and try again.`,
48-
);
49-
}
50-
return parseRegistryUrl(raw);
51-
}
52-
53-
/** Build the HTTP Basic auth header from the resolved API token. */
54-
export function basicAuthHeader(apiKey: string): string {
55-
const encoded = Buffer.from(`${REGISTRY_USERNAME}:${apiKey}`).toString(
56-
"base64",
57-
);
58-
return `Basic ${encoded}`;
59-
}
7+
export {
8+
parseRegistryUrl,
9+
REGISTRY_USERNAME,
10+
type RegistryEndpoint,
11+
resolveRegistryEndpoint,
12+
} from "../../core/bunny-registry.ts";
6013

6114
/**
6215
* Perform an authenticated request against an OCI distribution path

packages/cli/src/commands/registry/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { registryListCommand } from "./list.ts";
33
import { registryPushCommand } from "./push.ts";
44
import { registryTagsCommand } from "./tags.ts";
55

6-
// Hidden namespace: the registry endpoint is configured out-of-band via
7-
// BUNNYNET_REGISTRY_URL and not advertised in help output. Auth is handled
8-
// per-request with the API token — there's no separate login step.
96
export const registryNamespace = defineNamespace("registry", false, [
107
registryPushCommand,
118
registryListCommand,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { UserError } from "./errors.ts";
2+
3+
/** Endpoint and auth resolution for the bunny.net OCI registry, shared by the registry commands and apps deploy. */
4+
5+
/** Env var holding the OCI registry endpoint — a stub until `/registries` returns the bunny registry (host included) directly. */
6+
export const REGISTRY_URL_ENV = "BUNNYNET_REGISTRY_URL";
7+
8+
/** Basic-auth username for the registry. The API token is the password. */
9+
export const REGISTRY_USERNAME = "token";
10+
11+
export interface RegistryEndpoint {
12+
/** Normalised base URL with no trailing slash (e.g. `https://host`). */
13+
baseUrl: string;
14+
/** Host[:port] used for `docker login` / image refs. */
15+
host: string;
16+
}
17+
18+
/**
19+
* Normalise a raw registry URL into a base URL and host, defaulting the
20+
* scheme to https when omitted. Pure so it's testable without env access.
21+
*/
22+
export function parseRegistryUrl(raw: string): RegistryEndpoint {
23+
const trimmed = raw.trim();
24+
if (!trimmed) throw new UserError("Registry URL is empty.");
25+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
26+
? trimmed
27+
: `https://${trimmed}`;
28+
29+
let url: URL;
30+
try {
31+
url = new URL(withScheme);
32+
} catch {
33+
throw new UserError(`Invalid registry URL: ${raw}`);
34+
}
35+
36+
return { baseUrl: url.origin, host: url.host };
37+
}
38+
39+
/** Resolve the endpoint from the environment, or null when unset. */
40+
export function tryResolveRegistryEndpoint(): RegistryEndpoint | null {
41+
const raw = process.env[REGISTRY_URL_ENV];
42+
return raw ? parseRegistryUrl(raw) : null;
43+
}
44+
45+
/**
46+
* Resolve the registry endpoint from the environment. The URL is kept out
47+
* of help output and config so it isn't casually discoverable; an unset
48+
* env var is an expected, friendly error rather than a crash.
49+
*/
50+
export function resolveRegistryEndpoint(): RegistryEndpoint {
51+
const endpoint = tryResolveRegistryEndpoint();
52+
if (!endpoint) {
53+
throw new UserError(
54+
"Registry endpoint is not configured.",
55+
`Set ${REGISTRY_URL_ENV} to the registry URL and try again.`,
56+
);
57+
}
58+
return endpoint;
59+
}
60+
61+
/** Build the HTTP Basic auth header from the resolved API token. */
62+
export function basicAuthHeader(apiKey: string): string {
63+
const encoded = Buffer.from(`${REGISTRY_USERNAME}:${apiKey}`).toString(
64+
"base64",
65+
);
66+
return `Basic ${encoded}`;
67+
}

packages/cli/src/core/docker.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { UserError } from "./errors.ts";
22

3-
/**
4-
* Generic Docker CLI primitives shared across commands. Registry- or
5-
* app-specific helpers (image-ref construction, MC platform targeting)
6-
* stay in their own command modules and build on top of these.
7-
*/
8-
93
/**
104
* Ensure the Docker CLI is available on the system.
115
*

0 commit comments

Comments
 (0)