@podosoft/podokit 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add.d.ts +10 -1
- package/dist/add.js +20 -7
- package/dist/api-paths.d.ts +34 -0
- package/dist/api-paths.js +103 -0
- package/dist/deploy-compose-profile.d.ts +4 -0
- package/dist/deploy-compose-profile.js +13 -2
- package/dist/deploy-compose-render.d.ts +1 -1
- package/dist/deploy-compose-render.js +23 -7
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.d.ts +3 -0
- package/dist/deploy-profile.js +12 -4
- package/dist/deploy-render.js +54 -8
- package/dist/deploy.js +21 -1
- package/dist/dev.d.ts +14 -0
- package/dist/dev.js +152 -10
- package/dist/index.js +15 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +4 -2
- package/dist/lockfile.d.ts +3 -0
- package/dist/lockfile.js +9 -0
- package/dist/module-api-paths.d.ts +52 -0
- package/dist/module-api-paths.js +189 -0
- package/dist/remove.d.ts +3 -0
- package/dist/remove.js +12 -3
- package/dist/templates/fullstack/AGENTS.md +17 -2
- package/dist/templates/fullstack/README.md +19 -5
- package/dist/templates/fullstack/apps/api/package.json +1 -1
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +20 -1
- package/dist/templates/fullstack/apps/api/src/app.ts +11 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.spec.ts +48 -2
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.ts +45 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +46 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +48 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +9 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +196 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +181 -2
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +8 -1
- package/dist/templates/fullstack/apps/api/src/database/database.ts +29 -10
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.spec.ts +188 -0
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.ts +151 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +28 -4
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +10 -2
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -1
- package/dist/templates/fullstack/apps/web/components.json +3 -1
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/index.ts +7 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/textarea.svelte +22 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +276 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +194 -9
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +15 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-headers.ts +12 -0
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-elysia-endpoint/SKILL.md +5 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +7 -1
- package/dist/templates/fullstack/dot-devcontainer/devcontainer.json +1 -0
- package/dist/templates/fullstack/{dot-env.docker → dot-env.docker.example} +7 -4
- package/dist/templates/fullstack/dot-env.example +10 -0
- package/dist/templates/fullstack/dot-gitignore +1 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +2 -1
- package/dist/templates/fullstack/infra/k3s/README.md +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +27 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/asset-paths.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts +125 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.ts +107 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +10 -10
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/object-response.ts +12 -0
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.spec.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +17 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +8 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.spec.ts +114 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.ts +50 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +4 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +11 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance-command.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance.ts +14 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.spec.ts +136 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.ts +40 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +121 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +108 -10
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +3 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +31 -3
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +2 -4
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.spec.ts +224 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +124 -35
- package/dist/templates/modules/events-redis/module.manifest.json +4 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.routes.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.spec.ts +636 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.ts +673 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +10 -4
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +37 -8
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-config.ts +99 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/migrations/1792100000000-InitFileUploadSessions.ts +49 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.test.ts +456 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.ts +356 -0
- package/dist/templates/modules/file-upload/files/tests/api/chunked-uploads.api.spec.ts +147 -0
- package/dist/templates/modules/file-upload/module.manifest.json +34 -6
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +1 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +23 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +30 -20
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +20 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +79 -12
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +16 -1
- package/dist/templates/modules/object-storage-local/module.manifest.json +11 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.spec.ts +115 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.ts +171 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +222 -9
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +55 -19
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.spec.ts +71 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.ts +98 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +6 -3
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +7 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +187 -44
- package/dist/templates/modules/object-storage-s3/module.manifest.json +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +11 -22
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +113 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +122 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +24 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +7 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +14 -1
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +9 -12
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +16 -4
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +233 -5
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +134 -32
- package/dist/templates/modules/redis/module.manifest.json +4 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +11 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +33 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +7 -0
- package/dist/templates/todo/AGENTS.md +9 -1
- package/dist/templates/todo/README.md +18 -5
- package/dist/update.d.ts +5 -0
- package/dist/update.js +35 -0
- package/package.json +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-validation.ts +0 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/skip-schema-validation.ts +0 -5
package/dist/add.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
2
|
import { type ProviderCapability, type ProviderSelections } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { type ApiPathResult } from "./module-api-paths";
|
|
3
4
|
interface Injection {
|
|
4
5
|
file: string;
|
|
5
6
|
marker: string;
|
|
@@ -9,7 +10,7 @@ interface Injection {
|
|
|
9
10
|
}
|
|
10
11
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
11
12
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
12
|
-
export declare const SUPPORTED_MANIFEST_VERSION =
|
|
13
|
+
export declare const SUPPORTED_MANIFEST_VERSION = 4;
|
|
13
14
|
export interface ModuleRuntimeConstraints {
|
|
14
15
|
/** Maximum safe API replicas for process-local providers. */
|
|
15
16
|
maxApiReplicas?: number;
|
|
@@ -79,6 +80,12 @@ export interface ModuleManifest {
|
|
|
79
80
|
requiredProjectFiles?: string[];
|
|
80
81
|
/** Stable source contracts that must be present in generated baseline files. */
|
|
81
82
|
requiredProjectContents?: Record<string, string[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Path prefixes under /api/ that the gateway must send straight to the API,
|
|
85
|
+
* such as routes taking bodies above the web server's BODY_SIZE_LIMIT. Merged
|
|
86
|
+
* into `.podokit/dev.json` and every deployment profile.
|
|
87
|
+
*/
|
|
88
|
+
apiPaths?: string[];
|
|
82
89
|
}
|
|
83
90
|
export interface PackageOverlay {
|
|
84
91
|
dependencies?: Record<string, string>;
|
|
@@ -111,6 +118,8 @@ export interface AddResult {
|
|
|
111
118
|
adopted: string[];
|
|
112
119
|
/** Files shipped, merged, or injected while applying the module. */
|
|
113
120
|
touched: string[];
|
|
121
|
+
/** Direct API paths merged into `.podokit/dev.json` and deployment profiles. */
|
|
122
|
+
apiPaths: ApiPathResult;
|
|
114
123
|
}
|
|
115
124
|
export interface ResolvedModule {
|
|
116
125
|
dir: string;
|
package/dist/add.js
CHANGED
|
@@ -14,9 +14,11 @@ const node_module_1 = require("node:module");
|
|
|
14
14
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
15
15
|
const podokit_runtime_1 = require("@podosoft/podokit-runtime");
|
|
16
16
|
const lockfile_1 = require("./lockfile");
|
|
17
|
+
const api_paths_1 = require("./api-paths");
|
|
18
|
+
const module_api_paths_1 = require("./module-api-paths");
|
|
17
19
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
18
20
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
19
|
-
exports.SUPPORTED_MANIFEST_VERSION =
|
|
21
|
+
exports.SUPPORTED_MANIFEST_VERSION = 4;
|
|
20
22
|
/** Return the legacy targetApp package fields plus any additional app overlays. */
|
|
21
23
|
function modulePackageOverlays(manifest) {
|
|
22
24
|
const overlays = new Map();
|
|
@@ -101,6 +103,12 @@ function assertManifestVersion(name, manifest) {
|
|
|
101
103
|
throw new Error(`Module "${name}" declares invalid provider ${capability}=${String(provider)}.`);
|
|
102
104
|
}
|
|
103
105
|
}
|
|
106
|
+
try {
|
|
107
|
+
(0, api_paths_1.parseApiPaths)(manifest.apiPaths ?? [], "apiPaths", []);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`Module "${name}" declares invalid apiPaths. ${error.message}`);
|
|
111
|
+
}
|
|
104
112
|
const maxApiReplicas = manifest.runtimeConstraints?.maxApiReplicas;
|
|
105
113
|
if (maxApiReplicas !== undefined
|
|
106
114
|
&& (!Number.isSafeInteger(maxApiReplicas) || maxApiReplicas < 1)) {
|
|
@@ -363,16 +371,20 @@ function addModule(options) {
|
|
|
363
371
|
.map(([path]) => path)
|
|
364
372
|
: [];
|
|
365
373
|
const placement = modulePlacement(options.projectRoot, options.module, options.modulesDir);
|
|
366
|
-
const
|
|
367
|
-
if (options.adopt &&
|
|
368
|
-
releaseOwnedPaths(options.projectRoot,
|
|
374
|
+
const applied = applyModule(options.projectRoot, options.module, options.modulesDir, new Set(), options.adopt ?? false, placement);
|
|
375
|
+
if (options.adopt && applied.adopted.length) {
|
|
376
|
+
releaseOwnedPaths(options.projectRoot, applied.adopted);
|
|
369
377
|
}
|
|
370
378
|
// Record the module (and any auto-added requirements), fold in the modules'
|
|
371
379
|
// declared ownedGlobs, and refresh the lock.
|
|
372
|
-
const records = [...
|
|
380
|
+
const records = [...applied.added, applied.module]
|
|
373
381
|
.map((name) => resolveModule(name, options.modulesDir, options.projectRoot))
|
|
374
382
|
.filter((module) => module !== null)
|
|
375
383
|
.map(moduleRecord);
|
|
384
|
+
const result = {
|
|
385
|
+
...applied,
|
|
386
|
+
apiPaths: (0, module_api_paths_1.addApiPaths)(options.projectRoot, records.flatMap((record) => (record.appliedApiPaths ?? []).map((path) => ({ module: record.name, path })))),
|
|
387
|
+
};
|
|
376
388
|
(0, lockfile_1.recordModules)(options.projectRoot, records, options.podokitVersion, result.ownedGlobs, previousLock
|
|
377
389
|
? {
|
|
378
390
|
previous: previousLock,
|
|
@@ -488,12 +500,13 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt, placement)
|
|
|
488
500
|
};
|
|
489
501
|
}
|
|
490
502
|
function moduleRecord(module) {
|
|
491
|
-
const
|
|
503
|
+
const manifest = readModuleManifest(module.dir);
|
|
492
504
|
return {
|
|
493
505
|
name: module.name,
|
|
494
506
|
packageName: module.packageName,
|
|
495
507
|
moduleVersion: module.moduleVersion,
|
|
496
|
-
appliedMigrations: migrations.map((migration) => migration.id),
|
|
508
|
+
appliedMigrations: (manifest.migrations ?? []).map((migration) => migration.id),
|
|
509
|
+
...(manifest.apiPaths?.length ? { appliedApiPaths: manifest.apiPaths } : {}),
|
|
497
510
|
};
|
|
498
511
|
}
|
|
499
512
|
/** Copy a module overlay without clobbering app-owned presentation files. */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The prefix the gateway removes before a direct request reaches the API. */
|
|
2
|
+
export declare const API_PATH_PREFIX = "/api";
|
|
3
|
+
/** The path an entry names, without its optional trailing slash. */
|
|
4
|
+
export declare function apiPathBase(path: string): string;
|
|
5
|
+
/** The entry in `paths` that already routes `path` directly, if any. */
|
|
6
|
+
export declare function coveringApiPath(paths: readonly string[], path: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Validate the direct API path allowlist. Each entry covers its own path and
|
|
9
|
+
* every path below it, and must stay under /api/ outside the authentication
|
|
10
|
+
* routes, because the gateway removes /api before the request reaches the API.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseApiPaths(value: unknown, field: string, webSocketPaths: readonly string[]): string[];
|
|
13
|
+
export interface ApiPathMerge {
|
|
14
|
+
/** The resulting list: the current entries followed by the added ones. */
|
|
15
|
+
paths: string[];
|
|
16
|
+
added: string[];
|
|
17
|
+
/** Paths left out because the list would no longer validate. */
|
|
18
|
+
rejected: {
|
|
19
|
+
path: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append `additions` to `current`, skipping paths an entry already covers and
|
|
25
|
+
* paths that would make `validate` throw, such as an entry covering an existing
|
|
26
|
+
* one or a WebSocket path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mergeApiPaths(current: readonly string[], additions: readonly string[], validate: (paths: string[]) => void): ApiPathMerge;
|
|
29
|
+
/** The direct API paths installed modules contributed, as a valid list for a new configuration. */
|
|
30
|
+
export declare function installedApiPaths(modules: readonly {
|
|
31
|
+
appliedApiPaths?: string[];
|
|
32
|
+
}[]): string[];
|
|
33
|
+
/** Plan text naming the paths that reach the API without the web proxy. */
|
|
34
|
+
export declare function directApiRoutes(apiPaths: readonly string[]): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.API_PATH_PREFIX = void 0;
|
|
4
|
+
exports.apiPathBase = apiPathBase;
|
|
5
|
+
exports.coveringApiPath = coveringApiPath;
|
|
6
|
+
exports.parseApiPaths = parseApiPaths;
|
|
7
|
+
exports.mergeApiPaths = mergeApiPaths;
|
|
8
|
+
exports.installedApiPaths = installedApiPaths;
|
|
9
|
+
exports.directApiRoutes = directApiRoutes;
|
|
10
|
+
const MAX_PATHS = 32;
|
|
11
|
+
const MAX_PATH_LENGTH = 2_048;
|
|
12
|
+
const API_PATH = /^\/api(?:\/[A-Za-z0-9._~-]+)+\/?$/;
|
|
13
|
+
/** The prefix the gateway removes before a direct request reaches the API. */
|
|
14
|
+
exports.API_PATH_PREFIX = "/api";
|
|
15
|
+
/** The path an entry names, without its optional trailing slash. */
|
|
16
|
+
function apiPathBase(path) {
|
|
17
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
18
|
+
}
|
|
19
|
+
/** Whether `base` covers `path`: the same path or any path below it. */
|
|
20
|
+
function covers(base, path) {
|
|
21
|
+
return path === base || path.startsWith(`${base}/`);
|
|
22
|
+
}
|
|
23
|
+
/** The entry in `paths` that already routes `path` directly, if any. */
|
|
24
|
+
function coveringApiPath(paths, path) {
|
|
25
|
+
const base = apiPathBase(path);
|
|
26
|
+
return paths.find((entry) => covers(apiPathBase(entry), base));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate the direct API path allowlist. Each entry covers its own path and
|
|
30
|
+
* every path below it, and must stay under /api/ outside the authentication
|
|
31
|
+
* routes, because the gateway removes /api before the request reaches the API.
|
|
32
|
+
*/
|
|
33
|
+
function parseApiPaths(value, field, webSocketPaths) {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
throw new Error(`Configuration field "${field}" must be an array of path prefixes under /api/.`);
|
|
36
|
+
}
|
|
37
|
+
if (value.length > MAX_PATHS) {
|
|
38
|
+
throw new Error(`Configuration field "${field}" must contain at most ${MAX_PATHS} paths.`);
|
|
39
|
+
}
|
|
40
|
+
const paths = [];
|
|
41
|
+
for (const path of value) {
|
|
42
|
+
if (typeof path !== "string" ||
|
|
43
|
+
path.length > MAX_PATH_LENGTH ||
|
|
44
|
+
!API_PATH.test(path) ||
|
|
45
|
+
path.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
46
|
+
throw new Error(`Configuration field "${field}" must contain only static path prefixes under /api/, such as "/api/files/uploads/". /api/ itself, patterns, queries, fragments, encoded characters, empty segments, and traversal segments are not allowed.`);
|
|
47
|
+
}
|
|
48
|
+
const base = apiPathBase(path);
|
|
49
|
+
if (covers("/api/auth", base)) {
|
|
50
|
+
throw new Error(`Configuration field "${field}" cannot list "${path}": the API serves /api/auth/ with its prefix, so authentication routes always go through the web proxy.`);
|
|
51
|
+
}
|
|
52
|
+
const overlap = paths.find((other) => {
|
|
53
|
+
const otherBase = apiPathBase(other);
|
|
54
|
+
return covers(otherBase, base) || covers(base, otherBase);
|
|
55
|
+
});
|
|
56
|
+
if (overlap !== undefined) {
|
|
57
|
+
throw new Error(overlap === path
|
|
58
|
+
? `Configuration field "${field}" contains duplicate path "${path}".`
|
|
59
|
+
: `Configuration field "${field}" contains overlapping paths "${overlap}" and "${path}".`);
|
|
60
|
+
}
|
|
61
|
+
const socket = webSocketPaths.find((socketPath) => covers(base, socketPath));
|
|
62
|
+
if (socket !== undefined) {
|
|
63
|
+
throw new Error(`Configuration field "${field}" entry "${path}" covers WebSocket path "${socket}", which goes to the API without removing /api.`);
|
|
64
|
+
}
|
|
65
|
+
paths.push(path);
|
|
66
|
+
}
|
|
67
|
+
return paths;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Append `additions` to `current`, skipping paths an entry already covers and
|
|
71
|
+
* paths that would make `validate` throw, such as an entry covering an existing
|
|
72
|
+
* one or a WebSocket path.
|
|
73
|
+
*/
|
|
74
|
+
function mergeApiPaths(current, additions, validate) {
|
|
75
|
+
const paths = [...current];
|
|
76
|
+
const added = [];
|
|
77
|
+
const rejected = [];
|
|
78
|
+
for (const path of additions) {
|
|
79
|
+
if (coveringApiPath(paths, path) !== undefined)
|
|
80
|
+
continue;
|
|
81
|
+
const candidate = [...paths, path];
|
|
82
|
+
try {
|
|
83
|
+
validate(candidate);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
rejected.push({ path, reason: error.message });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
paths.push(path);
|
|
90
|
+
added.push(path);
|
|
91
|
+
}
|
|
92
|
+
return { paths, added, rejected };
|
|
93
|
+
}
|
|
94
|
+
/** The direct API paths installed modules contributed, as a valid list for a new configuration. */
|
|
95
|
+
function installedApiPaths(modules) {
|
|
96
|
+
return mergeApiPaths([], modules.flatMap((module) => module.appliedApiPaths ?? []), (paths) => parseApiPaths(paths, "apiPaths", [])).paths;
|
|
97
|
+
}
|
|
98
|
+
/** Plan text naming the paths that reach the API without the web proxy. */
|
|
99
|
+
function directApiRoutes(apiPaths) {
|
|
100
|
+
return apiPaths.length
|
|
101
|
+
? ` Route ${apiPaths.join(", ")} directly to the API without /api.`
|
|
102
|
+
: "";
|
|
103
|
+
}
|
|
@@ -69,6 +69,8 @@ export interface DockerComposeProfileV1 {
|
|
|
69
69
|
bindAddress: string;
|
|
70
70
|
port: number;
|
|
71
71
|
webSocketPaths: string[];
|
|
72
|
+
/** Path prefixes under /api/ that the gateway sends to the API with /api removed. */
|
|
73
|
+
apiPaths: string[];
|
|
72
74
|
trustedProxyCidrs: string[];
|
|
73
75
|
gatewayImage: string;
|
|
74
76
|
};
|
|
@@ -107,4 +109,6 @@ export interface ComposeProfileSummary {
|
|
|
107
109
|
profile: DockerComposeProfileV1;
|
|
108
110
|
}
|
|
109
111
|
export declare function initializeComposeProfile(projectRoot: string, name: string, options: InitializeComposeProfileOptions): ComposeProfileSummary;
|
|
112
|
+
/** The gateway runs only when some path must bypass the web service. */
|
|
113
|
+
export declare function composeHasGateway(profile: DockerComposeProfileV1): boolean;
|
|
110
114
|
export declare function composeProfileDigest(profile: DockerComposeProfileV1): string;
|
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseComposeProfile = parseComposeProfile;
|
|
4
4
|
exports.buildDefaultComposeProfile = buildDefaultComposeProfile;
|
|
5
5
|
exports.initializeComposeProfile = initializeComposeProfile;
|
|
6
|
+
exports.composeHasGateway = composeHasGateway;
|
|
6
7
|
exports.composeProfileDigest = composeProfileDigest;
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_net_1 = require("node:net");
|
|
10
11
|
const lockfile_1 = require("./lockfile");
|
|
11
12
|
const deploy_schema_1 = require("./deploy-schema");
|
|
13
|
+
const api_paths_1 = require("./api-paths");
|
|
12
14
|
const websocket_paths_1 = require("./websocket-paths");
|
|
13
15
|
/** Decimal core count, e.g. "0.25" or "2". */
|
|
14
16
|
const CPU_VALUE = /^(?:0\.[0-9]{1,3}|[1-9][0-9]{0,2}(?:\.[0-9]{1,3})?)$/;
|
|
@@ -167,6 +169,7 @@ function parseComposeProfile(value) {
|
|
|
167
169
|
"bindAddress",
|
|
168
170
|
"port",
|
|
169
171
|
"webSocketPaths",
|
|
172
|
+
"apiPaths",
|
|
170
173
|
"trustedProxyCidrs",
|
|
171
174
|
"gatewayImage",
|
|
172
175
|
], "exposure");
|
|
@@ -197,9 +200,10 @@ function parseComposeProfile(value) {
|
|
|
197
200
|
if (port > 65535)
|
|
198
201
|
throw new Error("Deployment exposure.port must be a TCP port.");
|
|
199
202
|
const webSocketPaths = (0, websocket_paths_1.parseExactWebSocketPaths)(exposure.webSocketPaths ?? [], "exposure.webSocketPaths");
|
|
203
|
+
const apiPaths = (0, api_paths_1.parseApiPaths)(exposure.apiPaths ?? [], "exposure.apiPaths", webSocketPaths);
|
|
200
204
|
const trustedProxyCidrs = parseTrustedProxyCidrs(exposure.trustedProxyCidrs ?? [], "exposure.trustedProxyCidrs");
|
|
201
|
-
if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0) {
|
|
202
|
-
throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths entry.");
|
|
205
|
+
if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0 && apiPaths.length === 0) {
|
|
206
|
+
throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths or exposure.apiPaths entry.");
|
|
203
207
|
}
|
|
204
208
|
const gatewayImage = exposure.gatewayImage === undefined
|
|
205
209
|
? "caddy:2.10-alpine"
|
|
@@ -238,6 +242,7 @@ function parseComposeProfile(value) {
|
|
|
238
242
|
bindAddress,
|
|
239
243
|
port,
|
|
240
244
|
webSocketPaths,
|
|
245
|
+
apiPaths,
|
|
241
246
|
trustedProxyCidrs,
|
|
242
247
|
gatewayImage,
|
|
243
248
|
},
|
|
@@ -302,6 +307,7 @@ function buildDefaultComposeProfile(projectName, options) {
|
|
|
302
307
|
bindAddress: "127.0.0.1",
|
|
303
308
|
port: 8080,
|
|
304
309
|
webSocketPaths: [],
|
|
310
|
+
apiPaths: [],
|
|
305
311
|
trustedProxyCidrs: [],
|
|
306
312
|
gatewayImage: "caddy:2.10-alpine",
|
|
307
313
|
},
|
|
@@ -385,6 +391,7 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
385
391
|
|| providers.events === "memory"
|
|
386
392
|
|| providers.jobs === "local";
|
|
387
393
|
draft.workloads.api.replicas = usesEmbeddedProviders ? 1 : 2;
|
|
394
|
+
draft.exposure.apiPaths = (0, api_paths_1.installedApiPaths)(manifest.modules);
|
|
388
395
|
draft.dependencies.postgres.mode = providers.database === "postgres" ? "managed" : "disabled";
|
|
389
396
|
draft.dependencies.redis.mode = usesRedis ? "managed" : "disabled";
|
|
390
397
|
draft.dependencies.objectStorage.mode = providers["object-storage"] === "s3"
|
|
@@ -423,6 +430,10 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
423
430
|
(0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
|
|
424
431
|
return { name, path, profile: validated };
|
|
425
432
|
}
|
|
433
|
+
/** The gateway runs only when some path must bypass the web service. */
|
|
434
|
+
function composeHasGateway(profile) {
|
|
435
|
+
return profile.exposure.webSocketPaths.length > 0 || profile.exposure.apiPaths.length > 0;
|
|
436
|
+
}
|
|
426
437
|
function composeProfileDigest(profile) {
|
|
427
438
|
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(profile)).digest("hex")}`;
|
|
428
439
|
}
|
|
@@ -10,6 +10,8 @@ exports.renderComposeDeployment = renderComposeDeployment;
|
|
|
10
10
|
const node_crypto_1 = require("node:crypto");
|
|
11
11
|
const node_fs_1 = require("node:fs");
|
|
12
12
|
const node_path_1 = require("node:path");
|
|
13
|
+
const api_paths_1 = require("./api-paths");
|
|
14
|
+
const deploy_compose_profile_1 = require("./deploy-compose-profile");
|
|
13
15
|
const deploy_schema_1 = require("./deploy-schema");
|
|
14
16
|
const lockfile_1 = require("./lockfile");
|
|
15
17
|
const toolchain_1 = require("./toolchain");
|
|
@@ -249,12 +251,26 @@ function gatewayService(profile, image, release, stateDigest) {
|
|
|
249
251
|
}
|
|
250
252
|
`
|
|
251
253
|
: "";
|
|
254
|
+
const api = `${serviceName(profile, "api")}:${CONTAINER_PORT}`;
|
|
255
|
+
const webSocketRoutes = profile.exposure.webSocketPaths.length
|
|
256
|
+
? ` @api_websockets path ${profile.exposure.webSocketPaths.join(" ")}\n` +
|
|
257
|
+
` handle @api_websockets {\n` +
|
|
258
|
+
` reverse_proxy ${api}\n` +
|
|
259
|
+
` }\n`
|
|
260
|
+
: "";
|
|
261
|
+
const apiPathRoutes = profile.exposure.apiPaths.length
|
|
262
|
+
? ` @api_paths path ${profile.exposure.apiPaths
|
|
263
|
+
.map((path) => `${(0, api_paths_1.apiPathBase)(path)} ${(0, api_paths_1.apiPathBase)(path)}/*`)
|
|
264
|
+
.join(" ")}\n` +
|
|
265
|
+
` handle @api_paths {\n` +
|
|
266
|
+
` uri strip_prefix ${api_paths_1.API_PATH_PREFIX}\n` +
|
|
267
|
+
` reverse_proxy ${api}\n` +
|
|
268
|
+
` }\n`
|
|
269
|
+
: "";
|
|
252
270
|
const caddyfile = globalOptions +
|
|
253
271
|
`:3000 {\n` +
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
` reverse_proxy ${serviceName(profile, "api")}:${CONTAINER_PORT}\n` +
|
|
257
|
-
` }\n` +
|
|
272
|
+
webSocketRoutes +
|
|
273
|
+
apiPathRoutes +
|
|
258
274
|
` handle {\n` +
|
|
259
275
|
` reverse_proxy ${serviceName(profile, "web")}:${CONTAINER_PORT}\n` +
|
|
260
276
|
` }\n` +
|
|
@@ -278,7 +294,7 @@ function gatewayService(profile, image, release, stateDigest) {
|
|
|
278
294
|
}
|
|
279
295
|
function renderComposeDocument(profile, release, images, stateDigest, workerCommand = ["bun", "dist/main-worker.js"]) {
|
|
280
296
|
const services = [];
|
|
281
|
-
const hasGateway = profile
|
|
297
|
+
const hasGateway = (0, deploy_compose_profile_1.composeHasGateway)(profile);
|
|
282
298
|
if (profile.dependencies.postgres.mode === "managed" && images.postgres) {
|
|
283
299
|
services.push(postgresService(profile, images.postgres));
|
|
284
300
|
}
|
|
@@ -300,7 +316,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
|
|
|
300
316
|
}));
|
|
301
317
|
if (hasGateway) {
|
|
302
318
|
if (!images.gateway) {
|
|
303
|
-
throw new Error("A resolved gateway image is required when WebSocket paths are configured.");
|
|
319
|
+
throw new Error("A resolved gateway image is required when WebSocket or API paths are configured.");
|
|
304
320
|
}
|
|
305
321
|
services.push(gatewayService(profile, images.gateway, release, stateDigest));
|
|
306
322
|
}
|
|
@@ -350,7 +366,7 @@ function defaultComposeImages(profile, release) {
|
|
|
350
366
|
return {
|
|
351
367
|
api: `${profile.release.apiRepository}:${release}`,
|
|
352
368
|
web: `${profile.release.webRepository}:${release}`,
|
|
353
|
-
gateway: profile
|
|
369
|
+
gateway: (0, deploy_compose_profile_1.composeHasGateway)(profile) ? profile.exposure.gatewayImage : null,
|
|
354
370
|
postgres: profile.dependencies.postgres.mode === "managed"
|
|
355
371
|
? profile.dependencies.postgres.image
|
|
356
372
|
: null,
|
package/dist/deploy-compose.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.rollbackComposeDeployment = rollbackComposeDeployment;
|
|
|
11
11
|
exports.composeRuntimeRoot = composeRuntimeRoot;
|
|
12
12
|
const node_crypto_1 = require("node:crypto");
|
|
13
13
|
const node_path_1 = require("node:path");
|
|
14
|
+
const api_paths_1 = require("./api-paths");
|
|
14
15
|
const deploy_driver_1 = require("./deploy-driver");
|
|
15
16
|
const deploy_compose_exec_1 = require("./deploy-compose-exec");
|
|
16
17
|
const deploy_compose_sync_1 = require("./deploy-compose-sync");
|
|
@@ -373,7 +374,7 @@ function planComposeDeployment(projectRoot, profileName, release, runner = deplo
|
|
|
373
374
|
actions.push({
|
|
374
375
|
order: order++,
|
|
375
376
|
kind: "application",
|
|
376
|
-
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""}${profile
|
|
377
|
+
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""}${(0, deploy_compose_profile_1.composeHasGateway)(profile) ? ", gateway" : ""} and wait for health.${(0, api_paths_1.directApiRoutes)(profile.exposure.apiPaths)}`,
|
|
377
378
|
});
|
|
378
379
|
actions.push({
|
|
379
380
|
order: order++,
|
package/dist/deploy-profile.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface DeployProfileV1 {
|
|
|
50
50
|
ingressClassName: string;
|
|
51
51
|
tlsSecretName: string | null;
|
|
52
52
|
webSocketPaths: string[];
|
|
53
|
+
/** Path prefixes under /api/ that the Ingress sends to the API with /api removed. */
|
|
54
|
+
apiPaths: string[];
|
|
53
55
|
};
|
|
54
56
|
workloads: {
|
|
55
57
|
api: WorkloadProfile;
|
|
@@ -78,6 +80,7 @@ export interface DeploymentProfileSummary {
|
|
|
78
80
|
path: string;
|
|
79
81
|
profile: DeployProfileV1;
|
|
80
82
|
}
|
|
83
|
+
export declare function parseDeploymentProfile(value: unknown): DeployProfileV1;
|
|
81
84
|
export declare function loadDeploymentProfile(projectRoot: string, name: string): DeployProfileV1;
|
|
82
85
|
export declare function listDeploymentProfiles(projectRoot: string): DeploymentProfileSummary[];
|
|
83
86
|
export interface InitializeDeploymentProfileOptions {
|
package/dist/deploy-profile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.profilePath = exports.profileDirectory = void 0;
|
|
4
|
+
exports.parseDeploymentProfile = parseDeploymentProfile;
|
|
4
5
|
exports.loadDeploymentProfile = loadDeploymentProfile;
|
|
5
6
|
exports.listDeploymentProfiles = listDeploymentProfiles;
|
|
6
7
|
exports.initializeDeploymentProfile = initializeDeploymentProfile;
|
|
@@ -11,6 +12,7 @@ const node_fs_1 = require("node:fs");
|
|
|
11
12
|
const node_path_1 = require("node:path");
|
|
12
13
|
const lockfile_1 = require("./lockfile");
|
|
13
14
|
const deploy_schema_1 = require("./deploy-schema");
|
|
15
|
+
const api_paths_1 = require("./api-paths");
|
|
14
16
|
const websocket_paths_1 = require("./websocket-paths");
|
|
15
17
|
var deploy_schema_2 = require("./deploy-schema");
|
|
16
18
|
Object.defineProperty(exports, "profileDirectory", { enumerable: true, get: function () { return deploy_schema_2.profileDirectory; } });
|
|
@@ -84,7 +86,7 @@ function parseStatefulDependency(value, field, extraKeys = []) {
|
|
|
84
86
|
function parseRedisDependency(value, field) {
|
|
85
87
|
return parseStatefulDependency(value, field);
|
|
86
88
|
}
|
|
87
|
-
function
|
|
89
|
+
function parseDeploymentProfile(value) {
|
|
88
90
|
if (!(0, deploy_schema_1.isRecord)(value))
|
|
89
91
|
throw new Error("Deployment profile must be a JSON object.");
|
|
90
92
|
(0, deploy_schema_1.assertOnlyKeys)(value, [
|
|
@@ -117,7 +119,7 @@ function parseProfile(value) {
|
|
|
117
119
|
const runtimeConfig = (0, deploy_schema_1.requiredRecord)(value, "runtimeConfig");
|
|
118
120
|
(0, deploy_schema_1.assertOnlyKeys)(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
|
|
119
121
|
(0, deploy_schema_1.assertOnlyKeys)(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
|
|
120
|
-
(0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths"], "exposure");
|
|
122
|
+
(0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths", "apiPaths"], "exposure");
|
|
121
123
|
(0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
|
|
122
124
|
(0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
|
|
123
125
|
(0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "imagePull"], "secrets");
|
|
@@ -167,6 +169,10 @@ function parseProfile(value) {
|
|
|
167
169
|
if (exposureMode === "nodePort" && webSocketPaths.length > 0) {
|
|
168
170
|
throw new Error("Deployment exposure.webSocketPaths requires exposure.mode ingress.");
|
|
169
171
|
}
|
|
172
|
+
const apiPaths = (0, api_paths_1.parseApiPaths)(exposure.apiPaths ?? [], "exposure.apiPaths", webSocketPaths);
|
|
173
|
+
if (exposureMode === "nodePort" && apiPaths.length > 0) {
|
|
174
|
+
throw new Error("Deployment exposure.apiPaths requires exposure.mode ingress.");
|
|
175
|
+
}
|
|
170
176
|
const postgres = parseStatefulDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres");
|
|
171
177
|
const redis = parseRedisDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis");
|
|
172
178
|
const objectStorageRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "objectStorage");
|
|
@@ -210,6 +216,7 @@ function parseProfile(value) {
|
|
|
210
216
|
ingressClassName,
|
|
211
217
|
tlsSecretName,
|
|
212
218
|
webSocketPaths,
|
|
219
|
+
apiPaths,
|
|
213
220
|
},
|
|
214
221
|
workloads: {
|
|
215
222
|
api: parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
|
|
@@ -246,7 +253,7 @@ function loadDeploymentProfile(projectRoot, name) {
|
|
|
246
253
|
catch (error) {
|
|
247
254
|
throw new Error(`Cannot read deployment profile "${name}": ${error.message}`);
|
|
248
255
|
}
|
|
249
|
-
return
|
|
256
|
+
return parseDeploymentProfile(parsed);
|
|
250
257
|
}
|
|
251
258
|
function listDeploymentProfiles(projectRoot) {
|
|
252
259
|
const directory = (0, deploy_schema_1.profileDirectory)(projectRoot);
|
|
@@ -319,6 +326,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
319
326
|
ingressClassName: "traefik",
|
|
320
327
|
tlsSecretName: null,
|
|
321
328
|
webSocketPaths: [],
|
|
329
|
+
apiPaths: (0, api_paths_1.installedApiPaths)(manifest.modules),
|
|
322
330
|
},
|
|
323
331
|
workloads: {
|
|
324
332
|
api: {
|
|
@@ -418,7 +426,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
418
426
|
],
|
|
419
427
|
},
|
|
420
428
|
};
|
|
421
|
-
const validated =
|
|
429
|
+
const validated = parseDeploymentProfile(defaultProfile);
|
|
422
430
|
(0, node_fs_1.mkdirSync)((0, deploy_schema_1.profileDirectory)(projectRoot), { recursive: true });
|
|
423
431
|
(0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
|
|
424
432
|
return { name, path, profile: validated };
|
package/dist/deploy-render.js
CHANGED
|
@@ -5,6 +5,7 @@ const node_fs_1 = require("node:fs");
|
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
7
|
const deploy_profile_1 = require("./deploy-profile");
|
|
8
|
+
const api_paths_1 = require("./api-paths");
|
|
8
9
|
const lockfile_1 = require("./lockfile");
|
|
9
10
|
const toolchain_1 = require("./toolchain");
|
|
10
11
|
function quote(value) {
|
|
@@ -525,6 +526,57 @@ spec:
|
|
|
525
526
|
app.kubernetes.io/name: ${name}
|
|
526
527
|
`;
|
|
527
528
|
}
|
|
529
|
+
function ingressPath(path, pathType, service) {
|
|
530
|
+
return ` - path: ${quote(path)}
|
|
531
|
+
pathType: ${pathType}
|
|
532
|
+
backend:
|
|
533
|
+
service:
|
|
534
|
+
name: ${service}
|
|
535
|
+
port: { number: 3000 }
|
|
536
|
+
`;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Direct API paths need /api removed on the way to the API, which a standard
|
|
540
|
+
* Ingress cannot express, so they get their own Ingress with a Traefik
|
|
541
|
+
* stripPrefix middleware. ImplementationSpecific makes Traefik match the
|
|
542
|
+
* trailing-slash prefix as a plain PathPrefix, and the Exact path adds the bare
|
|
543
|
+
* path, so every controller version routes the same set of requests.
|
|
544
|
+
*/
|
|
545
|
+
function apiPathIngress(profile, tls) {
|
|
546
|
+
if (profile.exposure.apiPaths.length === 0)
|
|
547
|
+
return "";
|
|
548
|
+
const apiService = `${profile.target.release}-api`;
|
|
549
|
+
const middleware = `${profile.target.release}-strip-api-prefix`;
|
|
550
|
+
const paths = profile.exposure.apiPaths
|
|
551
|
+
.map((path) => {
|
|
552
|
+
const base = (0, api_paths_1.apiPathBase)(path);
|
|
553
|
+
return (ingressPath(base, "Exact", apiService) +
|
|
554
|
+
ingressPath(`${base}/`, "ImplementationSpecific", apiService));
|
|
555
|
+
})
|
|
556
|
+
.join("");
|
|
557
|
+
return `---
|
|
558
|
+
apiVersion: traefik.io/v1alpha1
|
|
559
|
+
kind: Middleware
|
|
560
|
+
metadata:
|
|
561
|
+
name: ${middleware}
|
|
562
|
+
spec:
|
|
563
|
+
stripPrefix:
|
|
564
|
+
prefixes: [${quote(api_paths_1.API_PATH_PREFIX)}]
|
|
565
|
+
---
|
|
566
|
+
apiVersion: networking.k8s.io/v1
|
|
567
|
+
kind: Ingress
|
|
568
|
+
metadata:
|
|
569
|
+
name: ${profile.target.release}-api-paths
|
|
570
|
+
annotations:
|
|
571
|
+
traefik.ingress.kubernetes.io/router.middlewares: ${quote(`${profile.target.namespace}-${middleware}@kubernetescrd`)}
|
|
572
|
+
spec:
|
|
573
|
+
ingressClassName: ${quote(profile.exposure.ingressClassName)}
|
|
574
|
+
${tls} rules:
|
|
575
|
+
- host: ${quote(profile.exposure.host)}
|
|
576
|
+
http:
|
|
577
|
+
paths:
|
|
578
|
+
${paths}`;
|
|
579
|
+
}
|
|
528
580
|
function exposure(profile) {
|
|
529
581
|
const webService = `${profile.target.release}-web`;
|
|
530
582
|
const apiService = `${profile.target.release}-api`;
|
|
@@ -548,13 +600,7 @@ spec:
|
|
|
548
600
|
`
|
|
549
601
|
: "";
|
|
550
602
|
const webSocketPaths = profile.exposure.webSocketPaths
|
|
551
|
-
.map((path) =>
|
|
552
|
-
pathType: Exact
|
|
553
|
-
backend:
|
|
554
|
-
service:
|
|
555
|
-
name: ${apiService}
|
|
556
|
-
port: { number: 3000 }
|
|
557
|
-
`)
|
|
603
|
+
.map((path) => ingressPath(path, "Exact", apiService))
|
|
558
604
|
.join("");
|
|
559
605
|
return `apiVersion: networking.k8s.io/v1
|
|
560
606
|
kind: Ingress
|
|
@@ -572,7 +618,7 @@ ${webSocketPaths} - path: /
|
|
|
572
618
|
service:
|
|
573
619
|
name: ${webService}
|
|
574
620
|
port: { number: 3000 }
|
|
575
|
-
`;
|
|
621
|
+
${apiPathIngress(profile, tls)}`;
|
|
576
622
|
}
|
|
577
623
|
function applicationManifest(profile, images, rolloutStateDigest, workerCommand) {
|
|
578
624
|
const runtimeConfigDigest = derivedRuntimeConfigDigest(profile);
|
package/dist/deploy.js
CHANGED
|
@@ -13,6 +13,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
13
13
|
const node_fs_1 = require("node:fs");
|
|
14
14
|
const node_os_1 = require("node:os");
|
|
15
15
|
const node_path_1 = require("node:path");
|
|
16
|
+
const api_paths_1 = require("./api-paths");
|
|
16
17
|
const deploy_profile_1 = require("./deploy-profile");
|
|
17
18
|
const deploy_render_1 = require("./deploy-render");
|
|
18
19
|
function defaultRunner(command, args, options) {
|
|
@@ -168,6 +169,25 @@ function doctorDeployment(projectRoot, profileName, runner = defaultRunner) {
|
|
|
168
169
|
? `IngressClass ${profile.exposure.ingressClassName} exists.`
|
|
169
170
|
: `IngressClass ${profile.exposure.ingressClassName} is missing or inaccessible.`,
|
|
170
171
|
});
|
|
172
|
+
if (profile.exposure.apiPaths.length > 0) {
|
|
173
|
+
// Direct API paths remove /api with a Traefik Middleware resource.
|
|
174
|
+
const middlewareCrd = runner("kubectl", [
|
|
175
|
+
"--context",
|
|
176
|
+
profile.target.context,
|
|
177
|
+
"get",
|
|
178
|
+
"crd",
|
|
179
|
+
"middlewares.traefik.io",
|
|
180
|
+
"-o",
|
|
181
|
+
"name",
|
|
182
|
+
], { capture: true });
|
|
183
|
+
findings.push({
|
|
184
|
+
code: "crd:middlewares.traefik.io",
|
|
185
|
+
ok: middlewareCrd.status === 0,
|
|
186
|
+
message: middlewareCrd.status === 0
|
|
187
|
+
? "Traefik Middleware resources are available for exposure.apiPaths."
|
|
188
|
+
: "exposure.apiPaths needs the Traefik Middleware CRD (middlewares.traefik.io); serve the IngressClass with Traefik or remove exposure.apiPaths.",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
171
191
|
if (profile.exposure.tlsSecretName) {
|
|
172
192
|
const tlsType = runner("kubectl", kubectlArgs(profile, [
|
|
173
193
|
"get",
|
|
@@ -465,7 +485,7 @@ function planDeployment(projectRoot, profileName, release, runner = defaultRunne
|
|
|
465
485
|
{
|
|
466
486
|
order: 3,
|
|
467
487
|
kind: "application",
|
|
468
|
-
description: `Roll out API, web${profile.workloads.worker ? ", and worker" : ""} workloads with zero unavailable replicas
|
|
488
|
+
description: `Roll out API, web${profile.workloads.worker ? ", and worker" : ""} workloads with zero unavailable replicas.${(0, api_paths_1.directApiRoutes)(profile.exposure.apiPaths)}`,
|
|
469
489
|
},
|
|
470
490
|
{
|
|
471
491
|
order: 4,
|