@tdk-landscape/tdk-cli-core 1.3.91 → 1.3.92
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/cli/dist/backend-frameworks/mcp.js +87 -0
- package/cli/dist/backend-frameworks/registry.js +16 -0
- package/cli/dist/commands/completion.js +3 -3
- package/cli/dist/commands/doctor.js +8 -2
- package/cli/dist/commands/networks.js +2 -1
- package/cli/dist/commands/project.js +15 -2
- package/cli/dist/commands/resource.js +19 -13
- package/cli/dist/commands/up.js +37 -1
- package/cli/dist/types/index.js +1 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/doctor-runtime.js +3 -2
- package/cli/dist/utils/doctor-wiring.js +2 -1
- package/cli/dist/utils/env-validator.js +118 -30
- package/cli/dist/utils/resource-kind.js +8 -0
- package/cli/dist/utils/service-manifest.js +23 -3
- package/cli/dist/utils/service-urls.js +4 -3
- package/cli/dist/utils/services.js +2 -1
- package/cli/dist/utils/smoke.js +225 -0
- package/cli/package.json +1 -1
- package/engine/schemas/service-schema.json +73 -27
- package/engine/topologies/platform/docker/auth/auth.star +66 -28
- package/engine/topologies/platform/docker/compose/compose.star +8 -26
- package/engine/topologies/platform/docker/layers/l3_builder_layers.star +3 -0
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +3 -0
- package/engine/topologies/platform/docker/networking/api_path_constants.star +1 -57
- package/engine/topologies/platform/docker/secrets/env_secrets.star +72 -0
- package/engine/topologies/tilt/common/utils.star +0 -2
- package/engine/topologies/tilt/common/utils_env.star +72 -25
- package/engine/topologies/tilt/manifest/constants.star +2 -0
- package/engine/topologies/tilt/manifest/json_to_yaml.py +4 -0
- package/engine/topologies/tilt/manifest/loader.star +7 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/env.star +43 -96
- package/package.json +1 -1
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"type": "string"
|
|
108
108
|
},
|
|
109
109
|
"appType": {
|
|
110
|
-
"description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React, Vue, Svelte, Preact, Lit, Solid, Qwik or Vanilla TypeScript, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library\n• bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
|
|
110
|
+
"description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React, Vue, Svelte, Preact, Lit, Solid, Qwik or Vanilla TypeScript, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library\n• mcp - Model Context Protocol server (an HTTP service on Bun, run like a backend)\n• bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
|
|
111
111
|
"enum": [
|
|
112
112
|
"backend",
|
|
113
113
|
"frontend",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"worker",
|
|
116
116
|
"migrator",
|
|
117
117
|
"library",
|
|
118
|
+
"mcp",
|
|
118
119
|
"bring-your-own"
|
|
119
120
|
],
|
|
120
121
|
"type": "string"
|
|
@@ -179,10 +180,10 @@
|
|
|
179
180
|
"type": "object"
|
|
180
181
|
},
|
|
181
182
|
"secrets": {
|
|
182
|
-
"description": "
|
|
183
|
+
"description": "Names of secrets the resource needs. By default each is read from the project .env (gitignored) and injected into the container; with TDK_SECRET_PROVIDER=infisical they are left to Infisical. NO secret values in params/envVars or anywhere in service.json.",
|
|
183
184
|
"properties": {
|
|
184
185
|
"required": {
|
|
185
|
-
"description": "Required secret names.
|
|
186
|
+
"description": "Required secret names. The resource does not start if any is missing from the project .env.",
|
|
186
187
|
"items": {
|
|
187
188
|
"type": "string",
|
|
188
189
|
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
"uniqueItems": true
|
|
192
193
|
},
|
|
193
194
|
"optional": {
|
|
194
|
-
"description": "Optional secret names.
|
|
195
|
+
"description": "Optional secret names. The resource starts with the variable empty when it is missing from the project .env.",
|
|
195
196
|
"items": {
|
|
196
197
|
"type": "string",
|
|
197
198
|
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
|
@@ -226,34 +227,13 @@
|
|
|
226
227
|
},
|
|
227
228
|
"framework": {
|
|
228
229
|
"description": "Framework provider id. For frontend resources it selects the Vite and TypeScript config (omitted means react, legacy manifests). For backend resources it selects the HTTP framework on the Bun runtime (omitted means hono; not used with a language that owns its runtime, such as python). Valid ids come from the CLI provider registries, which reject unknown ids before writing a resource.",
|
|
229
|
-
"examples": [
|
|
230
|
-
"react",
|
|
231
|
-
"vue",
|
|
232
|
-
"svelte",
|
|
233
|
-
"preact",
|
|
234
|
-
"lit",
|
|
235
|
-
"solid",
|
|
236
|
-
"qwik",
|
|
237
|
-
"vanilla",
|
|
238
|
-
"hono",
|
|
239
|
-
"express",
|
|
240
|
-
"elysia",
|
|
241
|
-
"fastify",
|
|
242
|
-
"nestjs",
|
|
243
|
-
"koa",
|
|
244
|
-
"h3"
|
|
245
|
-
],
|
|
230
|
+
"examples": ["react", "vue", "solid"],
|
|
246
231
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
247
232
|
"type": "string"
|
|
248
233
|
},
|
|
249
234
|
"language": {
|
|
250
235
|
"description": "Backend language provider id used to scaffold and run the service. Only meaningful for backend resources; omitted means bun (legacy manifests). Valid ids come from the CLI provider registry.",
|
|
251
|
-
"examples": [
|
|
252
|
-
"bun",
|
|
253
|
-
"python",
|
|
254
|
-
"go",
|
|
255
|
-
"rust"
|
|
256
|
-
],
|
|
236
|
+
"examples": ["bun", "python", "go"],
|
|
257
237
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
258
238
|
"type": "string"
|
|
259
239
|
},
|
|
@@ -360,6 +340,72 @@
|
|
|
360
340
|
},
|
|
361
341
|
"type": "array"
|
|
362
342
|
},
|
|
343
|
+
"smoke": {
|
|
344
|
+
"description": "Post-start check run by `tdk up` through the public proxy URL (not inside the container): write a record, read it back. `tdk up` fails if a step misses. Absent means no smoke check. Workers and resources with exposeViaProxy false are skipped.",
|
|
345
|
+
"type": "object",
|
|
346
|
+
"required": [
|
|
347
|
+
"via",
|
|
348
|
+
"steps"
|
|
349
|
+
],
|
|
350
|
+
"additionalProperties": false,
|
|
351
|
+
"properties": {
|
|
352
|
+
"via": {
|
|
353
|
+
"const": "proxy",
|
|
354
|
+
"description": "Only `proxy` is supported: the request goes to the URL `tdk up` prints."
|
|
355
|
+
},
|
|
356
|
+
"timeoutSeconds": {
|
|
357
|
+
"type": "number",
|
|
358
|
+
"exclusiveMinimum": 0,
|
|
359
|
+
"default": 60,
|
|
360
|
+
"description": "How long to wait for the route to come up. Connection errors and 404/502/503/504 are retried until then; any other wrong answer fails at once."
|
|
361
|
+
},
|
|
362
|
+
"steps": {
|
|
363
|
+
"type": "array",
|
|
364
|
+
"minItems": 1,
|
|
365
|
+
"items": {
|
|
366
|
+
"type": "object",
|
|
367
|
+
"required": [
|
|
368
|
+
"path"
|
|
369
|
+
],
|
|
370
|
+
"additionalProperties": false,
|
|
371
|
+
"properties": {
|
|
372
|
+
"name": {
|
|
373
|
+
"type": "string"
|
|
374
|
+
},
|
|
375
|
+
"method": {
|
|
376
|
+
"type": "string",
|
|
377
|
+
"default": "GET"
|
|
378
|
+
},
|
|
379
|
+
"path": {
|
|
380
|
+
"type": "string",
|
|
381
|
+
"pattern": "^/(?!/)",
|
|
382
|
+
"description": "Appended to the service's public base URL. `{{name}}` is replaced by a value saved by an earlier step."
|
|
383
|
+
},
|
|
384
|
+
"body": {
|
|
385
|
+
"description": "JSON request body."
|
|
386
|
+
},
|
|
387
|
+
"expect": {
|
|
388
|
+
"type": "integer",
|
|
389
|
+
"minimum": 100,
|
|
390
|
+
"maximum": 599,
|
|
391
|
+
"default": 200
|
|
392
|
+
},
|
|
393
|
+
"bodyContains": {
|
|
394
|
+
"type": "string"
|
|
395
|
+
},
|
|
396
|
+
"save": {
|
|
397
|
+
"type": "object",
|
|
398
|
+
"additionalProperties": {
|
|
399
|
+
"type": "string",
|
|
400
|
+
"pattern": "^\\$\\."
|
|
401
|
+
},
|
|
402
|
+
"description": "Saves response fields for later steps, for example {\"id\": \"$.id\"}."
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
363
409
|
"traefik": {
|
|
364
410
|
"description": "Custom Traefik routing. Auto-generated if not specified.",
|
|
365
411
|
"properties": {
|
|
@@ -12,38 +12,80 @@
|
|
|
12
12
|
# them as the auth_config dict.
|
|
13
13
|
# =============================================================================
|
|
14
14
|
|
|
15
|
-
# Default values for auth configuration
|
|
15
|
+
# Default values for auth configuration.
|
|
16
|
+
#
|
|
17
|
+
# There is deliberately no default JWT secret: a secret that is the same in every project is not a secret. The JWT secret is
|
|
18
|
+
# generated once per project by the CLI into the project `.env` (JWT_SECRET) and Compose reads it from there.
|
|
16
19
|
DEFAULT_AUTH_MODE = 'local-jwt'
|
|
17
|
-
DEFAULT_JWT_SECRET = 'local-development-secret-min-32-chars-long'
|
|
18
20
|
DEFAULT_IDENTITY_RESOURCE_URL = 'http://identity-service:3000'
|
|
19
21
|
|
|
22
|
+
# Shown by Compose when the project `.env` has no JWT_SECRET.
|
|
23
|
+
JWT_SECRET_MISSING_MESSAGE = 'JWT_SECRET is not set. Run `tdk up` (it adds a generated one to the project .env) or add JWT_SECRET to .env'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _setting(auth_config, key, default):
|
|
27
|
+
"""A configured value, or the default when the key is missing OR present with a None value.
|
|
28
|
+
|
|
29
|
+
Callers build the config dict with every key present and None for anything unset, and `dict.get(key, default)` returns that
|
|
30
|
+
None instead of the default, which used to turn into the literal text `AUTH_MODE=None` in generated files.
|
|
31
|
+
"""
|
|
32
|
+
if not auth_config:
|
|
33
|
+
return default
|
|
34
|
+
value = auth_config.get(key)
|
|
35
|
+
if value == None or value == '':
|
|
36
|
+
return default
|
|
37
|
+
return value
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
|
|
40
|
+
def _first_set(*values):
|
|
41
|
+
"""The first value that is not None or an empty string, else None."""
|
|
42
|
+
for value in values:
|
|
43
|
+
if value != None and value != '':
|
|
44
|
+
return value
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def auth_config_from_manifest(manifest):
|
|
49
|
+
"""Build the auth configuration both the env file and the Compose entry use, from one place.
|
|
50
|
+
|
|
51
|
+
The two writers used to build this separately and drifted: the env file read `identityServiceUrl` while Compose read
|
|
52
|
+
`envVars.IDENTITY_RESOURCE_URL`, so setting one field changed only one output.
|
|
53
|
+
|
|
54
|
+
- authMode: the manifest's `authMode`, else `identity-service` when `dependsOn` contains `identity`, else unset (local-jwt).
|
|
55
|
+
- identityServiceUrl: the manifest's `identityServiceUrl`. The deprecated `params` / `envVars` entry named
|
|
56
|
+
`IDENTITY_RESOURCE_URL` is still honoured when the field is absent, so an existing manifest keeps working.
|
|
23
57
|
|
|
24
58
|
Args:
|
|
25
|
-
|
|
59
|
+
manifest: The service manifest dict (may be None)
|
|
26
60
|
|
|
27
61
|
Returns:
|
|
28
|
-
|
|
62
|
+
dict: {'authMode': str or None, 'identityServiceUrl': str or None}; None means "use the default"
|
|
29
63
|
"""
|
|
30
|
-
if not
|
|
31
|
-
return
|
|
32
|
-
|
|
64
|
+
if not manifest:
|
|
65
|
+
return {'authMode': None, 'identityServiceUrl': None}
|
|
66
|
+
|
|
67
|
+
auth_mode = _first_set(
|
|
68
|
+
manifest.get('authMode'),
|
|
69
|
+
'identity-service' if 'identity' in (manifest.get('dependsOn') or []) else None,
|
|
70
|
+
)
|
|
71
|
+
identity_url = _first_set(
|
|
72
|
+
manifest.get('identityServiceUrl'),
|
|
73
|
+
(manifest.get('params') or {}).get('IDENTITY_RESOURCE_URL'),
|
|
74
|
+
(manifest.get('envVars') or {}).get('IDENTITY_RESOURCE_URL'),
|
|
75
|
+
)
|
|
76
|
+
return {'authMode': auth_mode, 'identityServiceUrl': identity_url}
|
|
33
77
|
|
|
34
78
|
|
|
35
|
-
def
|
|
36
|
-
"""Get
|
|
79
|
+
def get_auth_mode(auth_config):
|
|
80
|
+
"""Get auth mode from auth config with default fallback.
|
|
37
81
|
|
|
38
82
|
Args:
|
|
39
83
|
auth_config: Auth configuration dictionary (may be None or empty)
|
|
40
84
|
|
|
41
85
|
Returns:
|
|
42
|
-
str:
|
|
86
|
+
str: Auth mode ('local-jwt' or 'identity-service')
|
|
43
87
|
"""
|
|
44
|
-
|
|
45
|
-
return DEFAULT_JWT_SECRET
|
|
46
|
-
return auth_config.get('jwtSecret', DEFAULT_JWT_SECRET)
|
|
88
|
+
return _setting(auth_config, 'authMode', DEFAULT_AUTH_MODE)
|
|
47
89
|
|
|
48
90
|
|
|
49
91
|
def get_identity_resource_url(auth_config):
|
|
@@ -55,9 +97,7 @@ def get_identity_resource_url(auth_config):
|
|
|
55
97
|
Returns:
|
|
56
98
|
str: URL for identity service
|
|
57
99
|
"""
|
|
58
|
-
|
|
59
|
-
return DEFAULT_IDENTITY_RESOURCE_URL
|
|
60
|
-
return auth_config.get('identityServiceUrl', DEFAULT_IDENTITY_RESOURCE_URL)
|
|
100
|
+
return _setting(auth_config, 'identityServiceUrl', DEFAULT_IDENTITY_RESOURCE_URL)
|
|
61
101
|
|
|
62
102
|
|
|
63
103
|
def get_auth_config(auth_config):
|
|
@@ -67,11 +107,10 @@ def get_auth_config(auth_config):
|
|
|
67
107
|
auth_config: Auth configuration dictionary (may be None or empty)
|
|
68
108
|
|
|
69
109
|
Returns:
|
|
70
|
-
struct: Auth configuration with mode
|
|
110
|
+
struct: Auth configuration with mode and identity_resource_url (the JWT secret lives in the project .env, not here)
|
|
71
111
|
"""
|
|
72
112
|
return struct(
|
|
73
113
|
mode = get_auth_mode(auth_config),
|
|
74
|
-
jwt_secret = get_jwt_secret(auth_config),
|
|
75
114
|
identity_resource_url = get_identity_resource_url(auth_config),
|
|
76
115
|
)
|
|
77
116
|
|
|
@@ -113,12 +152,12 @@ def generate_docker_compose_auth_env(auth_config):
|
|
|
113
152
|
str: Formatted auth environment variables for docker-compose
|
|
114
153
|
"""
|
|
115
154
|
auth_mode = get_auth_mode(auth_config)
|
|
116
|
-
jwt_secret = get_jwt_secret(auth_config)
|
|
117
155
|
identity_resource_url = get_identity_resource_url(auth_config)
|
|
118
156
|
|
|
119
157
|
if auth_mode == 'local-jwt':
|
|
158
|
+
# Required from the project .env (see JWT_SECRET_MISSING_MESSAGE); there is no shared default.
|
|
120
159
|
return """ - AUTH_MODE=local-jwt
|
|
121
|
-
- JWT_SECRET=${JWT_SECRET
|
|
160
|
+
- JWT_SECRET=${JWT_SECRET:?%(message)s}""" % {'message': JWT_SECRET_MISSING_MESSAGE}
|
|
122
161
|
else:
|
|
123
162
|
return """ - AUTH_MODE=identity-service
|
|
124
163
|
- IDENTITY_RESOURCE_URL=${IDENTITY_RESOURCE_URL:-%(identity_resource_url)s}""" % {'identity_resource_url': identity_resource_url}
|
|
@@ -128,7 +167,9 @@ def generate_env_file_auth_section(auth_config):
|
|
|
128
167
|
"""Generate .env file auth section content.
|
|
129
168
|
|
|
130
169
|
This function returns the formatted authentication configuration section
|
|
131
|
-
for .env.backend.autogenerated files.
|
|
170
|
+
for .env.backend.autogenerated files. These files are generated, checked by
|
|
171
|
+
`tdk config verify` and never carry secrets, so there is no JWT secret here:
|
|
172
|
+
in local-jwt mode Compose takes JWT_SECRET from the project .env.
|
|
132
173
|
|
|
133
174
|
Args:
|
|
134
175
|
auth_config: Auth configuration dictionary (may be None or empty)
|
|
@@ -137,7 +178,6 @@ def generate_env_file_auth_section(auth_config):
|
|
|
137
178
|
str: Formatted auth section for .env files
|
|
138
179
|
"""
|
|
139
180
|
auth_mode = get_auth_mode(auth_config)
|
|
140
|
-
jwt_secret = get_jwt_secret(auth_config)
|
|
141
181
|
identity_resource_url = get_identity_resource_url(auth_config)
|
|
142
182
|
|
|
143
183
|
content = """
|
|
@@ -146,9 +186,7 @@ def generate_env_file_auth_section(auth_config):
|
|
|
146
186
|
AUTH_MODE={auth_mode}
|
|
147
187
|
""".format(auth_mode=auth_mode)
|
|
148
188
|
|
|
149
|
-
if auth_mode
|
|
150
|
-
content += "JWT_SECRET={jwt_secret}\n".format(jwt_secret=jwt_secret)
|
|
151
|
-
else:
|
|
189
|
+
if auth_mode != 'local-jwt':
|
|
152
190
|
content += "IDENTITY_RESOURCE_URL={identity_resource_url}\n".format(identity_resource_url=identity_resource_url)
|
|
153
191
|
|
|
154
192
|
return content
|
|
@@ -156,8 +194,8 @@ AUTH_MODE={auth_mode}
|
|
|
156
194
|
|
|
157
195
|
# Export all functions as a struct for easy importing
|
|
158
196
|
AuthConfig = struct(
|
|
197
|
+
from_manifest = auth_config_from_manifest,
|
|
159
198
|
get_auth_mode = get_auth_mode,
|
|
160
|
-
get_jwt_secret = get_jwt_secret,
|
|
161
199
|
get_identity_resource_url = get_identity_resource_url,
|
|
162
200
|
get_auth_config = get_auth_config,
|
|
163
201
|
is_local_jwt_mode = is_local_jwt_mode,
|
|
@@ -20,6 +20,7 @@ load("./compose_helpers.star", "with_runtime_image_tag", "compose_build_config")
|
|
|
20
20
|
load("../networking/traefik.star", "get_frontend_traefik_labels", "get_backend_traefik_labels")
|
|
21
21
|
load("../networking/api_path_constants.star", "get_api_path_for_domain", "get_frontend_base_path")
|
|
22
22
|
load("../secrets/infisical.star", "get_infisical_environment_vars")
|
|
23
|
+
load("../secrets/env_secrets.star", "get_secret_environment_lines")
|
|
23
24
|
load("../auth/auth.star", "AuthConfig")
|
|
24
25
|
|
|
25
26
|
load("../constants.star", "PlatformDockerConstants")
|
|
@@ -189,32 +190,12 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
189
190
|
nats_queue = nats_cfg.get('queueGroup', stack + '_backend_svc')
|
|
190
191
|
nats_url = PlatformDockerConstants.NATS_URL
|
|
191
192
|
|
|
192
|
-
# Auth configuration
|
|
193
|
-
|
|
194
|
-
# Auto-detect identity-service mode if 'identity' is in dependsOn
|
|
195
|
-
internal_deps = manifest.get('dependsOn', []) if manifest else []
|
|
196
|
-
has_identity_dep = 'identity' in internal_deps
|
|
197
|
-
|
|
198
|
-
# If manifest has authMode, use it; otherwise auto-detect based on identity dependency
|
|
199
|
-
manifest_auth_mode = manifest.get('authMode') if manifest else None
|
|
200
|
-
if manifest_auth_mode:
|
|
201
|
-
auth_mode = manifest_auth_mode
|
|
202
|
-
elif has_identity_dep:
|
|
203
|
-
auth_mode = 'identity-service'
|
|
204
|
-
else:
|
|
205
|
-
auth_mode = None
|
|
206
|
-
|
|
207
|
-
auth_config = {
|
|
208
|
-
'authMode': auth_mode,
|
|
209
|
-
'jwtSecret': manifest.get('jwtSecret') if manifest else None,
|
|
210
|
-
'identityServiceUrl': manifest.get('envVars', {}).get('IDENTITY_RESOURCE_URL') if manifest else None,
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
# If we auto-detected identity-service mode but no URL is set, use default
|
|
214
|
-
if auth_mode == 'identity-service' and not auth_config['identityServiceUrl']:
|
|
215
|
-
auth_config['identityServiceUrl'] = AuthConfig.get_identity_resource_url(None)
|
|
193
|
+
# Auth configuration: one shared builder keeps this identical to the generated env file (see AuthConfig.from_manifest).
|
|
194
|
+
auth_config = AuthConfig.from_manifest(manifest)
|
|
216
195
|
|
|
217
196
|
auth_env = AuthConfig.generate_docker_compose_auth_env(auth_config)
|
|
197
|
+
# secrets.required / secrets.optional, read from the project .env by Compose (empty with TDK_SECRET_PROVIDER=infisical)
|
|
198
|
+
secrets_env = get_secret_environment_lines(manifest, res_name)
|
|
218
199
|
|
|
219
200
|
# Use .autogenerated/Dockerfile.app.autogenerated by default for build config (primary only)
|
|
220
201
|
if manifest and manifest.get('appType') == 'bring-your-own':
|
|
@@ -240,7 +221,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
240
221
|
|
|
241
222
|
# REMOVED: External port exposure - all services accessed via Traefik only
|
|
242
223
|
# Services are available at {project}.localhost/api/v1/{service}
|
|
243
|
-
#
|
|
224
|
+
# Inside the Docker network a service listens on its own port (manifest.port, passed to it as PORT), for service-to-service calls
|
|
244
225
|
ports_section = "" # No external ports - Traefik-only access
|
|
245
226
|
|
|
246
227
|
# Determine service name based on instance
|
|
@@ -325,7 +306,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
325
306
|
# to skip Prisma auto-migration at container boot (e.g. while diagnosing a
|
|
326
307
|
# migration/CLI issue) without touching the generator or any service code.
|
|
327
308
|
- AUTO_MIGRATE=${{AUTO_MIGRATE:-true}}
|
|
328
|
-
{auth_env}
|
|
309
|
+
{auth_env}{secrets_env}
|
|
329
310
|
labels:
|
|
330
311
|
{traefik_labels}
|
|
331
312
|
networks:
|
|
@@ -373,6 +354,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
373
354
|
healthcheck_section=healthcheck_section,
|
|
374
355
|
infisical_env=infisical_env,
|
|
375
356
|
auth_env=auth_env,
|
|
357
|
+
secrets_env=secrets_env,
|
|
376
358
|
network_traefik_public=PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC,
|
|
377
359
|
network_backend=PlatformDockerConstants.NETWORK_BACKEND,
|
|
378
360
|
network_database=PlatformDockerConstants.NETWORK_DATABASE,
|
|
@@ -28,6 +28,9 @@ load('./prisma/prisma_build.star',
|
|
|
28
28
|
'prisma_normalize_output',
|
|
29
29
|
'L3_generate_migration_engine',
|
|
30
30
|
)
|
|
31
|
+
# Infisical is opt-in: use_infisical defaults to False and nothing in the free engine passes True, so the generator behind this
|
|
32
|
+
# (tilt/generators/infisical/secrets_generator.star) is not on the default path. It is an older contract with fixed secret names and
|
|
33
|
+
# ports. Do not wire `secrets.required` through it; the default provider is platform/docker/secrets/env_secrets.star.
|
|
31
34
|
load('./infisical/infisical_docker.star', 'InfisicalDocker')
|
|
32
35
|
|
|
33
36
|
# Load project name for dynamic golden image naming
|
|
@@ -29,6 +29,9 @@ load('./prisma/prisma_runtime.star',
|
|
|
29
29
|
|
|
30
30
|
load('./bun_helpers.star', 'bun_hoisted_packages_symlink_fix')
|
|
31
31
|
load('../config/healthcheck.star', 'DOCKER_HEALTHCHECK', 'dockerfile_healthcheck_flags')
|
|
32
|
+
# Infisical is opt-in: use_infisical defaults to False and nothing in the free engine passes True, so the generator behind this
|
|
33
|
+
# (tilt/generators/infisical/secrets_generator.star) is not on the default path. It is an older contract with fixed secret names and
|
|
34
|
+
# ports. Do not wire `secrets.required` through it; the default provider is platform/docker/secrets/env_secrets.star.
|
|
32
35
|
load('./infisical/infisical_docker.star', 'InfisicalDocker')
|
|
33
36
|
|
|
34
37
|
|
|
@@ -33,21 +33,6 @@ def generate_api_path(stack, app_name):
|
|
|
33
33
|
# Examples: /api/user-management, /api/order-management
|
|
34
34
|
return "/api/" + stack + "-management"
|
|
35
35
|
|
|
36
|
-
# =============================================================================
|
|
37
|
-
# SERVICE STACK TO API PATH MAPPING (Dynamic)
|
|
38
|
-
# =============================================================================
|
|
39
|
-
# This is populated at runtime from discovered manifests
|
|
40
|
-
# No hardcoded resource names
|
|
41
|
-
|
|
42
|
-
RESOURCE_STACK_TO_API_PATH = {}
|
|
43
|
-
|
|
44
|
-
# =============================================================================
|
|
45
|
-
# API PATH TO SERVICE MAPPING (Dynamic)
|
|
46
|
-
# =============================================================================
|
|
47
|
-
|
|
48
|
-
API_PATH_TO_RESOURCE_STACK = {}
|
|
49
|
-
API_PATH_TO_RESOURCE_DOMAIN = API_PATH_TO_RESOURCE_STACK
|
|
50
|
-
|
|
51
36
|
# =============================================================================
|
|
52
37
|
# HELPER FUNCTIONS
|
|
53
38
|
# =============================================================================
|
|
@@ -110,7 +95,7 @@ def get_api_path_for_stack(stack, manifest=None):
|
|
|
110
95
|
return manifest.get("apiPath")
|
|
111
96
|
|
|
112
97
|
# NEW: Use /api/{stack}-management pattern instead of /api/v1/{pluralized}
|
|
113
|
-
return
|
|
98
|
+
return "/api/" + stack + "-management"
|
|
114
99
|
|
|
115
100
|
# Backwards compatibility alias
|
|
116
101
|
get_api_path_for_domain = get_api_path_for_stack
|
|
@@ -176,20 +161,6 @@ def get_api_path_for_resource(resource_name):
|
|
|
176
161
|
# Backwards compatibility alias
|
|
177
162
|
get_api_path_for_service = get_api_path_for_resource
|
|
178
163
|
|
|
179
|
-
def get_stack_for_api_path(api_path):
|
|
180
|
-
"""Returns the service stack for an API path (reverse lookup).
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
api_path: Full API path
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
Service stack string
|
|
187
|
-
"""
|
|
188
|
-
return API_PATH_TO_RESOURCE_STACK.get(api_path, "")
|
|
189
|
-
|
|
190
|
-
# Backwards compatibility alias
|
|
191
|
-
get_domain_for_api_path = get_stack_for_api_path
|
|
192
|
-
|
|
193
164
|
# Load project name for dynamic localhost domain
|
|
194
165
|
# Use TDK_PROJECT_ROOT env var set by Tilt, fallback to current directory
|
|
195
166
|
def _load_project_localhost():
|
|
@@ -231,30 +202,3 @@ def build_health_endpoint(api_path, health_path="/health"):
|
|
|
231
202
|
Full health endpoint path
|
|
232
203
|
"""
|
|
233
204
|
return api_path + health_path
|
|
234
|
-
|
|
235
|
-
def get_all_api_paths():
|
|
236
|
-
"""Returns a list of all defined API paths from discovered services.
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
List of API path strings
|
|
240
|
-
"""
|
|
241
|
-
return RESOURCE_DOMAIN_TO_API_PATH.values()
|
|
242
|
-
|
|
243
|
-
def is_valid_api_path(api_path):
|
|
244
|
-
"""Checks if an API path is valid/defined.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
api_path: API path to check
|
|
248
|
-
|
|
249
|
-
Returns:
|
|
250
|
-
True if valid, False otherwise
|
|
251
|
-
"""
|
|
252
|
-
return api_path in API_PATH_TO_RESOURCE_DOMAIN
|
|
253
|
-
|
|
254
|
-
# =============================================================================
|
|
255
|
-
# LEGACY COMPATIBILITY (Deprecated - for migration only)
|
|
256
|
-
# =============================================================================
|
|
257
|
-
# All legacy paths removed - use dynamic path generation from manifests
|
|
258
|
-
|
|
259
|
-
LEGACY_DOMAIN_TO_API_PATH = {}
|
|
260
|
-
RESOURCE_DOMAIN_TO_API_PATH = LEGACY_DOMAIN_TO_API_PATH
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# 🔑 SECRETS FROM THE PROJECT .env
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Purpose: Make `secrets.required` / `secrets.optional` in service.json real for local development.
|
|
5
|
+
#
|
|
6
|
+
# The default provider is `env-file`: a secret's value lives in the project `.env` (gitignored) and reaches the container through
|
|
7
|
+
# Docker Compose's own `${NAME}` interpolation, so nothing secret is ever written to a generated, committed file. A required secret
|
|
8
|
+
# uses `${NAME:?message}`, so Compose refuses to start the service when the name is missing; an optional one defaults to empty.
|
|
9
|
+
#
|
|
10
|
+
# `TDK_SECRET_PROVIDER=infisical` leaves the names to Infisical instead, and nothing is injected here.
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
DEFAULT_SECRET_PROVIDER = 'env-file'
|
|
14
|
+
|
|
15
|
+
_NAME_FIRST = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'
|
|
16
|
+
_NAME_REST = _NAME_FIRST + '0123456789'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_secret_provider():
|
|
20
|
+
"""The selected provider: `env-file` (default) or `infisical`."""
|
|
21
|
+
provider = str(os.environ.get('TDK_SECRET_PROVIDER', '')).strip().lower()
|
|
22
|
+
return provider if provider else DEFAULT_SECRET_PROVIDER
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_valid_env_name(name):
|
|
26
|
+
if type(name) != 'string' or not name:
|
|
27
|
+
return False
|
|
28
|
+
if name[0] not in _NAME_FIRST:
|
|
29
|
+
return False
|
|
30
|
+
for i in range(len(name)):
|
|
31
|
+
if name[i] not in _NAME_REST:
|
|
32
|
+
return False
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _names(manifest, resource_name, kind):
|
|
37
|
+
secrets = manifest.get('secrets') or {}
|
|
38
|
+
names = secrets.get(kind) or []
|
|
39
|
+
if type(names) != 'list':
|
|
40
|
+
fail("{}: secrets.{} must be a list of environment variable names".format(resource_name, kind))
|
|
41
|
+
for name in names:
|
|
42
|
+
if not _is_valid_env_name(name):
|
|
43
|
+
fail("{}: invalid secret name '{}' in secrets.{}: use letters, digits and underscores, not starting with a digit".format(resource_name, name, kind))
|
|
44
|
+
return names
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_secret_environment_lines(manifest, resource_name):
|
|
48
|
+
"""Compose `environment:` list lines (each starting with a newline) for a resource's secrets, or '' when there are none.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
manifest: The service manifest (reads secrets.required and secrets.optional)
|
|
52
|
+
resource_name: Used in error messages and in the text Compose shows for a missing required secret
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: Lines to append to the Compose environment block
|
|
56
|
+
"""
|
|
57
|
+
if not manifest or not manifest.get('secrets'):
|
|
58
|
+
return ''
|
|
59
|
+
|
|
60
|
+
required = _names(manifest, resource_name, 'required')
|
|
61
|
+
optional = _names(manifest, resource_name, 'optional')
|
|
62
|
+
|
|
63
|
+
if get_secret_provider() != DEFAULT_SECRET_PROVIDER:
|
|
64
|
+
return ''
|
|
65
|
+
|
|
66
|
+
lines = ''
|
|
67
|
+
for name in required:
|
|
68
|
+
lines += '\n - {name}=${{{name}:?{name} is required by {resource}. Add it to the project .env}}'.format(name=name, resource=resource_name)
|
|
69
|
+
for name in optional:
|
|
70
|
+
if name not in required:
|
|
71
|
+
lines += '\n - {name}=${{{name}:-}}'.format(name=name)
|
|
72
|
+
return lines
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
load('./utils_env.star',
|
|
9
9
|
'load_dotenv',
|
|
10
|
-
'validate_infisical_environment',
|
|
11
10
|
'should_enable',
|
|
12
11
|
)
|
|
13
12
|
load('./utils_debug.star',
|
|
@@ -371,7 +370,6 @@ Utils = struct(
|
|
|
371
370
|
|
|
372
371
|
# Environment
|
|
373
372
|
load_dotenv = load_dotenv,
|
|
374
|
-
validate_infisical_environment = validate_infisical_environment,
|
|
375
373
|
should_enable = should_enable,
|
|
376
374
|
|
|
377
375
|
# Debug utilities
|