@tdk-landscape/tdk-cli-core 1.3.124 → 1.3.126
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 +289 -18
- package/README.md +20 -1
- package/cli/dist/commands/doctor.js +3 -127
- package/cli/dist/commands/down.js +8 -1
- package/cli/dist/commands/eject.js +2 -2
- package/cli/dist/commands/logs.js +16 -10
- package/cli/dist/commands/networks.js +10 -10
- package/cli/dist/commands/resource.js +7 -126
- package/cli/dist/commands/ui.js +35 -14
- package/cli/dist/commands/upgrade.js +39 -52
- package/cli/dist/components/TabBar.js +2 -7
- package/cli/dist/generator/resource-templates.js +127 -0
- package/cli/dist/generator/template-engine.js +7 -1
- package/cli/dist/utils/doctor-runtime.js +6 -3
- package/cli/dist/utils/doctor-starlark.js +131 -0
- package/cli/dist/utils/env-validator.js +61 -19
- package/cli/dist/utils/service-manifest.js +145 -0
- package/cli/dist/utils/service-runtime-state.js +2 -2
- package/cli/dist/utils/services.js +8 -1
- package/cli/dist/utils/startup-report.js +10 -6
- package/cli/dist/utils/tilt-resource-state.js +19 -0
- package/cli/dist/utils/tilt.js +2 -1
- package/cli/dist/utils/ui-keymap.js +118 -0
- package/cli/package.json +1 -1
- package/discovery/manifest/loading.star +5 -0
- package/discovery/manifest/normalize.star +6 -0
- package/engine/topologies/tilt/common/utils.star +3 -3
- package/engine/topologies/tilt/common/utils_debug.star +1 -1
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +4 -6
- package/engine/topologies/tilt/manifest/loader.star +7 -0
- package/engine/topologies/tilt/manifest/parser.star +86 -3
- package/engine/topologies/tilt/manifest/tests/test_loader_parser.star +63 -1
- package/engine/topologies/tilt/resources/infra-loader.star +12 -11
- package/engine/topologies/tilt/resources/orchestrator/apply.star +2 -1
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +19 -18
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const TABS = [
|
|
2
|
+
{ id: "overview", label: "OVERVIEW", shortcut: "1" },
|
|
3
|
+
{ id: "resources", label: "RESOURCES", shortcut: "2" },
|
|
4
|
+
{ id: "events", label: "EVENTS", shortcut: "3" },
|
|
5
|
+
{ id: "files", label: "FILES", shortcut: "4" },
|
|
6
|
+
{ id: "config", label: "CONFIG", shortcut: "5" },
|
|
7
|
+
];
|
|
8
|
+
const tabRange = `${TABS[0].shortcut}-${TABS[TABS.length - 1].shortcut}`;
|
|
9
|
+
const uiKeymap = [
|
|
10
|
+
{
|
|
11
|
+
id: "list-navigation",
|
|
12
|
+
keys: ["ArrowUp", "ArrowDown", "j", "k"],
|
|
13
|
+
label: "Navigate list items",
|
|
14
|
+
group: "Navigation",
|
|
15
|
+
display: { ascii: "UP/DOWN or k/j", unicode: "↑/↓ or k/j" },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "first-last",
|
|
19
|
+
keys: ["g", "G", "Home", "End"],
|
|
20
|
+
label: "First/last item",
|
|
21
|
+
group: "Navigation",
|
|
22
|
+
display: { ascii: "g/G or Home/End", unicode: "g/G or Home/End" },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "page-list",
|
|
26
|
+
keys: ["PageUp", "PageDown"],
|
|
27
|
+
label: "Move one page",
|
|
28
|
+
group: "Navigation",
|
|
29
|
+
display: { ascii: "PgUp/PgDn", unicode: "PgUp/PgDn" },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "select",
|
|
33
|
+
keys: ["Enter", "Space"],
|
|
34
|
+
label: "Select item / Open detail",
|
|
35
|
+
group: "Navigation",
|
|
36
|
+
display: { ascii: "Enter or Space", unicode: "Enter or Space" },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "cycle-tabs",
|
|
40
|
+
keys: ["Tab", "Shift+Tab"],
|
|
41
|
+
label: "Next / previous tab",
|
|
42
|
+
group: "Navigation",
|
|
43
|
+
display: { ascii: "Tab/Shift+Tab", unicode: "Tab/Shift+Tab" },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "direct-tabs",
|
|
47
|
+
keys: TABS.map((tab) => tab.shortcut),
|
|
48
|
+
label: "Direct tab access",
|
|
49
|
+
group: "Navigation",
|
|
50
|
+
display: { ascii: tabRange, unicode: tabRange },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "toggle-mouse",
|
|
54
|
+
keys: ["m"],
|
|
55
|
+
label: "Toggle mouse support",
|
|
56
|
+
group: "Actions",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "toggle-tooltips",
|
|
60
|
+
keys: ["t"],
|
|
61
|
+
label: "Toggle tooltips",
|
|
62
|
+
group: "Actions",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "toggle-enabled",
|
|
66
|
+
keys: ["e"],
|
|
67
|
+
label: "Toggle enabled/disabled services",
|
|
68
|
+
group: "Actions",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "refresh",
|
|
72
|
+
keys: ["r"],
|
|
73
|
+
label: "Refresh data",
|
|
74
|
+
group: "Actions",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "search",
|
|
78
|
+
keys: ["/"],
|
|
79
|
+
label: "Search/filter",
|
|
80
|
+
group: "Actions",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "help",
|
|
84
|
+
keys: ["?"],
|
|
85
|
+
label: "Show this help",
|
|
86
|
+
group: "Actions",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "quit",
|
|
90
|
+
keys: ["q"],
|
|
91
|
+
label: "Quit",
|
|
92
|
+
group: "Actions",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "back",
|
|
96
|
+
keys: ["Esc"],
|
|
97
|
+
label: "Back",
|
|
98
|
+
group: "Actions",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
export const UI_KEYMAP = uiKeymap;
|
|
102
|
+
function getUiKeymapEntry(id) {
|
|
103
|
+
const entry = UI_KEYMAP.find((shortcut) => shortcut.id === id);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
throw new Error(`Unknown TDK UI shortcut: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
export function formatUiKeyNames(id, ascii = false) {
|
|
110
|
+
const entry = getUiKeymapEntry(id);
|
|
111
|
+
const display = ascii ? entry.display?.ascii : entry.display?.unicode;
|
|
112
|
+
return display ?? entry.keys.join("/");
|
|
113
|
+
}
|
|
114
|
+
export function formatUiKeyHint(id, label, ascii = false) {
|
|
115
|
+
const entry = getUiKeymapEntry(id);
|
|
116
|
+
return `[${formatUiKeyNames(id, ascii)}] ${label ?? entry.label}`;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=ui-keymap.js.map
|
package/cli/package.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# Priority: service.json → synthesize from path
|
|
9
9
|
# =============================================================================
|
|
10
10
|
|
|
11
|
+
load('../../engine/topologies/tilt/manifest/parser.star', 'generated_string_error')
|
|
11
12
|
load('../../engine/topologies/tilt/manifest/constants.star',
|
|
12
13
|
'MANIFEST_DEFAULTS',
|
|
13
14
|
'DEFAULT_SYNCS',
|
|
@@ -100,6 +101,10 @@ def load_manifest(resource_path, persist_to_disk=False):
|
|
|
100
101
|
|
|
101
102
|
if content and str(content).strip():
|
|
102
103
|
manifest = decode_json(content)
|
|
104
|
+
# Refuse service.json values that would add keys to generated config (GHSA-phgf-pww4-7jxc)
|
|
105
|
+
unsafe_field_error = generated_string_error(manifest, manifest_full_path) if type(manifest) == "dict" else None
|
|
106
|
+
if unsafe_field_error:
|
|
107
|
+
fail(unsafe_field_error)
|
|
103
108
|
manifest_source = 'service.json'
|
|
104
109
|
manifest_path = manifest_full_path
|
|
105
110
|
else:
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# Purpose: Normalize manifests and provide helper accessors
|
|
6
6
|
# =============================================================================
|
|
7
7
|
|
|
8
|
+
load('../../engine/topologies/tilt/manifest/parser.star', 'generated_string_error')
|
|
8
9
|
load('./constants.star', 'MANIFEST_DEFAULTS', 'MANIFEST_FILENAME')
|
|
9
10
|
load('./loading.star', 'apply_manifest_defaults', 'check_prisma_folder', 'get_default_syncs_for_type')
|
|
10
11
|
load('./validation.star', 'validate_manifest')
|
|
@@ -56,6 +57,11 @@ def _load_and_normalize(manifest_path, warn_only=True):
|
|
|
56
57
|
print(" ⚠️ Invalid JSON in manifest: " + manifest_path)
|
|
57
58
|
return None
|
|
58
59
|
fail("Invalid JSON in manifest: " + manifest_path)
|
|
60
|
+
|
|
61
|
+
# Refuse service.json values that would add keys to generated config (GHSA-phgf-pww4-7jxc)
|
|
62
|
+
unsafe_field_error = generated_string_error(manifest, manifest_path) if type(manifest) == "dict" else None
|
|
63
|
+
if unsafe_field_error:
|
|
64
|
+
fail(unsafe_field_error)
|
|
59
65
|
|
|
60
66
|
# Validate
|
|
61
67
|
issues = validate_manifest(manifest)
|
|
@@ -318,11 +318,11 @@ def write_file_if_changed(file_path, content):
|
|
|
318
318
|
existing = str(existing_blob).rstrip()
|
|
319
319
|
|
|
320
320
|
if existing == expected:
|
|
321
|
-
|
|
321
|
+
debug_log("WRITE: Skipping {} (no changes)".format(file_path))
|
|
322
322
|
else:
|
|
323
|
-
|
|
323
|
+
debug_log("WRITE: Writing {} bytes to {}".format(len(expected), file_path))
|
|
324
324
|
_write_generated_file(file_path, expected)
|
|
325
|
-
|
|
325
|
+
debug_log("WRITE: Completed {}".format(file_path))
|
|
326
326
|
|
|
327
327
|
# Keep a generated-content snapshot beside the service outputs. `tdk config verify`
|
|
328
328
|
# compares these snapshots with the files and the source manifest, detecting both
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# 🛠️ TILT SDK - DEBUG UTILITIES
|
|
3
3
|
# =============================================================================
|
|
4
4
|
|
|
5
|
-
DEBUG_MODE = os.environ.get('TILT_DEBUG', 'false').lower() in ['true', '1', 'yes']
|
|
5
|
+
DEBUG_MODE = (os.environ.get('TDK_DEBUG') or os.environ.get('TILT_DEBUG', 'false')).lower() in ['true', '1', 'yes']
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def debug_log(message, data=None):
|
|
@@ -3,6 +3,7 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
|
|
|
3
3
|
# === END INLINED CONSTANTS ===
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
load("../../common/utils_debug.star", "debug_log")
|
|
6
7
|
load("../../../../topologies/tilt/common/utils.star", "Utils")
|
|
7
8
|
load("../../../../topologies/tilt/manifest/constants.star", "GENERATED_CONFIG_FILENAMES")
|
|
8
9
|
|
|
@@ -126,15 +127,12 @@ def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_d
|
|
|
126
127
|
}
|
|
127
128
|
if is_docker:
|
|
128
129
|
output_path = resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER + '/' + GENERATED_CONFIG_FILENAMES['tsconfig_docker']
|
|
129
|
-
|
|
130
|
-
print("📝 DEBUG: Writing Docker tsconfig to: {}".format(output_path))
|
|
130
|
+
debug_log("📝 Writing Docker tsconfig to: {}".format(output_path))
|
|
131
131
|
write_fn(output_path, Utils.encode_json(content))
|
|
132
|
-
|
|
133
|
-
print("📝 DEBUG: Docker tsconfig write complete")
|
|
132
|
+
debug_log("📝 Docker tsconfig write complete")
|
|
134
133
|
else:
|
|
135
134
|
output_path = resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER + '/' + GENERATED_CONFIG_FILENAMES['tsconfig_local']
|
|
136
|
-
|
|
137
|
-
print("📝 DEBUG: Writing Local tsconfig to: {}".format(output_path))
|
|
135
|
+
debug_log("📝 Writing Local tsconfig to: {}".format(output_path))
|
|
138
136
|
write_fn(output_path, Utils.encode_json(content))
|
|
139
137
|
|
|
140
138
|
DEBUG = {}
|
|
@@ -19,6 +19,7 @@ DEFAULTS = {}
|
|
|
19
19
|
# === END INLINED CONSTANTS ===
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
load("./parser.star", "generated_string_error")
|
|
22
23
|
load("./constants.star",
|
|
23
24
|
"MANIFEST_FILENAME",
|
|
24
25
|
"MANIFEST_DEFAULTS",
|
|
@@ -63,6 +64,12 @@ def load_from_file(path):
|
|
|
63
64
|
|
|
64
65
|
if manifest == None:
|
|
65
66
|
return struct(manifest=None, error="Invalid manifest: " + path)
|
|
67
|
+
|
|
68
|
+
# Refuse service.json values that would add keys to generated config (GHSA-phgf-pww4-7jxc). Every Tilt discovery path loads service.json here.
|
|
69
|
+
if type(manifest) == "dict":
|
|
70
|
+
unsafe_field_error = generated_string_error(manifest, path)
|
|
71
|
+
if unsafe_field_error:
|
|
72
|
+
fail(unsafe_field_error)
|
|
66
73
|
|
|
67
74
|
# An `mcp` resource is a Model Context Protocol server: an HTTP service on Bun that is built, routed and health-checked
|
|
68
75
|
# exactly like a backend. Every backend code path keys off appType == 'backend', so normalise here, once, at the single
|
|
@@ -73,24 +73,106 @@ def parse(content, path=""):
|
|
|
73
73
|
warnings=warnings,
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
+
# Characters allowed in the string fields that are written into generated Compose, Traefik and NATS config as they are. A line break
|
|
77
|
+
# or YAML syntax in one of them adds keys to the generated service (GHSA-phgf-pww4-7jxc). Keep in step with GENERATED_STRING_FIELDS
|
|
78
|
+
# in cli/src/utils/service-manifest.ts.
|
|
79
|
+
_ALPHANUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
80
|
+
_PATH_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789/-"
|
|
81
|
+
_GENERATED_STRING_RULES = [
|
|
82
|
+
# (field path, allowed characters, required first character or "")
|
|
83
|
+
(['healthCheckPath'], _PATH_CHARS, '/'),
|
|
84
|
+
(['traefik', 'host'], _ALPHANUM + '.-', ''),
|
|
85
|
+
(['traefik', 'pathPrefix'], _PATH_CHARS, '/'),
|
|
86
|
+
(['traefik', 'healthCheck'], _PATH_CHARS, '/'),
|
|
87
|
+
(['nats', 'queueGroup'], _ALPHANUM + '_.-', ''),
|
|
88
|
+
(['databaseName'], _ALPHANUM + '_-', ''),
|
|
89
|
+
(['stack'], _ALPHANUM + '_-', ''),
|
|
90
|
+
(['image'], _ALPHANUM + '._/:@-', ''),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def generated_string_error(manifest, resource_path=""):
|
|
94
|
+
"""
|
|
95
|
+
Return an error message if a value that is written into generated config has unsafe characters, else None.
|
|
96
|
+
|
|
97
|
+
The value is not echoed, because it may contain the line break that makes it dangerous.
|
|
98
|
+
"""
|
|
99
|
+
for keys, allowed, first in _GENERATED_STRING_RULES:
|
|
100
|
+
value = manifest
|
|
101
|
+
for key in keys:
|
|
102
|
+
value = value.get(key) if type(value) == "dict" else None
|
|
103
|
+
if value == None or value == "":
|
|
104
|
+
continue
|
|
105
|
+
field = '.'.join(keys)
|
|
106
|
+
where = " in " + resource_path if resource_path else ""
|
|
107
|
+
if type(value) != "string":
|
|
108
|
+
return "MANIFEST ERROR: '{}' must be a string{}".format(field, where)
|
|
109
|
+
if first and not value.startswith(first):
|
|
110
|
+
return "MANIFEST ERROR: '{}' must start with '{}'{}".format(field, first, where)
|
|
111
|
+
for c in value.elems():
|
|
112
|
+
if c not in allowed:
|
|
113
|
+
return "MANIFEST ERROR: '{}' has characters that are not allowed (no spaces, line breaks or YAML syntax){}".format(field, where)
|
|
114
|
+
return _line_break_error(manifest, resource_path)
|
|
115
|
+
|
|
116
|
+
# Line breaks (including the Unicode ones YAML 1.1 honours) and other control characters.
|
|
117
|
+
_LINE_BREAK_OR_CONTROL = [chr(i) for i in range(32)] + [chr(127), "\u0085", "
", "
"]
|
|
118
|
+
|
|
119
|
+
def _has_line_break(text):
|
|
120
|
+
for bad in _LINE_BREAK_OR_CONTROL:
|
|
121
|
+
if bad in text:
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def _line_break_error(manifest, resource_path):
|
|
126
|
+
"""
|
|
127
|
+
Many more values than _GENERATED_STRING_RULES reach generated config (port, appName, dockerfile, buildContext, basePath, envVars,
|
|
128
|
+
...). A key can only be added through a line break, so no string key or value may contain one. `smoke` is skipped: it is never
|
|
129
|
+
written into generated config. Keep in step with lineBreakErrors in cli/src/utils/service-manifest.ts.
|
|
130
|
+
"""
|
|
131
|
+
where = " in " + resource_path if resource_path else ""
|
|
132
|
+
pending = [(value, key) for key, value in manifest.items() if key != 'smoke']
|
|
133
|
+
for key in manifest.keys():
|
|
134
|
+
if type(key) == "string" and _has_line_break(key):
|
|
135
|
+
return "MANIFEST ERROR: a key contains a line break{}".format(where)
|
|
136
|
+
# Starlark has no recursion or while loops; a service.json never nests anywhere near this many values.
|
|
137
|
+
for _ in range(100000):
|
|
138
|
+
if not pending:
|
|
139
|
+
return None
|
|
140
|
+
value, field = pending.pop()
|
|
141
|
+
if type(value) == "string":
|
|
142
|
+
if _has_line_break(value):
|
|
143
|
+
return "MANIFEST ERROR: '{}' must not contain line breaks or control characters{}".format(field, where)
|
|
144
|
+
elif type(value) == "list":
|
|
145
|
+
for index, item in enumerate(value):
|
|
146
|
+
pending.append((item, "{}[{}]".format(field, index)))
|
|
147
|
+
elif type(value) == "dict":
|
|
148
|
+
for key, item in value.items():
|
|
149
|
+
if type(key) == "string" and _has_line_break(key):
|
|
150
|
+
return "MANIFEST ERROR: a key in '{}' contains a line break{}".format(field, where)
|
|
151
|
+
pending.append((item, "{}.{}".format(field, key)))
|
|
152
|
+
return "MANIFEST ERROR: service.json is nested too deeply{}".format(where)
|
|
153
|
+
|
|
76
154
|
def normalize(manifest, resource_path=""):
|
|
77
155
|
"""
|
|
78
156
|
Normalize manifest with default values and computed fields.
|
|
79
|
-
|
|
157
|
+
|
|
80
158
|
Args:
|
|
81
159
|
manifest: Raw parsed manifest dict
|
|
82
160
|
resource_path: Resource directory path for context
|
|
83
|
-
|
|
161
|
+
|
|
84
162
|
Returns:
|
|
85
163
|
Normalized manifest dict with all fields populated
|
|
86
164
|
"""
|
|
87
165
|
normalized = {}
|
|
88
166
|
warnings = []
|
|
89
|
-
|
|
167
|
+
|
|
90
168
|
# Copy all existing fields
|
|
91
169
|
for key, value in manifest.items():
|
|
92
170
|
normalized[key] = value
|
|
93
171
|
|
|
172
|
+
unsafe_field_error = generated_string_error(normalized, resource_path)
|
|
173
|
+
if unsafe_field_error:
|
|
174
|
+
fail(unsafe_field_error)
|
|
175
|
+
|
|
94
176
|
# 1. Apply defaults for missing fields
|
|
95
177
|
for field, default_value in MANIFEST_DEFAULTS.items():
|
|
96
178
|
if field not in normalized or normalized[field] == None:
|
|
@@ -391,6 +473,7 @@ def get_resource_type_description(manifest):
|
|
|
391
473
|
ManifestParser = struct(
|
|
392
474
|
parse=parse,
|
|
393
475
|
normalize=normalize,
|
|
476
|
+
generated_string_error=generated_string_error,
|
|
394
477
|
extract_resource_path=extract_resource_path,
|
|
395
478
|
extract_stack=extract_stack_from_path,
|
|
396
479
|
parse_traefik=parse_traefik_config,
|
|
@@ -306,6 +306,67 @@ def _test_manifest_merging():
|
|
|
306
306
|
|
|
307
307
|
print("✅ Manifest merging tests complete\n")
|
|
308
308
|
|
|
309
|
+
# =============================================================================
|
|
310
|
+
# TEST SUITE: Values written into generated config (GHSA-phgf-pww4-7jxc)
|
|
311
|
+
# =============================================================================
|
|
312
|
+
|
|
313
|
+
def _test_generated_string_fields():
|
|
314
|
+
"""A line break or YAML syntax in a field that reaches generated compose output must be rejected."""
|
|
315
|
+
base = {'appName': 'api', 'stack': 'shop', 'traefik': {'pathPrefix': '/api/v1/shop-management'}}
|
|
316
|
+
|
|
317
|
+
assert_equal(None, ManifestParser.generated_string_error(base), "Clean manifest has no generated-field error")
|
|
318
|
+
|
|
319
|
+
safe = {
|
|
320
|
+
'appName': 'api',
|
|
321
|
+
'stack': 'shop-1',
|
|
322
|
+
'healthCheckPath': '/api/v1/health',
|
|
323
|
+
'databaseName': 'TDK_shop',
|
|
324
|
+
'image': 'ghcr.io/acme/app:1.0',
|
|
325
|
+
'traefik': {'host': 'shop.backend.my-project.local', 'pathPrefix': '/api/v1/shop', 'healthCheck': '/health'},
|
|
326
|
+
'nats': {'queueGroup': 'shop_backend_svc'},
|
|
327
|
+
}
|
|
328
|
+
assert_equal(None, ManifestParser.generated_string_error(safe), "Safe values are accepted")
|
|
329
|
+
|
|
330
|
+
hostile = [
|
|
331
|
+
{'healthCheckPath': '/health\n privileged: true'},
|
|
332
|
+
{'healthCheckPath': '/health"'},
|
|
333
|
+
{'traefik': {'host': 'a.local\n privileged: true', 'pathPrefix': '/api'}},
|
|
334
|
+
{'traefik': {'pathPrefix': '/api/v1/x\n network_mode: host'}},
|
|
335
|
+
{'traefik': {'pathPrefix': '/api', 'healthCheck': '/h\n volumes: []'}},
|
|
336
|
+
{'traefik': {'pathPrefix': '/api'}, 'nats': {'queueGroup': 'g: true'}},
|
|
337
|
+
{'traefik': {'pathPrefix': '/api'}, 'databaseName': 'TDK_x\n privileged: true'},
|
|
338
|
+
{'traefik': {'pathPrefix': '/api'}, 'stack': 'shop # comment'},
|
|
339
|
+
{'traefik': {'pathPrefix': '/api'}, 'image': 'app\n privileged: true'},
|
|
340
|
+
{'traefik': {'pathPrefix': '/api'}, 'healthCheckPath': 'health'},
|
|
341
|
+
{'traefik': {'pathPrefix': '/api'}, 'healthCheckPath': 42},
|
|
342
|
+
]
|
|
343
|
+
for manifest in hostile:
|
|
344
|
+
assert_not_none(ManifestParser.generated_string_error(manifest), "Rejects unsafe value: " + str(manifest))
|
|
345
|
+
|
|
346
|
+
# The message names the field and never echoes the value
|
|
347
|
+
message = ManifestParser.generated_string_error(hostile[0], 'services/shop/api')
|
|
348
|
+
assert_true("healthCheckPath" in message, "Error names the field")
|
|
349
|
+
assert_true("\n" not in message, "Error does not echo the value")
|
|
350
|
+
|
|
351
|
+
# Fields outside the allowlist also reach generated config, so a line break anywhere is refused
|
|
352
|
+
clean = {'appName': 'api', 'stack': 'shop', 'traefik': {'pathPrefix': '/api'}}
|
|
353
|
+
line_breaks = [
|
|
354
|
+
{'port': '4000\n privileged: true'},
|
|
355
|
+
{'appName': 'api\n privileged: true'},
|
|
356
|
+
{'dockerfile': 'Dockerfile\r privileged: true'},
|
|
357
|
+
{'buildContext': 'src\u2028privileged: true'},
|
|
358
|
+
{'envVars': [{'name': 'A', 'value': 'v\n privileged: true'}]},
|
|
359
|
+
{'params': {'a\n privileged': 'x'}},
|
|
360
|
+
]
|
|
361
|
+
for extra in line_breaks:
|
|
362
|
+
manifest = dict(clean)
|
|
363
|
+
manifest.update(extra)
|
|
364
|
+
assert_not_none(ManifestParser.generated_string_error(manifest), "Rejects a line break: " + repr(extra))
|
|
365
|
+
|
|
366
|
+
smoke = dict(clean)
|
|
367
|
+
smoke['smoke'] = {'via': '/api', 'create': {'body': '{\n}'}}
|
|
368
|
+
assert_equal(None, ManifestParser.generated_string_error(smoke), "smoke is never written into generated config")
|
|
369
|
+
|
|
309
370
|
# =============================================================================
|
|
310
371
|
# TEST SUITE: Get Normalized Convenience Function
|
|
311
372
|
# =============================================================================
|
|
@@ -366,12 +427,13 @@ def run_tests():
|
|
|
366
427
|
_test_loader_functions()
|
|
367
428
|
_test_parser_functions()
|
|
368
429
|
_test_parse_and_normalize()
|
|
369
|
-
|
|
430
|
+
_test_stack_extraction()
|
|
370
431
|
_test_frontend_normalization()
|
|
371
432
|
_test_traefik_config()
|
|
372
433
|
_test_manifest_merging()
|
|
373
434
|
_test_get_normalized()
|
|
374
435
|
_test_resource_description()
|
|
436
|
+
_test_generated_string_fields()
|
|
375
437
|
|
|
376
438
|
# Print results
|
|
377
439
|
print("\n" + "="*70)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# Infra.load_all(should_enable_fn)
|
|
19
19
|
# =============================================================================
|
|
20
20
|
|
|
21
|
+
load("../common/utils_debug.star", "debug_log")
|
|
21
22
|
load("../../platform/docker/constants.star", "PlatformDockerConstants")
|
|
22
23
|
load("../../platform/docker/compose/traefik_standalone.star", "generate_standalone_traefik_compose")
|
|
23
24
|
load("../../platform/docker/networking/sablier_container_cycle.star", "sablier_middleware_suffix")
|
|
@@ -130,14 +131,14 @@ def _register_platform_postgres(should_enable, root_prefix="", env_file=None, wr
|
|
|
130
131
|
"""
|
|
131
132
|
postgres_compose = _ensure_database_management_compose(root_prefix, write_fn)
|
|
132
133
|
if _file_exists(postgres_compose):
|
|
133
|
-
|
|
134
|
+
debug_log("INFRA: Loading postgres compose from {}".format(postgres_compose))
|
|
134
135
|
_docker_compose(postgres_compose, env_file)
|
|
135
136
|
dc_resource('postgres', labels=['infra.tools'], resource_deps=['init-networks'], auto_init=True)
|
|
136
137
|
return
|
|
137
138
|
message = "Shared platform Postgres compose was not materialized at {}".format(postgres_compose)
|
|
138
139
|
if required:
|
|
139
140
|
fail(message + " (write_fn present: {}). Refusing to register a postgres Tilt resource with no compose.".format(write_fn != None))
|
|
140
|
-
|
|
141
|
+
debug_log("INFRA: Skipping postgres (compose file not found): {}".format(message))
|
|
141
142
|
|
|
142
143
|
|
|
143
144
|
def force_start_platform_postgres(should_enable, root_prefix="", env_file=None, write_fn=None):
|
|
@@ -161,7 +162,7 @@ def force_start_platform_postgres(should_enable, root_prefix="", env_file=None,
|
|
|
161
162
|
def _load_database_management(should_enable, root_prefix="", env_file=None, write_fn=None):
|
|
162
163
|
"""Load database and messaging infrastructure."""
|
|
163
164
|
if not should_enable('database-management'):
|
|
164
|
-
|
|
165
|
+
debug_log("INFRA: database-management not enabled")
|
|
165
166
|
return
|
|
166
167
|
|
|
167
168
|
print("🗃️ Loading database management services...")
|
|
@@ -172,7 +173,7 @@ def _load_database_management(should_enable, root_prefix="", env_file=None, writ
|
|
|
172
173
|
# Load messaging if compose file exists
|
|
173
174
|
messaging_compose = root_prefix + 'services/platform/messaging/docker-compose.yml'
|
|
174
175
|
if _file_exists(messaging_compose):
|
|
175
|
-
|
|
176
|
+
debug_log("INFRA: Loading messaging compose from {}".format(messaging_compose))
|
|
176
177
|
_docker_compose(messaging_compose, env_file)
|
|
177
178
|
# Register only services present in the project compose file. Minimal
|
|
178
179
|
# NATS-only examples should not need to add an unrelated Redis broker.
|
|
@@ -181,7 +182,7 @@ def _load_database_management(should_enable, root_prefix="", env_file=None, writ
|
|
|
181
182
|
if _compose_has_service(messaging_compose, 'nats'):
|
|
182
183
|
dc_resource('nats', labels=['infra.messaging'], auto_init=True)
|
|
183
184
|
else:
|
|
184
|
-
|
|
185
|
+
debug_log("INFRA: Skipping messaging (compose file not found)")
|
|
185
186
|
|
|
186
187
|
# Only create kafka resources if debezium is enabled AND messaging exists
|
|
187
188
|
cdc_enabled = should_enable('debezium')
|
|
@@ -211,7 +212,7 @@ def _load_infisical(should_enable, root_prefix="", env_file=None):
|
|
|
211
212
|
|
|
212
213
|
compose_file = root_prefix + 'docker-compose.infisical.yml'
|
|
213
214
|
if not _file_exists(compose_file):
|
|
214
|
-
|
|
215
|
+
debug_log("INFRA: Skipping Infisical (compose file not found)")
|
|
215
216
|
return
|
|
216
217
|
|
|
217
218
|
print("🔐 Loading Infisical...")
|
|
@@ -254,7 +255,7 @@ def _load_standalone_traefik(root_prefix, env_file, write_fn):
|
|
|
254
255
|
def _load_proxy(should_enable, root_prefix="", env_file=None, write_fn=None):
|
|
255
256
|
"""Load Traefik reverse proxy with proper health check sequencing."""
|
|
256
257
|
if not should_enable('proxy'):
|
|
257
|
-
|
|
258
|
+
debug_log("INFRA: proxy not enabled")
|
|
258
259
|
return
|
|
259
260
|
|
|
260
261
|
compose_files = [
|
|
@@ -266,7 +267,7 @@ def _load_proxy(should_enable, root_prefix="", env_file=None, write_fn=None):
|
|
|
266
267
|
]
|
|
267
268
|
missing = [path for path in compose_files if not _file_exists(path)]
|
|
268
269
|
if missing:
|
|
269
|
-
|
|
270
|
+
debug_log("INFRA: Platform proxy compose missing ({}), using standalone Traefik".format(missing[0]))
|
|
270
271
|
_load_standalone_traefik(root_prefix, env_file, write_fn)
|
|
271
272
|
return
|
|
272
273
|
|
|
@@ -422,11 +423,11 @@ def load_all_infrastructure(should_enable, fix_docker_networks_fn=None, docker_p
|
|
|
422
423
|
"""
|
|
423
424
|
# Initialize networks first
|
|
424
425
|
if fix_docker_networks_fn:
|
|
425
|
-
|
|
426
|
+
debug_log("INFRA: Initializing networks...")
|
|
426
427
|
_init_networks(fix_docker_networks_fn)
|
|
427
|
-
|
|
428
|
+
debug_log("INFRA: Networks initialized")
|
|
428
429
|
else:
|
|
429
|
-
|
|
430
|
+
debug_log("INFRA: SKIPPING network initialization (no fix_docker_networks_fn)")
|
|
430
431
|
|
|
431
432
|
# These stacks both define an `elasticsearch` resource (and host port 9200).
|
|
432
433
|
# Running both at once causes compose/resource collisions and unstable startup.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Purpose: thin coordinator for service apply lifecycle.
|
|
5
5
|
# =============================================================================
|
|
6
6
|
|
|
7
|
+
load("../../common/utils_debug.star", "debug_log")
|
|
7
8
|
load('../databases.star', 'Database')
|
|
8
9
|
load('./apply_runtime_flags.star', 'RuntimeFlags')
|
|
9
10
|
load('./apply_resource_validation.star', 'ResourceValidation')
|
|
@@ -38,7 +39,7 @@ def apply_app_service(resource_config, ctx):
|
|
|
38
39
|
for nested in resource_config.get('resources', []):
|
|
39
40
|
nested_name = nested.get('name', '')
|
|
40
41
|
if nested_name and should_enable(nested_name):
|
|
41
|
-
|
|
42
|
+
debug_log("APPLY: stack '{}' not individually enabled, but nested service '{}' is - proceeding".format(resource_name, nested_name))
|
|
42
43
|
service_enabled = True
|
|
43
44
|
break
|
|
44
45
|
|