@flisk-sh/flisk 1.4.2 → 1.4.6
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/CHANGELOG.md +4 -0
- package/README.md +148 -49
- package/cli/dist/backend-languages/bun.js +5 -0
- package/cli/dist/backend-languages/go.js +3 -1
- package/cli/dist/cli.js +9 -0
- package/cli/dist/commands/completion.js +8 -1
- package/cli/dist/commands/config.js +130 -55
- package/cli/dist/commands/doctor.js +66 -27
- package/cli/dist/commands/down.js +33 -1
- package/cli/dist/commands/logs.js +39 -1
- package/cli/dist/commands/project.js +12 -0
- package/cli/dist/commands/resource.js +39 -11
- package/cli/dist/commands/status.js +57 -19
- package/cli/dist/commands/up.js +221 -18
- package/cli/dist/commands/upgrade.js +2 -2
- package/cli/dist/frontend-frameworks/next.js +197 -0
- package/cli/dist/frontend-frameworks/react.js +81 -12
- package/cli/dist/frontend-frameworks/registry.js +5 -4
- package/cli/dist/frontend-frameworks/vue.js +82 -12
- package/cli/dist/generator/resource-templates.js +2 -4
- package/cli/dist/generator/template-engine.js +5 -3
- package/cli/dist/utils/bun-requirement.js +7 -4
- package/cli/dist/utils/cold-preflight.js +55 -34
- package/cli/dist/utils/config-explain.js +197 -0
- package/cli/dist/utils/demo-reset.js +22 -0
- package/cli/dist/utils/discovery-paths.js +7 -1
- package/cli/dist/utils/doctor-native.js +96 -0
- package/cli/dist/utils/host-port-plan.js +8 -3
- package/cli/dist/utils/native-project.js +41 -0
- package/cli/dist/utils/native-runtime.js +513 -0
- package/cli/dist/utils/native-service.js +95 -0
- package/cli/dist/utils/native-up.js +156 -0
- package/cli/dist/utils/platform.js +1 -1
- package/cli/dist/utils/port-conflict-fix.js +16 -14
- package/cli/dist/utils/port-occupant.js +35 -0
- package/cli/dist/utils/project-features.js +7 -16
- package/cli/dist/utils/project-templates.js +9 -9
- package/cli/dist/utils/resource-features.js +5 -5
- package/cli/dist/utils/service-manifest.js +20 -3
- package/cli/dist/utils/service-urls.js +11 -0
- package/cli/dist/utils/services.js +15 -2
- package/cli/dist/utils/startup-report.js +4 -3
- package/cli/dist/utils/tar.js +33 -28
- package/cli/dist/utils/tilt-logs.js +8 -0
- package/cli/dist/utils/tilt-process.js +4 -3
- package/cli/dist/utils/windows-doctor.js +18 -15
- package/cli/dist-bundle/chunk-BIVAAWBX.js +24 -0
- package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-C4UFBXRD.js} +1 -1
- package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CAEMWNPF.js} +1 -1
- package/cli/dist-bundle/chunk-CDKEHFW6.js +35 -0
- package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-DPQK5TIS.js} +1 -1
- package/cli/dist-bundle/chunk-ETXZ5LUG.js +18 -0
- package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
- package/cli/dist-bundle/chunk-GT76VMUL.js +130 -0
- package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-IDNES5PE.js} +14 -7
- package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-MUDE4DZD.js} +5 -5
- package/cli/dist-bundle/chunk-QEBN6ZVQ.js +2 -0
- package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-R7DWXISM.js} +1 -1
- package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-U5FCQF42.js} +1 -1
- package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-WCGBVETR.js} +1 -1
- package/cli/dist-bundle/cli.js +626 -389
- package/cli/dist-bundle/cold-preflight-KUWOL2Q2.js +2 -0
- package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-QCC4IWXR.js} +1 -1
- package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
- package/cli/dist-bundle/{open-XROJ6YEY.js → open-AZNX4AC5.js} +1 -1
- package/cli/dist-bundle/project-5SWIGCNP.js +2 -0
- package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-2VQPUBSQ.js} +1 -1
- package/cli/dist-bundle/{services-BIMF6MVU.js → services-EY5WKPHQ.js} +1 -1
- package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-UBU6JWOA.js} +1 -1
- package/cli/package.json +1 -1
- package/examples/{tdk-example → flisk-example}/README.md +2 -2
- package/package.json +2 -2
- package/runtime/discovery/AGENTS.md +1 -1
- package/runtime/discovery/TILT_DISCOVERY.star +1 -1
- package/runtime/discovery/deterministic.star +1 -1
- package/runtime/engine/AGENTS.md +3 -3
- package/runtime/engine/README.md +1 -1
- package/runtime/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
- package/runtime/engine/assets/docker/wake-gateway/decide.ts +1 -1
- package/runtime/engine/assets/docker/wake-gateway/server.ts +2 -2
- package/runtime/engine/assets/scripts/mac-m1-resource-monitor.sh +6 -6
- package/runtime/engine/schemas/service-schema.json +99 -21
- package/runtime/engine/scripts/debug-container.sh +1 -1
- package/runtime/engine/scripts/generate-manifest.sh +2 -2
- package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
- package/runtime/engine/topologies/platform/docker/compose/traefik_standalone.star +19 -4
- package/runtime/engine/topologies/platform/docker/constants.star +2 -2
- package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
- package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
- package/runtime/engine/topologies/platform/docker/index.star +4 -0
- package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
- package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
- package/runtime/engine/topologies/platform/docker/networking/traefik_static_routes.star +2 -2
- package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
- package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
- package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
- package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/mapper.star +1 -1
- package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +9 -4
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
- package/runtime/ext/ide-components/README.md +1 -1
- package/runtime/ext/ui-enhancements/extension.yaml +2 -2
- package/runtime/specs/specs/TILT_SERVICE_DEFAULTS.star +1 -1
- package/runtime/specs/specs/TILT_TECH_STACK.star +1 -1
- package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
- package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
- package/cli/dist-bundle/chunk-ETKUVKJ2.js +0 -35
- package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
- package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
- package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
- package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
- package/cli/dist-bundle/project-7PTIOHGF.js +0 -2
- /package/examples/{tdk-example → flisk-example}/LICENSE +0 -0
- /package/examples/{tdk-example → flisk-example}/TILT_SERVICE_DEFAULTS.star +0 -0
- /package/examples/{tdk-example → flisk-example}/TILT_TECH_STACK.star +0 -0
- /package/examples/{tdk-example → flisk-example}/Tiltfile +0 -0
- /package/examples/{tdk-example → flisk-example}/services/platform/messaging/docker-compose.yml +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/src/index.ts +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/index.html +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/public/.gitkeep +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/main.tsx +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/orders.tsx +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/style.css +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/package.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/service.json +0 -0
- /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/src/index.ts +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2026 Flisk Landscape contributors
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Dockerfile for a Next.js frontend (`framework: next`). Node image, not the Bun + nginx SPA image; see
|
|
4
|
+
# language_dockerfile.star for the other provider-owned runtimes.
|
|
5
|
+
|
|
6
|
+
load('../build/golden_image_constants.star', 'get_layer_reference')
|
|
7
|
+
|
|
8
|
+
BASE_PORT_FRONTEND = 3000
|
|
9
|
+
|
|
10
|
+
# Same RUN line as the backend templates in language_dockerfile.star (Starlark does not export underscored names).
|
|
11
|
+
_NEXT_CURL = """RUN apt-get update && apt-get install -y --no-install-recommends curl \\
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Next.js frontend (`framework: next`): a Node image, not the Bun + nginx SPA image. `development` runs `next dev` (what Tilt builds, with
|
|
16
|
+
# the sources live-updated into it); `production` is the standalone server `next build` writes. The engine's env file becomes `.env`,
|
|
17
|
+
# which Next loads in both modes, so next.config.mjs sees FLISK_BASE_PATH and the browser bundle gets NEXT_PUBLIC_API_URL.
|
|
18
|
+
# The Node runtime comes from the approved golden layer `l4-backend-node` when the golden image is enabled (the same
|
|
19
|
+
# `use_golden` switch the Bun generators use); otherwise it is the upstream node image.
|
|
20
|
+
# The `production` stage is the last one, so a plain `docker build` of the file also produces it.
|
|
21
|
+
_BASE_ARG = "ARG BASE_IMAGE="
|
|
22
|
+
|
|
23
|
+
_NEXT = """# Generated by Flisk for a Next.js frontend. Do not edit; change service.json instead.
|
|
24
|
+
# The default is the upstream node image, pinned by digest like the backend Node template; the golden path replaces it.
|
|
25
|
+
ARG BASE_IMAGE=node:22-slim@sha256:c3de60bf2f9dd0ac6370e6117950ff62d6e339527e7472301c9c78a017978392
|
|
26
|
+
FROM ${{BASE_IMAGE}} AS base
|
|
27
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
28
|
+
# The golden runtime runs as a non-root user; install and build as root, then drop to {run_user} for the running stages.
|
|
29
|
+
USER root
|
|
30
|
+
WORKDIR /app/{res_path}
|
|
31
|
+
|
|
32
|
+
{curl}
|
|
33
|
+
COPY {res_path}/package.json {res_path}/package-lock.jso[n] ./
|
|
34
|
+
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
|
|
35
|
+
|
|
36
|
+
COPY {res_path}/.autogenerated/.env.frontend.autogenerated ./.env
|
|
37
|
+
COPY {res_path}/next.config.mjs ./next.config.mjs
|
|
38
|
+
COPY {res_path}/tsconfig.json ./tsconfig.json
|
|
39
|
+
COPY {res_path}/public ./public
|
|
40
|
+
COPY {res_path}/src ./src
|
|
41
|
+
|
|
42
|
+
FROM base AS development
|
|
43
|
+
# WORKDIR is created by root, and next dev writes .next (and next-env.d.ts) inside it as {run_user}:
|
|
44
|
+
# without this the dev server dies with EACCES: mkdir '.next'. Give the run user the app folder.
|
|
45
|
+
ENV PORT={port} HOSTNAME=0.0.0.0
|
|
46
|
+
RUN chown -R {run_user}:{run_user} /app/{res_path}
|
|
47
|
+
USER {run_user}
|
|
48
|
+
EXPOSE {port}
|
|
49
|
+
CMD ["npx", "next", "dev", "--hostname", "0.0.0.0", "--port", "{port}"]
|
|
50
|
+
|
|
51
|
+
FROM base AS build
|
|
52
|
+
RUN npx next build
|
|
53
|
+
|
|
54
|
+
FROM ${{BASE_IMAGE}} AS production
|
|
55
|
+
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT={port} HOSTNAME=0.0.0.0
|
|
56
|
+
WORKDIR /app/{res_path}
|
|
57
|
+
{curl}
|
|
58
|
+
COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/.next/standalone ./
|
|
59
|
+
COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/.next/static ./.next/static
|
|
60
|
+
COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/public ./public
|
|
61
|
+
USER {run_user}
|
|
62
|
+
EXPOSE {port}
|
|
63
|
+
CMD ["node", "server.js"]
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def generate_next_dockerfile(res_path, port = BASE_PORT_FRONTEND, use_golden = True):
|
|
67
|
+
"""Dockerfile text for a Next.js frontend: `development` (next dev), `build`, and `production` (standalone server).
|
|
68
|
+
|
|
69
|
+
With `use_golden` the base is the approved golden `l4-backend-node` layer (alpine, curl already present, non-root `app`);
|
|
70
|
+
without it, the upstream node image (debian slim, which needs curl installed, non-root `node`).
|
|
71
|
+
"""
|
|
72
|
+
if use_golden:
|
|
73
|
+
run_user = 'app'
|
|
74
|
+
curl = ''
|
|
75
|
+
else:
|
|
76
|
+
run_user = 'node'
|
|
77
|
+
curl = _NEXT_CURL
|
|
78
|
+
text = _NEXT.format(res_path = res_path, port = port, run_user = run_user, curl = curl)
|
|
79
|
+
if use_golden:
|
|
80
|
+
head, _, rest = text.partition(_BASE_ARG)
|
|
81
|
+
_, _, tail = rest.partition("\n")
|
|
82
|
+
text = head + _BASE_ARG + get_layer_reference('l4-backend-node') + "\n" + tail
|
|
83
|
+
return text
|
|
@@ -6,11 +6,13 @@ load(
|
|
|
6
6
|
"generate_app_dockerfile",
|
|
7
7
|
"generate_migrator_dockerfile",
|
|
8
8
|
)
|
|
9
|
+
load("./dockerfile/next_dockerfile.star", "generate_next_dockerfile")
|
|
9
10
|
load(
|
|
10
11
|
"./dockerfile/language_dockerfile.star",
|
|
11
12
|
"generate_language_dockerfile",
|
|
12
13
|
"is_provider_owned_language",
|
|
13
14
|
"go_live_reload_enabled",
|
|
15
|
+
"is_next_frontend",
|
|
14
16
|
"LANGUAGE_REQUIRED_FILES",
|
|
15
17
|
)
|
|
16
18
|
load("./config/dockerignore.star", "generate_dockerignore")
|
|
@@ -30,6 +32,8 @@ Docker = struct(
|
|
|
30
32
|
backend_language = generate_language_dockerfile,
|
|
31
33
|
is_provider_owned_language = is_provider_owned_language,
|
|
32
34
|
go_live_reload = go_live_reload_enabled,
|
|
35
|
+
next_frontend = generate_next_dockerfile,
|
|
36
|
+
is_next_frontend = is_next_frontend,
|
|
33
37
|
language_required_files = LANGUAGE_REQUIRED_FILES,
|
|
34
38
|
generate_dockerignore = generate_dockerignore,
|
|
35
39
|
frontend_compose = generate_frontend_compose,
|
|
@@ -19,6 +19,7 @@ load(
|
|
|
19
19
|
load(
|
|
20
20
|
'./constants.star',
|
|
21
21
|
'APP_BUN_STORE_DIR',
|
|
22
|
+
'APP_DIR',
|
|
22
23
|
'APP_GENERATED_CLIENT_DIR',
|
|
23
24
|
'APP_NODE_MODULES_BIN_DIR',
|
|
24
25
|
'APP_NODE_MODULES_DIR',
|
|
@@ -46,10 +47,16 @@ def prisma_dir_and_permissions():
|
|
|
46
47
|
Create directories that Prisma writes to at runtime and ensure the `bun` user owns
|
|
47
48
|
them. Handles both the standard `node_modules/.prisma/client` path and custom
|
|
48
49
|
output paths like `../src/generated` or `../generated-client`.
|
|
50
|
+
|
|
51
|
+
`/app` itself is owned by `bun` too: `migrate.sh` runs `prisma generate` as `bun`,
|
|
52
|
+
and a generator output such as `../generated/prisma` (the `flisk resource
|
|
53
|
+
--feature prisma` scaffold) makes Prisma create a new directory directly under
|
|
54
|
+
`/app`, which fails with EACCES while root owns it.
|
|
49
55
|
"""
|
|
50
56
|
return (
|
|
51
57
|
"# Ensure Prisma can write generated files including custom output paths\n"
|
|
52
58
|
+ "RUN mkdir -p " + APP_NODE_MODULES_DIR + " " + APP_SRC_DIR + " " + APP_GENERATED_CLIENT_DIR
|
|
59
|
+
+ " && chown " + BUN_USER + ":" + BUN_USER + " " + APP_DIR
|
|
53
60
|
+ " && chown -R " + BUN_USER + ":" + BUN_USER
|
|
54
61
|
+ " " + APP_PRISMA_DIR + " " + APP_NODE_MODULES_DIR + " " + APP_SRC_DIR + " " + APP_GENERATED_CLIENT_DIR
|
|
55
62
|
+ " " + PRISMA_CONFIG_ABS_PATH + " || true\n"
|
|
@@ -38,6 +38,31 @@ load("./sablier_container_cycle.star",
|
|
|
38
38
|
"sablier_container_labels",
|
|
39
39
|
)
|
|
40
40
|
load("../compose/unsafe_value.star", "reject_unsafe_value")
|
|
41
|
+
load("../constants.star", "PROJECT_NAME_HYPHEN")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _project_slug():
|
|
45
|
+
"""The project name with every character except letters and digits written as `_` plus its six-digit hex code.
|
|
46
|
+
|
|
47
|
+
The result has no `-` in it, so the first `-` in a scoped name always separates the project from the resource. That
|
|
48
|
+
keeps the mapping one-to-one: `team` + `app-api` and `team-app` + `api` get different names, and so do `a-b` and `a_b`.
|
|
49
|
+
"""
|
|
50
|
+
return "".join([ch if ch.isalnum() else "_" + _hex6(ord(ch)) for ch in PROJECT_NAME_HYPHEN.elems()])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hex6(code):
|
|
54
|
+
"""`code` in hex, zero-padded to six digits so every escape has the same width."""
|
|
55
|
+
return ("000000" + "%x" % code)[-6:]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def traefik_scoped_name(name):
|
|
59
|
+
"""Router, middleware and service name for `name`, prefixed with this Flisk project.
|
|
60
|
+
|
|
61
|
+
One Traefik instance serves every Flisk project on the machine, and it keys routers, middlewares and services by name
|
|
62
|
+
alone. Two projects that name a resource the same (`api`, say) would define the same router twice, and Traefik drops both.
|
|
63
|
+
The project prefix keeps them apart.
|
|
64
|
+
"""
|
|
65
|
+
return _project_slug() + "-" + name
|
|
41
66
|
|
|
42
67
|
|
|
43
68
|
def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=None, manifest=None):
|
|
@@ -55,8 +80,9 @@ def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=
|
|
|
55
80
|
TRAEFIK_FRONTEND_ENABLE_HTTP,
|
|
56
81
|
TRAEFIK_FRONTEND_ENABLE_HTTPS,
|
|
57
82
|
)
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
router_name = traefik_scoped_name(res_name)
|
|
84
|
+
middleware_name = router_name + TRAEFIK_MIDDLEWARE_SUFFIX
|
|
85
|
+
|
|
60
86
|
# Check if maintenance feature is enabled
|
|
61
87
|
features = manifest.get('featuresEnabled', []) if manifest else []
|
|
62
88
|
maintenance_middleware = ""
|
|
@@ -66,20 +92,31 @@ def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=
|
|
|
66
92
|
# On-demand scaling: attach the Sablier middleware when opted in via the manifest
|
|
67
93
|
sablier_middleware, sablier_enabled = sablier_middleware_suffix(manifest, res_name)
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- "traefik.http.routers.{
|
|
95
|
+
# Next.js serves under its own basePath, so the prefix must reach it: the route keeps /<name> and adds no stripprefix middleware.
|
|
96
|
+
strips_prefix = not manifest or manifest.get('framework') != 'next'
|
|
97
|
+
if strips_prefix:
|
|
98
|
+
middlewares = middleware_name + maintenance_middleware + sablier_middleware
|
|
99
|
+
middleware_labels = """ - "traefik.http.routers.{router_name}.middlewares={middlewares}"
|
|
74
100
|
- "traefik.http.middlewares.{middleware_name}.stripprefix.prefixes={base_path}"
|
|
75
|
-
|
|
101
|
+
""".format(router_name=router_name, middlewares=middlewares, middleware_name=middleware_name, base_path=base_path)
|
|
102
|
+
else:
|
|
103
|
+
middlewares = (maintenance_middleware + sablier_middleware).lstrip(",")
|
|
104
|
+
middleware_labels = ""
|
|
105
|
+
if middlewares:
|
|
106
|
+
middleware_labels = """ - "traefik.http.routers.{router_name}.middlewares={middlewares}"
|
|
107
|
+
""".format(router_name=router_name, middlewares=middlewares)
|
|
108
|
+
|
|
109
|
+
labels = """ - "{traefik_enable_label}"
|
|
110
|
+
- 'traefik.http.routers.{router_name}.rule={frontend_route_rule}'
|
|
111
|
+
- "traefik.http.routers.{router_name}.entrypoints={frontend_entrypoints}"
|
|
112
|
+
- "traefik.http.routers.{router_name}.priority={router_priority}"
|
|
113
|
+
{middleware_labels} - "traefik.http.services.{router_name}.loadbalancer.server.port={port}"
|
|
76
114
|
- "traefik.docker.network={traefik_network}\"""".format(
|
|
77
|
-
|
|
115
|
+
router_name=router_name,
|
|
78
116
|
traefik_enable_label=TRAEFIK_ENABLE_LABEL,
|
|
79
117
|
frontend_route_rule=frontend_route_rule,
|
|
80
118
|
frontend_entrypoints=frontend_entrypoints,
|
|
81
|
-
|
|
82
|
-
base_path=base_path,
|
|
119
|
+
middleware_labels=middleware_labels,
|
|
83
120
|
port=port,
|
|
84
121
|
router_priority=router_priority,
|
|
85
122
|
traefik_network=TRAEFIK_DOCKER_NETWORK,
|
|
@@ -135,8 +172,10 @@ def get_backend_traefik_labels(
|
|
|
135
172
|
TRAEFIK_BACKEND_ENABLE_HTTP,
|
|
136
173
|
TRAEFIK_BACKEND_ENABLE_HTTPS,
|
|
137
174
|
)
|
|
138
|
-
|
|
139
|
-
|
|
175
|
+
router_name = traefik_scoped_name(resource_entry_name)
|
|
176
|
+
service_name = traefik_scoped_name(traefik_resource_name)
|
|
177
|
+
middleware_name = router_name + TRAEFIK_MIDDLEWARE_SUFFIX
|
|
178
|
+
|
|
140
179
|
# Check if maintenance feature is enabled
|
|
141
180
|
features = manifest.get('featuresEnabled', []) if manifest else []
|
|
142
181
|
maintenance_middleware = ""
|
|
@@ -149,13 +188,13 @@ def get_backend_traefik_labels(
|
|
|
149
188
|
# Build middleware config only if traefik_path is not empty
|
|
150
189
|
if traefik_path:
|
|
151
190
|
middleware_config_lines = [
|
|
152
|
-
' - "traefik.http.routers.' +
|
|
191
|
+
' - "traefik.http.routers.' + router_name + '.middlewares=' + middleware_name + maintenance_middleware + sablier_middleware + '"',
|
|
153
192
|
' - "traefik.http.middlewares.' + middleware_name + '.stripprefix.prefixes=' + traefik_path + '"',
|
|
154
193
|
]
|
|
155
194
|
middleware_config = "\n".join(middleware_config_lines)
|
|
156
195
|
elif sablier_middleware:
|
|
157
196
|
# No strip-prefix path, but still front the router with the Sablier middleware
|
|
158
|
-
middleware_config = ' - "traefik.http.routers.' +
|
|
197
|
+
middleware_config = ' - "traefik.http.routers.' + router_name + '.middlewares=' + sablier_middleware.lstrip(",") + '"'
|
|
159
198
|
else:
|
|
160
199
|
# No middleware if path is empty - router has no middlewares
|
|
161
200
|
middleware_config = ""
|
|
@@ -171,15 +210,15 @@ def get_backend_traefik_labels(
|
|
|
171
210
|
if stack_shared and not backend_route_rule:
|
|
172
211
|
backend_router_labels = ""
|
|
173
212
|
else:
|
|
174
|
-
backend_router_labels = """ - "traefik.http.routers.{
|
|
175
|
-
- "traefik.http.routers.{
|
|
176
|
-
- "traefik.http.routers.{
|
|
213
|
+
backend_router_labels = """ - "traefik.http.routers.{router_name}.rule={backend_route_rule}"
|
|
214
|
+
- "traefik.http.routers.{router_name}.entrypoints={backend_entrypoints}"
|
|
215
|
+
- "traefik.http.routers.{router_name}.service={service_name}"
|
|
177
216
|
{middleware_config}
|
|
178
217
|
""".format(
|
|
179
|
-
|
|
218
|
+
router_name=router_name,
|
|
180
219
|
backend_route_rule=backend_route_rule,
|
|
181
220
|
backend_entrypoints=backend_entrypoints,
|
|
182
|
-
|
|
221
|
+
service_name=service_name,
|
|
183
222
|
middleware_config=middleware_config,
|
|
184
223
|
)
|
|
185
224
|
|
|
@@ -187,25 +226,25 @@ def get_backend_traefik_labels(
|
|
|
187
226
|
# probing a path the service never serves.
|
|
188
227
|
traefik_healthcheck_labels = ""
|
|
189
228
|
if health_path:
|
|
190
|
-
traefik_healthcheck_labels = """ - "traefik.http.services.{
|
|
191
|
-
- "traefik.http.services.{
|
|
192
|
-
- "traefik.http.services.{
|
|
193
|
-
- "traefik.http.services.{
|
|
229
|
+
traefik_healthcheck_labels = """ - "traefik.http.services.{service_name}.loadbalancer.healthcheck.path={health_path}"
|
|
230
|
+
- "traefik.http.services.{service_name}.loadbalancer.healthcheck.interval={health_interval}"
|
|
231
|
+
- "traefik.http.services.{service_name}.loadbalancer.healthcheck.timeout={health_timeout}"
|
|
232
|
+
- "traefik.http.services.{service_name}.loadbalancer.healthcheck.followredirects=false"
|
|
194
233
|
""".format(
|
|
195
|
-
|
|
234
|
+
service_name=service_name,
|
|
196
235
|
health_path=health_path,
|
|
197
236
|
health_interval=TRAEFIK_HEALTHCHECK_INTERVAL,
|
|
198
237
|
health_timeout=TRAEFIK_HEALTHCHECK_TIMEOUT,
|
|
199
238
|
)
|
|
200
239
|
|
|
201
240
|
labels = """ - "{traefik_enable_label}"
|
|
202
|
-
{backend_router_labels} - "traefik.http.services.{
|
|
241
|
+
{backend_router_labels} - "traefik.http.services.{service_name}.loadbalancer.server.port={internal_port}"
|
|
203
242
|
{traefik_healthcheck_labels} - "traefik.docker.network={traefik_network}"
|
|
204
243
|
""".format(
|
|
205
|
-
|
|
244
|
+
router_name=router_name,
|
|
206
245
|
traefik_enable_label=TRAEFIK_ENABLE_LABEL,
|
|
207
246
|
backend_router_labels=backend_router_labels,
|
|
208
|
-
|
|
247
|
+
service_name=service_name,
|
|
209
248
|
internal_port=internal_port,
|
|
210
249
|
traefik_healthcheck_labels=traefik_healthcheck_labels,
|
|
211
250
|
traefik_network=TRAEFIK_DOCKER_NETWORK,
|
|
@@ -233,15 +272,15 @@ def get_backend_traefik_labels(
|
|
|
233
272
|
management_path = get_api_path(stack, manifest) if stack else ""
|
|
234
273
|
|
|
235
274
|
labels += """
|
|
236
|
-
- "traefik.http.routers.{
|
|
237
|
-
- "traefik.http.routers.{
|
|
238
|
-
- "traefik.http.routers.{
|
|
239
|
-
- "traefik.http.routers.{
|
|
275
|
+
- "traefik.http.routers.{router_name}-project.rule={project_rule}"
|
|
276
|
+
- "traefik.http.routers.{router_name}-project.entrypoints={project_entrypoints}"
|
|
277
|
+
- "traefik.http.routers.{router_name}-project.service={service_name}"
|
|
278
|
+
- "traefik.http.routers.{router_name}-project.middlewares={middleware_name}-project{maintenance_middleware}{sablier_middleware}"
|
|
240
279
|
- "traefik.http.middlewares.{middleware_name}-project.stripprefix.prefixes={api_path}"
|
|
241
|
-
- "traefik.http.routers.{
|
|
280
|
+
- "traefik.http.routers.{router_name}-project.priority={router_priority}"
|
|
242
281
|
""".format(
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
router_name=router_name,
|
|
283
|
+
service_name=service_name,
|
|
245
284
|
project_rule=project_rule,
|
|
246
285
|
project_entrypoints=project_entrypoints,
|
|
247
286
|
middleware_name=middleware_name,
|
|
@@ -253,15 +292,15 @@ def get_backend_traefik_labels(
|
|
|
253
292
|
|
|
254
293
|
if management_path and management_path != api_path and not stack_shared:
|
|
255
294
|
labels += """
|
|
256
|
-
- "traefik.http.routers.{
|
|
257
|
-
- "traefik.http.routers.{
|
|
258
|
-
- "traefik.http.routers.{
|
|
259
|
-
- "traefik.http.routers.{
|
|
295
|
+
- "traefik.http.routers.{router_name}-management.rule=Host(`{api_host}`) && PathPrefix(`{management_path}`)"
|
|
296
|
+
- "traefik.http.routers.{router_name}-management.entrypoints={project_entrypoints}"
|
|
297
|
+
- "traefik.http.routers.{router_name}-management.service={service_name}"
|
|
298
|
+
- "traefik.http.routers.{router_name}-management.middlewares={middleware_name}-management{sablier_middleware}"
|
|
260
299
|
- "traefik.http.middlewares.{middleware_name}-management.stripprefix.prefixes={management_path}"
|
|
261
|
-
- "traefik.http.routers.{
|
|
300
|
+
- "traefik.http.routers.{router_name}-management.priority={mgmt_priority}"
|
|
262
301
|
""".format(
|
|
263
|
-
|
|
264
|
-
|
|
302
|
+
router_name=router_name,
|
|
303
|
+
service_name=service_name,
|
|
265
304
|
api_host=TRAEFIK_PROJECT_API_HOST,
|
|
266
305
|
management_path=management_path,
|
|
267
306
|
project_entrypoints=project_entrypoints,
|
|
@@ -273,11 +312,11 @@ def get_backend_traefik_labels(
|
|
|
273
312
|
if old_path_pattern:
|
|
274
313
|
labels += """
|
|
275
314
|
# Redirect middleware for URL restructuring: old path -> new path
|
|
276
|
-
- "traefik.http.middlewares.{
|
|
277
|
-
- "traefik.http.middlewares.{
|
|
278
|
-
- "traefik.http.middlewares.{
|
|
315
|
+
- "traefik.http.middlewares.{router_name}-redirect.redirectregex.regex=^`{old_path_pattern}/(.*)`"
|
|
316
|
+
- "traefik.http.middlewares.{router_name}-redirect.redirectregex.replacement=`{api_path}/$$1`"
|
|
317
|
+
- "traefik.http.middlewares.{router_name}-redirect.redirectregex.permanent=true"
|
|
279
318
|
""".format(
|
|
280
|
-
|
|
319
|
+
router_name=router_name,
|
|
281
320
|
old_path_pattern=old_path_pattern,
|
|
282
321
|
api_path=api_path,
|
|
283
322
|
)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# Config for a request travels to the gateway via headers (X-Wake-*), not
|
|
18
18
|
# the path/query. The route still strips the same `apiPath` prefix the real
|
|
19
19
|
# Docker-label `-project` router strips (get_backend_traefik_labels) before
|
|
20
|
-
# forwarding -- confirmed live against
|
|
20
|
+
# forwarding -- confirmed live against flisk-erp-system (task 7.2) that a
|
|
21
21
|
# backend's app only serves its bare health path (e.g. `/health`), not the
|
|
22
22
|
# full `/api/{name}/health` the public route is matched on; without this, the
|
|
23
23
|
# gateway's own proxy-through would forward the unstripped path once healthy
|
|
@@ -61,7 +61,7 @@ def _container_name(compose_project_name, res_name):
|
|
|
61
61
|
Confirmed empirically against a live Flisk container (analytics-autogenerated
|
|
62
62
|
-data-governance-api-1) -- see design.md D2 Context. Compose projects are now
|
|
63
63
|
<project>-<stack>-autogenerated, so the name starts with the Flisk project too
|
|
64
|
-
(
|
|
64
|
+
(flisk-erp-system-analytics-autogenerated-data-governance-api-1). Always replica 1: a
|
|
65
65
|
deferred resource's replica count is unrelated to which single instance
|
|
66
66
|
the wake gateway needs to address."""
|
|
67
67
|
return compose_project_name + "-" + res_name + "-1"
|
|
@@ -85,6 +85,7 @@ def get_template_header(file_type, generator_name, resource_name='', extra_info=
|
|
|
85
85
|
{c} TO MODIFY THIS CONFIGURATION:
|
|
86
86
|
{c} 1. Edit the source generator in: .tilt/topologies/
|
|
87
87
|
{c} 2. Or update service.json
|
|
88
|
+
{c} Regenerate with: flisk config regenerate
|
|
88
89
|
{c}
|
|
89
90
|
{c} Generation Source: {generator}{resource_line}{extra_line}
|
|
90
91
|
{border}
|
|
@@ -571,7 +571,7 @@ def generate_resource_health_check(resource_name, resource_path, manifest, check
|
|
|
571
571
|
}.get(manifest.get("language", "bun"), ["package.json", "prisma/schema.prisma"])
|
|
572
572
|
required_files = {
|
|
573
573
|
"backend": backend_files,
|
|
574
|
-
"frontend": ["package.json", "index.html", frontend_entry],
|
|
574
|
+
"frontend": ["package.json", "next.config.mjs", "src/app/page.tsx"] if manifest.get("framework") == "next" else ["package.json", "index.html", frontend_entry],
|
|
575
575
|
"library": ["package.json"],
|
|
576
576
|
}.get(app_type, ["package.json"])
|
|
577
577
|
|
|
@@ -111,9 +111,9 @@ def generate_frontend(manifest, backend_manifest=None, write_fn=None):
|
|
|
111
111
|
stack = manifest.get('stack', 'app')
|
|
112
112
|
port = manifest.get('port', BASE_PORT_FRONTEND)
|
|
113
113
|
hmr_port = port + 1000 # HMR port is always port + 1000
|
|
114
|
-
base_path = get_frontend_base_path(manifest)
|
|
115
|
-
vite_base_path = _normalize_vite_base_path(base_path)
|
|
116
114
|
app_name = manifest.get('appName', 'unknown')
|
|
115
|
+
base_path = get_frontend_base_path(manifest, app_name)
|
|
116
|
+
vite_base_path = _normalize_vite_base_path(base_path)
|
|
117
117
|
|
|
118
118
|
backend_port = BASE_PORT_BACKEND
|
|
119
119
|
if backend_manifest:
|
|
@@ -31,6 +31,9 @@ def generate_for_manifest(manifest, backend_manifest=None, write_fn=None):
|
|
|
31
31
|
return None
|
|
32
32
|
|
|
33
33
|
if app_type == 'frontend':
|
|
34
|
+
# Next.js owns its build config (next.config.mjs); there is no Vite config to generate.
|
|
35
|
+
if manifest.get('framework') == 'next':
|
|
36
|
+
return None
|
|
34
37
|
return generate_frontend(manifest, backend_manifest, write_fn)
|
|
35
38
|
elif app_type == 'backend':
|
|
36
39
|
return generate_backend(manifest, write_fn)
|
|
@@ -196,6 +196,9 @@ DEFAULT_SYNCS = {
|
|
|
196
196
|
'bring-your-own': [],
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
# Next.js owns its entry files (no index.html) and rebuilds from package.json or next.config.*, which are not synced.
|
|
200
|
+
NEXT_FRONTEND_SYNCS = ['src', 'public']
|
|
201
|
+
|
|
199
202
|
RUNTIME_CONFIGS = {
|
|
200
203
|
'bun': {
|
|
201
204
|
'image_base': 'oven/bun:1.3.11-alpine',
|
|
@@ -190,7 +190,7 @@ def map_manifest(manifest, target_format='internal'):
|
|
|
190
190
|
def _to_yaml_format(internal_manifest):
|
|
191
191
|
"""Convert internal format to YAML hierarchical format."""
|
|
192
192
|
return {
|
|
193
|
-
'apiVersion': 'tilt.
|
|
193
|
+
'apiVersion': 'tilt.flisk.io/v1',
|
|
194
194
|
'kind': 'ResourceManifest',
|
|
195
195
|
'metadata': {
|
|
196
196
|
'name': internal_manifest.get('appName', ''),
|
|
@@ -20,6 +20,7 @@ load("./constants.star",
|
|
|
20
20
|
"MANIFEST_FILENAME",
|
|
21
21
|
"VALID_APP_TYPES",
|
|
22
22
|
"DEFAULT_SYNCS",
|
|
23
|
+
"NEXT_FRONTEND_SYNCS",
|
|
23
24
|
"AUTOGENERATED_CONFIG_PATHS",
|
|
24
25
|
"TRAEFIK_DEFAULTS",
|
|
25
26
|
)
|
|
@@ -90,6 +91,7 @@ _GENERATED_STRING_RULES = [
|
|
|
90
91
|
(['databaseName'], _ALPHANUM + '_-', ''),
|
|
91
92
|
(['stack'], _ALPHANUM + '_-', ''),
|
|
92
93
|
(['image'], _ALPHANUM + '._/:@-', ''),
|
|
94
|
+
(['basePath'], _PATH_CHARS, '/'),
|
|
93
95
|
]
|
|
94
96
|
|
|
95
97
|
def generated_string_error(manifest, resource_path=""):
|
|
@@ -200,7 +202,8 @@ def normalize(manifest, resource_path=""):
|
|
|
200
202
|
if normalized.get('syncs') == None or normalized.get('syncs') == []:
|
|
201
203
|
normalized['syncs'] = _compute_syncs(
|
|
202
204
|
normalized['appType'],
|
|
203
|
-
normalized.get('featuresEnabled', [])
|
|
205
|
+
normalized.get('featuresEnabled', []),
|
|
206
|
+
normalized.get('framework')
|
|
204
207
|
)
|
|
205
208
|
|
|
206
209
|
# Compute backendName for frontends if missing
|
|
@@ -339,13 +342,14 @@ def _determine_app_type(manifest, resource_path):
|
|
|
339
342
|
# Default to backend
|
|
340
343
|
return "backend"
|
|
341
344
|
|
|
342
|
-
def _compute_syncs(app_type, features):
|
|
345
|
+
def _compute_syncs(app_type, features, framework=None):
|
|
343
346
|
"""
|
|
344
347
|
Compute default sync paths based on app type and features.
|
|
345
348
|
|
|
346
349
|
Args:
|
|
347
350
|
app_type: App type string
|
|
348
351
|
features: List of feature strings
|
|
352
|
+
framework: Frontend framework id (Next.js has no index.html, so it syncs less than a Vite SPA)
|
|
349
353
|
|
|
350
354
|
Returns:
|
|
351
355
|
List of sync paths
|
|
@@ -353,7 +357,9 @@ def _compute_syncs(app_type, features):
|
|
|
353
357
|
syncs = []
|
|
354
358
|
|
|
355
359
|
# Start with default syncs for app type
|
|
356
|
-
if app_type
|
|
360
|
+
if app_type == 'frontend' and framework == 'next':
|
|
361
|
+
syncs.extend(NEXT_FRONTEND_SYNCS)
|
|
362
|
+
elif app_type in DEFAULT_SYNCS:
|
|
357
363
|
syncs.extend(DEFAULT_SYNCS[app_type])
|
|
358
364
|
|
|
359
365
|
# Add feature-specific syncs
|
|
@@ -321,7 +321,7 @@ def _live_update_sync_source(project_root, full_res_path, sync_path):
|
|
|
321
321
|
return relative_path
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun', go_live_reload=False):
|
|
324
|
+
def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun', go_live_reload=False, node_frontend=False):
|
|
325
325
|
"""Build live update sync and run rules for a resource.
|
|
326
326
|
|
|
327
327
|
Python backends reload their own process after a sync (uvicorn --reload), so they have
|
|
@@ -363,7 +363,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
|
|
|
363
363
|
dest = '/app/' + full_res_path + '/' + sync_path
|
|
364
364
|
live_update_rules.append(sync(full_sync_path, dest))
|
|
365
365
|
|
|
366
|
-
if language == 'python' or language == 'node':
|
|
366
|
+
if language == 'python' or language == 'node' or node_frontend:
|
|
367
367
|
return live_update_rules
|
|
368
368
|
|
|
369
369
|
package_json_path = '/app/' + full_res_path + '/package.json'
|
|
@@ -407,6 +407,9 @@ def _wake_compose_invocation(project_root, resource_path, res_name, resource_nam
|
|
|
407
407
|
|
|
408
408
|
def _image_target(config):
|
|
409
409
|
"""The Dockerfile target Tilt builds and the deferStart prebuild must match: `development` for Go with `dev.liveReload`."""
|
|
410
|
+
# Next.js runs `next dev` in the development image, so a saved file hot-reloads through the synced sources.
|
|
411
|
+
if Docker.is_next_frontend(config.get('manifest', {})):
|
|
412
|
+
return 'development'
|
|
410
413
|
return 'development' if Docker.go_live_reload(config.get('manifest', {})) else 'production'
|
|
411
414
|
|
|
412
415
|
|
|
@@ -426,7 +429,7 @@ def _register_deferred_image_prebuild(config, auto_init_apps):
|
|
|
426
429
|
"""Build a deferStart resource's image during `flisk up` without creating its container.
|
|
427
430
|
|
|
428
431
|
`auto_init=False` on the dc_resource skips its docker_build too (confirmed
|
|
429
|
-
live on
|
|
432
|
+
live on flisk-erp-system, task 7.2), so a fresh project's first request would
|
|
430
433
|
find no image and the wake gateway would fall back to `tilt trigger`, which
|
|
431
434
|
queues behind every other build. This local_resource builds the same image
|
|
432
435
|
(same Dockerfile and target as docker_build: `production`, or `development` for Go with `dev.liveReload`; host network, and an allowlist
|
|
@@ -548,6 +551,8 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
548
551
|
config['syncs'],
|
|
549
552
|
project_root=config.get('project_root', ''),
|
|
550
553
|
language=config.get('manifest', {}).get('language', 'bun'),
|
|
554
|
+
# A Next.js image is Node with no Bun install step: syncing sources is enough, `next dev` reloads itself.
|
|
555
|
+
node_frontend=Docker.is_next_frontend(config.get('manifest', {})),
|
|
551
556
|
go_live_reload=Docker.go_live_reload(config.get('manifest', {})),
|
|
552
557
|
)
|
|
553
558
|
_register_docker_build(config, live_update_rules)
|
|
@@ -610,7 +615,7 @@ def _register_frontend_typecheck(res, service_dir, typecheck_cmd):
|
|
|
610
615
|
# Fixed script, variable parts passed as argv ($1 = service dir, $2 = typecheck command), never spliced into the shell text.
|
|
611
616
|
cmd=[
|
|
612
617
|
'sh', '-c',
|
|
613
|
-
'cd "$1" && { bunx tsc --version >/dev/null 2>&1 || { echo "typecheck needs host dependencies: run bun install in $1"; exit 1; }; } && eval "$2"',
|
|
618
|
+
'cd "$1" && { bunx tsc --version >/dev/null 2>&1 && test -f "$1/node_modules/vite/package.json" || { echo "typecheck needs host dependencies: run bun install in $1"; exit 1; }; } && eval "$2"',
|
|
614
619
|
'sh', service_dir, typecheck_cmd,
|
|
615
620
|
],
|
|
616
621
|
deps=[service_dir + '/src', service_dir + '/tsconfig.json'],
|
package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star
CHANGED
|
@@ -7,37 +7,63 @@
|
|
|
7
7
|
# Auto-inject configuration from internal dependencies
|
|
8
8
|
# =============================================================================
|
|
9
9
|
|
|
10
|
-
load("../../../../../../../discovery/registry.star", "get_resource_by_name")
|
|
10
|
+
load("../../../../../../../discovery/registry.star", "get_app_resources", "get_resource_by_name")
|
|
11
|
+
load("../../../../../platform/docker/networking/traefik_helpers.star", "cli_api_path")
|
|
12
|
+
|
|
13
|
+
def _find_backend_resource(resource_name):
|
|
14
|
+
"""Find a discovered resource by its own name, e.g. "api".
|
|
15
|
+
|
|
16
|
+
Registry services are stacks (`app`) whose resources are the runnable pieces (`api`, `web`), so the lookup
|
|
17
|
+
must search resources rather than service names. Frontends are skipped.
|
|
18
|
+
"""
|
|
19
|
+
for service in get_app_resources():
|
|
20
|
+
for resource in service.get("resources", []):
|
|
21
|
+
if resource.get("name") != resource_name:
|
|
22
|
+
continue
|
|
23
|
+
app_type = resource.get("_manifest", {}).get("appType") or resource.get("appType")
|
|
24
|
+
if app_type == "frontend":
|
|
25
|
+
continue
|
|
26
|
+
return resource
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_frontend_backend(manifest):
|
|
31
|
+
"""Find the backend a frontend talks to: `backendName` first, then the first API `dependsOn` entry.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
tuple: (backend resource name, backend manifest), or (None, None) when the frontend has no discovered backend
|
|
35
|
+
"""
|
|
36
|
+
backend_name = manifest.get("backendName")
|
|
37
|
+
if backend_name:
|
|
38
|
+
backend = _find_backend_resource(backend_name)
|
|
39
|
+
if backend:
|
|
40
|
+
return backend_name, backend.get("_manifest", {})
|
|
41
|
+
|
|
42
|
+
for dep_name in manifest.get("dependsOn", []):
|
|
43
|
+
dep = _find_backend_resource(dep_name)
|
|
44
|
+
if dep and dep.get("_manifest", {}).get("appType") in ["backend", "mcp", "agent"]:
|
|
45
|
+
return dep_name, dep.get("_manifest", {})
|
|
46
|
+
|
|
47
|
+
return None, None
|
|
48
|
+
|
|
11
49
|
|
|
12
50
|
def resolve_backend_api_path(manifest):
|
|
13
|
-
"""Resolve
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
51
|
+
"""Resolve the URL path a frontend uses to reach its backend, e.g. "/api/api".
|
|
52
|
+
|
|
53
|
+
The path is the backend's route on api.{project}.localhost: its explicit `apiPath` when set, otherwise
|
|
54
|
+
`/api/{name}` with a trailing `-api` stripped, the same rule `flisk up` prints and Traefik serves.
|
|
55
|
+
Frontends that have no discovered backend get None, so callers keep their stack-management fallback.
|
|
56
|
+
|
|
18
57
|
Args:
|
|
19
58
|
manifest: Frontend service manifest
|
|
20
|
-
|
|
59
|
+
|
|
21
60
|
Returns:
|
|
22
|
-
str: API path
|
|
61
|
+
str: API path of the frontend's backend, or None if there is no backend
|
|
23
62
|
"""
|
|
24
|
-
backend_name = manifest
|
|
25
|
-
if
|
|
26
|
-
return None
|
|
27
|
-
|
|
28
|
-
# Try to find the backend service
|
|
29
|
-
backend_service = get_resource_by_name(backend_name)
|
|
30
|
-
if not backend_service:
|
|
63
|
+
backend_name, backend_manifest = _find_frontend_backend(manifest)
|
|
64
|
+
if backend_name == None:
|
|
31
65
|
return None
|
|
32
|
-
|
|
33
|
-
# Get the backend manifest
|
|
34
|
-
backend_resources = backend_service.get("resources", [])
|
|
35
|
-
for resource in backend_resources:
|
|
36
|
-
if resource.get("name") == backend_name:
|
|
37
|
-
backend_manifest = resource.get("_manifest", {})
|
|
38
|
-
return backend_manifest.get("apiPath")
|
|
39
|
-
|
|
40
|
-
return None
|
|
66
|
+
return cli_api_path(backend_name, backend_manifest)
|
|
41
67
|
|
|
42
68
|
|
|
43
69
|
def resolve_dependency_api_urls(manifest):
|