infer-stack 0.6.0__py3-none-any.whl
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.
- infer_stack/__init__.py +2 -0
- infer_stack/backends/__init__.py +7 -0
- infer_stack/backends/compose_renderer.py +243 -0
- infer_stack/backends/kubeai_renderer.py +202 -0
- infer_stack/benchmark.py +38 -0
- infer_stack/catalog.py +438 -0
- infer_stack/cli/__init__.py +169 -0
- infer_stack/cli/__main__.py +4 -0
- infer_stack/cli/commands_profile.py +467 -0
- infer_stack/cli/commands_runtime.py +719 -0
- infer_stack/cli/commands_smoke.py +691 -0
- infer_stack/cli/compose.py +755 -0
- infer_stack/cli/context.py +471 -0
- infer_stack/cli/options.py +134 -0
- infer_stack/cli/probes.py +178 -0
- infer_stack/config.py +450 -0
- infer_stack/contracts.py +223 -0
- infer_stack/diff_prompt.py +117 -0
- infer_stack/docker_utils.py +230 -0
- infer_stack/env_utils.py +97 -0
- infer_stack/experimental/model_catalog_discover.py +1155 -0
- infer_stack/experimental/model_memory_estimator.py +1264 -0
- infer_stack/experimental/stress_test_long_context.py +397 -0
- infer_stack/hardware.py +70 -0
- infer_stack/kubeai_ops.py +76 -0
- infer_stack/paths.py +87 -0
- infer_stack/profile_runtime.py +46 -0
- infer_stack/renderer.py +19 -0
- infer_stack/resolver.py +1092 -0
- infer_stack/templates/default-models.yaml +674 -0
- infer_stack/templates/default-ollama-models.yaml +31 -0
- infer_stack/templates/default-profiles.yaml +1731 -0
- infer_stack/templates/default-vllm-models.yaml +714 -0
- infer_stack/templates/docker-compose.yml.j2 +430 -0
- infer_stack/templates/litellm_config.yaml.j2 +44 -0
- infer_stack/templates/nginx.conf.j2 +84 -0
- infer_stack/tuning.py +3 -0
- infer_stack/validator.py +314 -0
- infer_stack/verification.py +46 -0
- infer_stack-0.6.0.dist-info/METADATA +1034 -0
- infer_stack-0.6.0.dist-info/RECORD +44 -0
- infer_stack-0.6.0.dist-info/WHEEL +5 -0
- infer_stack-0.6.0.dist-info/entry_points.txt +2 -0
- infer_stack-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..catalog import PROFILE_NAME_ALIASES
|
|
4
|
+
from ..catalog import profile_summary
|
|
5
|
+
from ..config import initial_config
|
|
6
|
+
from ..config import load_kubeai_resource_profiles
|
|
7
|
+
from ..config import load_yaml
|
|
8
|
+
from ..config import normalized_catalogs
|
|
9
|
+
from ..config import save_kubeai_resource_profiles
|
|
10
|
+
from ..config import save_yaml
|
|
11
|
+
from ..contracts import load_profile_contract
|
|
12
|
+
from ..kubeai_ops import deploy_rendered_artifacts
|
|
13
|
+
from ..renderer import render_from_lock
|
|
14
|
+
from ..verification import verify_profile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
import json
|
|
18
|
+
import scriptconfig as scfg
|
|
19
|
+
|
|
20
|
+
from .context import (
|
|
21
|
+
_apply_path_overrides,
|
|
22
|
+
_arg_or_env,
|
|
23
|
+
_as_mapping,
|
|
24
|
+
apply_config_overrides,
|
|
25
|
+
backend_name,
|
|
26
|
+
build_plan,
|
|
27
|
+
config_for_runtime,
|
|
28
|
+
config_path,
|
|
29
|
+
effective_allow_unsupported,
|
|
30
|
+
effective_inventory,
|
|
31
|
+
ensure_renderable,
|
|
32
|
+
generated_dir,
|
|
33
|
+
kubeai_generated_dir,
|
|
34
|
+
load_config,
|
|
35
|
+
models_path,
|
|
36
|
+
plan_path,
|
|
37
|
+
runtime_dir_for_config,
|
|
38
|
+
save_plan,
|
|
39
|
+
)
|
|
40
|
+
from .compose import _compose_up_with_router_recreate
|
|
41
|
+
from .options import (
|
|
42
|
+
_AllowUnsupportedMixin,
|
|
43
|
+
_BackendOverrideMixin,
|
|
44
|
+
_ClusterOverridesMixin,
|
|
45
|
+
_ComposeOverrideMixin,
|
|
46
|
+
_PathOverridesMixin,
|
|
47
|
+
_PlanOverridesCLI,
|
|
48
|
+
_PortOverridesMixin,
|
|
49
|
+
_ProfileOverrideMixin,
|
|
50
|
+
_SimulateHardwareMixin,
|
|
51
|
+
_SwitchPathOverridesCLI,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Profile / config management commands
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InitCLI(_PathOverridesMixin):
|
|
60
|
+
"""Write a fresh config.yaml + empty models.yaml under config_root()."""
|
|
61
|
+
|
|
62
|
+
force = scfg.Value(
|
|
63
|
+
False, isflag=True, help='Overwrite an existing config.yaml.'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def main(cls, argv=True, **kwargs):
|
|
68
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
69
|
+
_apply_path_overrides(config)
|
|
70
|
+
cfg_path = config_path()
|
|
71
|
+
if cfg_path.exists() and not config.force:
|
|
72
|
+
raise SystemExit(
|
|
73
|
+
'config.yaml already exists. Use --force to overwrite.'
|
|
74
|
+
)
|
|
75
|
+
save_yaml(cfg_path, initial_config())
|
|
76
|
+
if not models_path().exists():
|
|
77
|
+
save_yaml(models_path(), {'models': {}, 'profiles': {}})
|
|
78
|
+
print(f'Wrote {cfg_path}')
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SetupCLI(
|
|
83
|
+
_PathOverridesMixin,
|
|
84
|
+
_ProfileOverrideMixin,
|
|
85
|
+
_BackendOverrideMixin,
|
|
86
|
+
_ComposeOverrideMixin,
|
|
87
|
+
_PortOverridesMixin,
|
|
88
|
+
_ClusterOverridesMixin,
|
|
89
|
+
):
|
|
90
|
+
"""Create / update config.yaml with the requested overrides applied."""
|
|
91
|
+
|
|
92
|
+
reset = scfg.Value(
|
|
93
|
+
False,
|
|
94
|
+
isflag=True,
|
|
95
|
+
help='Start from default config values before applying overrides.',
|
|
96
|
+
)
|
|
97
|
+
resource_profiles_file = scfg.Value(
|
|
98
|
+
None,
|
|
99
|
+
type=str,
|
|
100
|
+
help='For kubeai setups, sync a local Helm values file with resourceProfiles into kubeai-values.local.yaml.',
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def main(cls, argv=True, **kwargs):
|
|
105
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
106
|
+
_apply_path_overrides(config)
|
|
107
|
+
cfg_path = config_path()
|
|
108
|
+
if cfg_path.exists() and not config.reset:
|
|
109
|
+
cfg = load_yaml(cfg_path)
|
|
110
|
+
else:
|
|
111
|
+
cfg = initial_config()
|
|
112
|
+
cfg = apply_config_overrides(cfg, config)
|
|
113
|
+
save_yaml(cfg_path, cfg)
|
|
114
|
+
if not models_path().exists():
|
|
115
|
+
save_yaml(models_path(), {'models': {}, 'profiles': {}})
|
|
116
|
+
if config.resource_profiles_file:
|
|
117
|
+
# User-supplied path: anchor on CWD so a typed relative path
|
|
118
|
+
# behaves as the user expects.
|
|
119
|
+
source = Path(config.resource_profiles_file)
|
|
120
|
+
if not source.is_absolute():
|
|
121
|
+
source = Path.cwd() / source
|
|
122
|
+
values_doc = load_yaml(source)
|
|
123
|
+
if 'resourceProfiles' not in values_doc:
|
|
124
|
+
raise SystemExit(
|
|
125
|
+
f'{source} is missing a top-level resourceProfiles map'
|
|
126
|
+
)
|
|
127
|
+
target = save_kubeai_resource_profiles(values_doc)
|
|
128
|
+
if plan_path(cfg).exists():
|
|
129
|
+
plan_path(cfg).unlink()
|
|
130
|
+
print(f'Wrote {target}')
|
|
131
|
+
print(f'Wrote {cfg_path}')
|
|
132
|
+
print(
|
|
133
|
+
f'Configured backend={cfg.get("backend", "compose")} '
|
|
134
|
+
f'active_profile={cfg.get("active_profile", "") or "<unset>"}'
|
|
135
|
+
)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ResolveCLI(_PlanOverridesCLI):
|
|
140
|
+
"""Resolve the active profile into a deployment dict and print it."""
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def main(cls, argv=True, **kwargs):
|
|
144
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
145
|
+
_apply_path_overrides(config)
|
|
146
|
+
cfg = config_for_runtime(config)
|
|
147
|
+
plan = build_plan(
|
|
148
|
+
cfg,
|
|
149
|
+
profile_name=config.profile,
|
|
150
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
151
|
+
inventory=effective_inventory(config),
|
|
152
|
+
)
|
|
153
|
+
save_plan(plan, cfg)
|
|
154
|
+
print(json.dumps(plan['deployment'], indent=2))
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ValidateCLI(_PlanOverridesCLI):
|
|
159
|
+
"""Resolve + validate the active profile; exit non-zero on validation errors."""
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def main(cls, argv=True, **kwargs):
|
|
163
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
164
|
+
_apply_path_overrides(config)
|
|
165
|
+
cfg = config_for_runtime(config)
|
|
166
|
+
plan = build_plan(
|
|
167
|
+
cfg,
|
|
168
|
+
profile_name=config.profile,
|
|
169
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
170
|
+
inventory=effective_inventory(config),
|
|
171
|
+
)
|
|
172
|
+
save_plan(plan, cfg)
|
|
173
|
+
print(json.dumps(plan['validated'], indent=2))
|
|
174
|
+
return 0 if plan['validated']['ok'] else 2
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class LockCLI(_PlanOverridesCLI):
|
|
178
|
+
"""Write plan.yaml after resolving + validating the active profile."""
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def main(cls, argv=True, **kwargs):
|
|
182
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
183
|
+
_apply_path_overrides(config)
|
|
184
|
+
cfg = config_for_runtime(config)
|
|
185
|
+
plan = build_plan(
|
|
186
|
+
cfg,
|
|
187
|
+
profile_name=config.profile,
|
|
188
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
189
|
+
inventory=effective_inventory(config),
|
|
190
|
+
)
|
|
191
|
+
if not plan['validated']['ok'] and not plan['allow_unsupported']:
|
|
192
|
+
raise SystemExit(
|
|
193
|
+
'Refusing to write plan.yaml because validation failed. Use --allow-unsupported to override.'
|
|
194
|
+
)
|
|
195
|
+
save_plan(plan, cfg)
|
|
196
|
+
print(json.dumps(plan, indent=2))
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class RenderCLI(_PlanOverridesCLI):
|
|
201
|
+
"""Render the active profile's deployment into compose/kubeai artifacts."""
|
|
202
|
+
|
|
203
|
+
yes = scfg.Value(
|
|
204
|
+
False,
|
|
205
|
+
isflag=True,
|
|
206
|
+
short_alias=['y'],
|
|
207
|
+
help='Apply rendered changes without prompting. Without this, render shows a per-file diff and asks for confirmation.',
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def main(cls, argv=True, **kwargs):
|
|
212
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
213
|
+
_apply_path_overrides(config)
|
|
214
|
+
cfg = config_for_runtime(config)
|
|
215
|
+
plan = build_plan(
|
|
216
|
+
cfg,
|
|
217
|
+
profile_name=config.profile,
|
|
218
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
219
|
+
inventory=effective_inventory(config),
|
|
220
|
+
)
|
|
221
|
+
ensure_renderable(plan)
|
|
222
|
+
save_plan(plan, cfg)
|
|
223
|
+
render_from_lock(plan, assume_yes=bool(config.yes))
|
|
224
|
+
print(f'Wrote {plan_path(cfg)}')
|
|
225
|
+
if backend_name(cfg) == 'kubeai':
|
|
226
|
+
print(f'Rendered KubeAI artifacts into {kubeai_generated_dir(cfg)}')
|
|
227
|
+
else:
|
|
228
|
+
print(f'Rendered Compose into {generated_dir(cfg)}')
|
|
229
|
+
print(
|
|
230
|
+
f'Rendered mounted runtime files into {runtime_dir_for_config(cfg)}'
|
|
231
|
+
)
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class SwitchCLI(_SwitchPathOverridesCLI):
|
|
236
|
+
"""Persist a new active_profile and re-render (optionally re-applying)."""
|
|
237
|
+
|
|
238
|
+
profile = scfg.Value(
|
|
239
|
+
None, type=str, position=1, help='Profile name to switch to.'
|
|
240
|
+
)
|
|
241
|
+
apply = scfg.Value(
|
|
242
|
+
False,
|
|
243
|
+
isflag=True,
|
|
244
|
+
help='Also apply the new profile to the running stack.',
|
|
245
|
+
)
|
|
246
|
+
yes = scfg.Value(
|
|
247
|
+
False,
|
|
248
|
+
isflag=True,
|
|
249
|
+
short_alias=['y'],
|
|
250
|
+
help='Apply rendered changes without prompting.',
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def main(cls, argv=True, **kwargs):
|
|
255
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
256
|
+
_apply_path_overrides(config)
|
|
257
|
+
if not config.profile:
|
|
258
|
+
raise SystemExit('switch: missing required profile name')
|
|
259
|
+
persisted_cfg = load_config()
|
|
260
|
+
persisted_cfg['active_profile'] = config.profile
|
|
261
|
+
save_yaml(config_path(), persisted_cfg)
|
|
262
|
+
cfg = apply_config_overrides(persisted_cfg, config)
|
|
263
|
+
|
|
264
|
+
plan = build_plan(
|
|
265
|
+
cfg,
|
|
266
|
+
profile_name=config.profile,
|
|
267
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
268
|
+
inventory=effective_inventory(config),
|
|
269
|
+
)
|
|
270
|
+
ensure_renderable(plan)
|
|
271
|
+
save_plan(plan, cfg)
|
|
272
|
+
render_from_lock(plan, assume_yes=bool(config.yes))
|
|
273
|
+
if config.apply:
|
|
274
|
+
if backend_name(cfg) == 'compose':
|
|
275
|
+
_compose_up_with_router_recreate(cfg, detach=True)
|
|
276
|
+
else:
|
|
277
|
+
deploy_rendered_artifacts(plan['deployment'])
|
|
278
|
+
print(f'Switched active_profile to {config.profile}')
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ListModelsCLI(_PathOverridesMixin):
|
|
283
|
+
"""Print every model in the merged catalog."""
|
|
284
|
+
|
|
285
|
+
__command__ = 'list-models'
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def main(cls, argv=True, **kwargs):
|
|
289
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
290
|
+
_apply_path_overrides(config)
|
|
291
|
+
cfg = load_config() if config_path().exists() else initial_config()
|
|
292
|
+
cats = normalized_catalogs(cfg)
|
|
293
|
+
for name, model in cats.get('models', {}).items():
|
|
294
|
+
ref = model.get('hf_model_id') or model.get('url', '')
|
|
295
|
+
print(f'{name}: {ref}')
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class ListProfilesCLI(_PathOverridesMixin):
|
|
300
|
+
"""Print every serving profile in the merged catalog."""
|
|
301
|
+
|
|
302
|
+
__command__ = 'list-profiles'
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def main(cls, argv=True, **kwargs):
|
|
306
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
307
|
+
_apply_path_overrides(config)
|
|
308
|
+
cfg = load_config() if config_path().exists() else initial_config()
|
|
309
|
+
cats = normalized_catalogs(cfg)
|
|
310
|
+
profiles = cats.get('profiles', {})
|
|
311
|
+
hidden_legacy = set(PROFILE_NAME_ALIASES)
|
|
312
|
+
for name, profile in profiles.items():
|
|
313
|
+
if name in hidden_legacy:
|
|
314
|
+
continue
|
|
315
|
+
if profile.get('kind') == 'invalid-profile':
|
|
316
|
+
continue
|
|
317
|
+
summary = profile_summary(profile)
|
|
318
|
+
providers = ','.join(summary.get('providers', [])) or 'none'
|
|
319
|
+
print(
|
|
320
|
+
f'{name}: providers={providers} gateway={summary["gateway"]} '
|
|
321
|
+
f'frontend={summary["frontend"]} frontend_provider={summary["frontend_provider"]} '
|
|
322
|
+
f'routes={summary["route_count"]}'
|
|
323
|
+
)
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ExplainCLI(_PathOverridesMixin):
|
|
328
|
+
"""Pretty-print a YAML file (defaults to the current plan.yaml)."""
|
|
329
|
+
|
|
330
|
+
__command__ = 'explain'
|
|
331
|
+
|
|
332
|
+
file = scfg.Value(
|
|
333
|
+
None, type=str, help='Path to read (default: current plan.yaml).'
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def main(cls, argv=True, **kwargs):
|
|
338
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
339
|
+
_apply_path_overrides(config)
|
|
340
|
+
if config.file:
|
|
341
|
+
target = Path(config.file)
|
|
342
|
+
if not target.is_absolute():
|
|
343
|
+
target = Path.cwd() / target
|
|
344
|
+
else:
|
|
345
|
+
target = plan_path()
|
|
346
|
+
if not target.exists():
|
|
347
|
+
raise SystemExit(f'Missing file: {target}')
|
|
348
|
+
print(json.dumps(load_yaml(target), indent=2))
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class DescribeProfileCLI(
|
|
353
|
+
_PathOverridesMixin,
|
|
354
|
+
_BackendOverrideMixin,
|
|
355
|
+
_AllowUnsupportedMixin,
|
|
356
|
+
_SimulateHardwareMixin,
|
|
357
|
+
):
|
|
358
|
+
"""Print the profile contract for a given profile name."""
|
|
359
|
+
|
|
360
|
+
__command__ = 'describe-profile'
|
|
361
|
+
|
|
362
|
+
profile = scfg.Value(
|
|
363
|
+
None, type=str, position=1, help='Profile name to describe.'
|
|
364
|
+
)
|
|
365
|
+
format = scfg.Value('yaml', choices=['json', 'yaml'])
|
|
366
|
+
output = scfg.Value(
|
|
367
|
+
None, type=str, help='Write to this file instead of stdout.'
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
def main(cls, argv=True, **kwargs):
|
|
372
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
373
|
+
_apply_path_overrides(config)
|
|
374
|
+
overrides = _as_mapping(config)
|
|
375
|
+
if not overrides.get('profile'):
|
|
376
|
+
raise SystemExit('describe-profile: missing required profile name')
|
|
377
|
+
contract = load_profile_contract(
|
|
378
|
+
overrides['profile'],
|
|
379
|
+
backend=_arg_or_env(overrides, 'backend', 'INFER_STACK_BACKEND'),
|
|
380
|
+
simulate_hardware_spec=overrides.get('simulate_hardware'),
|
|
381
|
+
)
|
|
382
|
+
return _print_structured(
|
|
383
|
+
contract, overrides['format'], overrides.get('output')
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _print_structured(
|
|
388
|
+
data: dict[str, Any], fmt: str, output: str | None
|
|
389
|
+
) -> int:
|
|
390
|
+
if fmt == 'yaml':
|
|
391
|
+
import yaml
|
|
392
|
+
|
|
393
|
+
text = yaml.safe_dump(data, sort_keys=False)
|
|
394
|
+
else:
|
|
395
|
+
text = json.dumps(data, indent=2)
|
|
396
|
+
if output:
|
|
397
|
+
target = Path(output)
|
|
398
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
399
|
+
target.write_text(
|
|
400
|
+
text + ('' if text.endswith('\n') else '\n'), encoding='utf-8'
|
|
401
|
+
)
|
|
402
|
+
print(f'Wrote {target}')
|
|
403
|
+
return 0
|
|
404
|
+
print(text)
|
|
405
|
+
return 0
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class VerifyProfileCLI(_SwitchPathOverridesCLI):
|
|
409
|
+
"""Sanity-check a resolved profile (post-render expectations)."""
|
|
410
|
+
|
|
411
|
+
__command__ = 'verify-profile'
|
|
412
|
+
|
|
413
|
+
profile = scfg.Value(
|
|
414
|
+
None, type=str, position=1, help='Profile name to verify.'
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def main(cls, argv=True, **kwargs):
|
|
419
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
420
|
+
_apply_path_overrides(config)
|
|
421
|
+
if not config.profile:
|
|
422
|
+
raise SystemExit('verify-profile: missing required profile name')
|
|
423
|
+
cfg = config_for_runtime(config)
|
|
424
|
+
plan = build_plan(
|
|
425
|
+
cfg,
|
|
426
|
+
profile_name=config.profile,
|
|
427
|
+
allow_unsupported=effective_allow_unsupported(config, cfg),
|
|
428
|
+
inventory=effective_inventory(config),
|
|
429
|
+
)
|
|
430
|
+
result = verify_profile(plan['deployment'])
|
|
431
|
+
print(json.dumps(result, indent=2))
|
|
432
|
+
return 0 if result['ok'] else 2
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class KubeaiSyncResourceProfilesCLI(_PathOverridesMixin):
|
|
436
|
+
"""Pull a Helm ``resourceProfiles`` values file into kubeai-values.local.yaml."""
|
|
437
|
+
|
|
438
|
+
__command__ = 'kubeai-sync-resource-profiles'
|
|
439
|
+
|
|
440
|
+
from_file = scfg.Value(
|
|
441
|
+
None,
|
|
442
|
+
type=str,
|
|
443
|
+
required=True,
|
|
444
|
+
help='Helm values file with a top-level resourceProfiles map.',
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def main(cls, argv=True, **kwargs):
|
|
449
|
+
config = cls.cli(argv=argv, data=kwargs)
|
|
450
|
+
_apply_path_overrides(config)
|
|
451
|
+
cfg = config_for_runtime(config, allow_missing=True)
|
|
452
|
+
# User-supplied path: anchor on CWD.
|
|
453
|
+
source = Path(config.from_file)
|
|
454
|
+
if not source.is_absolute():
|
|
455
|
+
source = Path.cwd() / source
|
|
456
|
+
values_doc = load_yaml(source)
|
|
457
|
+
if 'resourceProfiles' not in values_doc:
|
|
458
|
+
raise SystemExit(
|
|
459
|
+
f'{source} is missing a top-level resourceProfiles map'
|
|
460
|
+
)
|
|
461
|
+
target = save_kubeai_resource_profiles(values_doc)
|
|
462
|
+
if plan_path(cfg).exists():
|
|
463
|
+
plan_path(cfg).unlink()
|
|
464
|
+
profiles, _, _ = load_kubeai_resource_profiles()
|
|
465
|
+
print(f'Wrote {target}')
|
|
466
|
+
print(f'Synced {len(profiles)} KubeAI resource profile(s)')
|
|
467
|
+
return 0
|