sima-cli 2.1.12__tar.gz → 2.1.13__tar.gz
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.
- {sima_cli-2.1.12/sima_cli.egg-info → sima_cli-2.1.13}/PKG-INFO +1 -1
- {sima_cli-2.1.12 → sima_cli-2.1.13}/pyproject.toml +1 -1
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/__version__.py +1 -1
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/cmdexec.py +16 -4
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/commands.py +78 -5
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/install.py +10 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/neat.py +128 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/utils.py +4 -2
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/docker.py +68 -39
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/vulcan/artifacts.py +35 -12
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/vulcan/commands.py +6 -2
- {sima_cli-2.1.12 → sima_cli-2.1.13/sima_cli.egg-info}/PKG-INFO +1 -1
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_docker_utils.py +49 -61
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_sdk_image_detection.py +251 -4
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_vulcan.py +108 -4
- {sima_cli-2.1.12 → sima_cli-2.1.13}/LICENSE +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/MANIFEST.in +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/README.md +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/requirements.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/setup.cfg +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/setup.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/__main__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/app_zoo/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/app_zoo/app.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/app_zoo/commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/auth/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/auth/auth0.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/auth/devportal.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/auth/login.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/cli.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/data/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/data/resources_internal.yaml +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/data/resources_public.yaml +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/deploy_only/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/deploy_only/device/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/deploy_only/device/commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/deploy_only/mpk/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/deploy_only/mpk/commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/discover/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/discover/discover.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/discover/linuxll.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/download/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/download/downloader.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/compatibility.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/github_assets.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/hostdriver.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/metadata_info.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/metadata_installer.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/metadata_validator.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/optiview.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/package_builder.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/palette.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/install/registry.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/mla/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/mla/meminfo.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/model_zoo/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/model_zoo/model.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/network/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/network/network.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/playbooks/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/playbooks/commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/playbooks/manager.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/config.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/linux_devkit_network.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/linux_shared_network.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/network_doctor.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/preinstall.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/requirements.json +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/script.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/stop.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/sdk/uninstall.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/serial/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/serial/serial.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/storage/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/storage/nvme.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/storage/sdcard.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/bmaptool.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/bootimg.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/cleanlog.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/elxr.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/local.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/netboot.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/query.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/remote.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/update/updater.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/upgrade/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/upgrade/selfupdate.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/api_common.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/artifactory.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/common.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/config.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/config_loader.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/container_registries.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/deprecation.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/device_api.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/disk.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/env.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/errors.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/mpk_api.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/net.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/network.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/pcie.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/pkg_update_check.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/serializers.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/services.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/utils/tag.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli/vulcan/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli.egg-info/SOURCES.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli.egg-info/dependency_links.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli.egg-info/entry_points.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli.egg-info/requires.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/sima_cli.egg-info/top_level.txt +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/e2e/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/__init__.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_app_zoo.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_appzoo_commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_auth.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_auth0.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_cli.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_cli_stdio.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_cli_update.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_cli_update_rerun.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_deprecation.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_download.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_elxr_update.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_firmware.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_generate_cli_markdown_docs.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_install_stub.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_metadata_installer.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_metadata_validator.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_model_zoo.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_netboot.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_package_builder.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_pkg_update_check.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_sdk_linux_shared_network.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_sdk_network_doctor.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_sdk_preinstall.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_sdk_uninstall.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_selfupdate.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_skills_commands.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_skills_manager.py +0 -0
- {sima_cli-2.1.12 → sima_cli-2.1.13}/tests/unit/test_utils.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# sima_cli/__version__.py
|
|
2
|
-
__version__ = "2.1.
|
|
2
|
+
__version__ = "2.1.13"
|
|
@@ -18,10 +18,15 @@ from sima_cli.sdk.utils import (
|
|
|
18
18
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
class SdkContainerUnavailable(RuntimeError):
|
|
23
|
+
"""Raised when no running SDK container matches a requested tool."""
|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
# ─────────────────────────────────────────────
|
|
22
27
|
# Core executer
|
|
23
28
|
# ─────────────────────────────────────────────
|
|
24
|
-
def exec_container_cmd(ctx, keyword: str, cmd: Optional[str] = None):
|
|
29
|
+
def exec_container_cmd(ctx, keyword: str, cmd: Optional[str] = None, raise_on_missing: bool = False):
|
|
25
30
|
"""
|
|
26
31
|
Find a running container matching the given SDK keyword (e.g. mpk, yocto),
|
|
27
32
|
optionally filtering by version (from ctx.obj['version_filter']),
|
|
@@ -34,6 +39,8 @@ def exec_container_cmd(ctx, keyword: str, cmd: Optional[str] = None):
|
|
|
34
39
|
containers = get_all_containers(running_containers_only=True)
|
|
35
40
|
if not containers:
|
|
36
41
|
console.print("[yellow]⚠️ No running containers found.[/yellow]")
|
|
42
|
+
if raise_on_missing:
|
|
43
|
+
raise SdkContainerUnavailable("No running containers found.")
|
|
37
44
|
sys.exit(0)
|
|
38
45
|
|
|
39
46
|
# ──────────────────────────────────────────────
|
|
@@ -61,11 +68,16 @@ def exec_container_cmd(ctx, keyword: str, cmd: Optional[str] = None):
|
|
|
61
68
|
)
|
|
62
69
|
|
|
63
70
|
if not matches:
|
|
64
|
-
|
|
65
|
-
f"
|
|
71
|
+
message = (
|
|
72
|
+
f"No running containers found for '{keyword}'"
|
|
66
73
|
+ (f" with version '{version_filter}'" if version_filter else "")
|
|
67
|
-
+ ".
|
|
74
|
+
+ "."
|
|
75
|
+
)
|
|
76
|
+
console.print(
|
|
77
|
+
f"[red]❌ {message}[/red]"
|
|
68
78
|
)
|
|
79
|
+
if raise_on_missing:
|
|
80
|
+
raise SdkContainerUnavailable(message)
|
|
69
81
|
sys.exit(1)
|
|
70
82
|
|
|
71
83
|
# ──────────────────────────────────────────────
|
|
@@ -24,10 +24,10 @@ from typing import Optional
|
|
|
24
24
|
from rich.console import Console
|
|
25
25
|
from rich.panel import Panel
|
|
26
26
|
from sima_cli.sdk.install import setup_and_start
|
|
27
|
-
from sima_cli.sdk.cmdexec import exec_container_cmd
|
|
27
|
+
from sima_cli.sdk.cmdexec import SdkContainerUnavailable, exec_container_cmd
|
|
28
28
|
from sima_cli.sdk.uninstall import remove_containers, remove_unused_images
|
|
29
29
|
from sima_cli.sdk.stop import stop_containers
|
|
30
|
-
from sima_cli.sdk.utils import get_all_containers
|
|
30
|
+
from sima_cli.sdk.utils import get_all_containers, container_matches_sdk_keyword
|
|
31
31
|
from sima_cli.sdk.utils import extract_short_name
|
|
32
32
|
from sima_cli.discover.discover import discover_and_probe
|
|
33
33
|
from rich.table import Table
|
|
@@ -38,6 +38,7 @@ from sima_cli.sdk.config import IMAGE_CONFIG
|
|
|
38
38
|
from sima_cli.sdk.network_doctor import (
|
|
39
39
|
build_network_doctor_report,
|
|
40
40
|
collect_network_doctor_bundle,
|
|
41
|
+
ensure_existing_neat_container_startable,
|
|
41
42
|
print_network_doctor_report,
|
|
42
43
|
repair_linux_devkit_network,
|
|
43
44
|
)
|
|
@@ -156,7 +157,66 @@ def _resolve_devkit_ipv4(devkit: Optional[str]) -> str:
|
|
|
156
157
|
return value
|
|
157
158
|
|
|
158
159
|
|
|
159
|
-
def
|
|
160
|
+
def _container_name(container) -> str:
|
|
161
|
+
return container.get("Names") or container.get("Name") or container.get("name") or ""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _container_is_running(container) -> bool:
|
|
165
|
+
state = (container.get("State") or "").lower().strip()
|
|
166
|
+
status = (container.get("Status") or "").lower().strip()
|
|
167
|
+
return state == "running" or status.startswith("up") or "running" in status
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _version_matches(container, version_filter: str) -> bool:
|
|
171
|
+
if not version_filter:
|
|
172
|
+
return True
|
|
173
|
+
needle = version_filter.lower()
|
|
174
|
+
name = _container_name(container).lower()
|
|
175
|
+
image = (container.get("Image") or container.get("image") or "").lower()
|
|
176
|
+
return needle in name or needle in image
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _start_stopped_neat_containers(ctx) -> None:
|
|
180
|
+
version_filter = None
|
|
181
|
+
if ctx and getattr(ctx, "obj", None):
|
|
182
|
+
version_filter = ctx.obj.get("version_filter")
|
|
183
|
+
|
|
184
|
+
containers = get_all_containers(running_containers_only=False)
|
|
185
|
+
matches = [
|
|
186
|
+
container
|
|
187
|
+
for container in containers
|
|
188
|
+
if container_matches_sdk_keyword(container, "neat")
|
|
189
|
+
and _version_matches(container, version_filter)
|
|
190
|
+
and not _container_is_running(container)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if not matches:
|
|
194
|
+
message = "No stopped Neat SDK containers found."
|
|
195
|
+
if version_filter:
|
|
196
|
+
message = f"No stopped Neat SDK containers found for version '{version_filter}'."
|
|
197
|
+
raise click.ClickException(f"{message} Run: sima-cli sdk setup")
|
|
198
|
+
|
|
199
|
+
for container in matches:
|
|
200
|
+
name = _container_name(container)
|
|
201
|
+
if not name:
|
|
202
|
+
continue
|
|
203
|
+
try:
|
|
204
|
+
ensure_existing_neat_container_startable(name)
|
|
205
|
+
console.print(
|
|
206
|
+
f"[cyan]▶ Starting existing Neat SDK container:[/cyan] [bold]{name}[/bold] "
|
|
207
|
+
"[dim](this may take a moment)[/dim]"
|
|
208
|
+
)
|
|
209
|
+
subprocess.run(["docker", "start", name], check=True, capture_output=True, text=True)
|
|
210
|
+
except subprocess.CalledProcessError as e:
|
|
211
|
+
raise click.ClickException(
|
|
212
|
+
f"Failed to start Neat SDK container '{name}' while running: {' '.join(e.cmd)}"
|
|
213
|
+
) from e
|
|
214
|
+
except RuntimeError as e:
|
|
215
|
+
raise click.ClickException(str(e)) from e
|
|
216
|
+
console.print(f"[green]✅ Started Neat SDK container:[/green] [bold]{name}[/bold]")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def launch_sdk_tool(tool: str, cmd, ctx, recover_unavailable: bool = False):
|
|
160
220
|
"""
|
|
161
221
|
Launch a selected SDK tool container, optionally executing a command inside it.
|
|
162
222
|
If no command is provided, defaults to an interactive bash login shell.
|
|
@@ -182,7 +242,17 @@ def launch_sdk_tool(tool: str, cmd, ctx):
|
|
|
182
242
|
else:
|
|
183
243
|
cmd_str = str(cmd)
|
|
184
244
|
|
|
185
|
-
|
|
245
|
+
if not recover_unavailable:
|
|
246
|
+
exec_container_cmd(ctx, tool, cmd_str)
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
exec_container_cmd(ctx, tool, cmd_str, raise_on_missing=True)
|
|
251
|
+
except SdkContainerUnavailable:
|
|
252
|
+
if tool != "neat":
|
|
253
|
+
raise
|
|
254
|
+
_start_stopped_neat_containers(ctx)
|
|
255
|
+
exec_container_cmd(ctx, tool, cmd_str)
|
|
186
256
|
|
|
187
257
|
|
|
188
258
|
# ------------------------------------------------------------
|
|
@@ -554,13 +624,16 @@ def neat(ctx, cmd):
|
|
|
554
624
|
running container with bash -lc. If CMD is omitted, sima-cli opens an
|
|
555
625
|
interactive login shell.
|
|
556
626
|
|
|
627
|
+
If no matching Neat SDK container is running, existing stopped Neat SDK
|
|
628
|
+
container(s) are started automatically and the command is retried.
|
|
629
|
+
|
|
557
630
|
\b
|
|
558
631
|
Examples:
|
|
559
632
|
sima-cli sdk neat
|
|
560
633
|
sima-cli sdk neat python --version
|
|
561
634
|
sima-cli sdk neat "python app.py --config config.json"
|
|
562
635
|
"""
|
|
563
|
-
launch_sdk_tool("neat", cmd, ctx)
|
|
636
|
+
launch_sdk_tool("neat", cmd, ctx, recover_unavailable=True)
|
|
564
637
|
|
|
565
638
|
|
|
566
639
|
@sdk.command(
|
|
@@ -1148,6 +1148,16 @@ def setup_and_start(
|
|
|
1148
1148
|
configure_container_user(existing_container, login_name, user_uid, user_gid)
|
|
1149
1149
|
|
|
1150
1150
|
if devkit_env and is_neat_sdk_image(img):
|
|
1151
|
+
if not skip_insight:
|
|
1152
|
+
# Re-issue the mounted HTTPS cert for the (possibly new) host
|
|
1153
|
+
# IP before bootstrap restarts neat-insight, so the browser
|
|
1154
|
+
# doesn't hit a SAN mismatch ("connection is not private")
|
|
1155
|
+
# after a re-point.
|
|
1156
|
+
from sima_cli.sdk.neat import refresh_neat_certificates
|
|
1157
|
+
refresh_neat_certificates(
|
|
1158
|
+
workspace, existing_container, devkit_env,
|
|
1159
|
+
yes_to_all=yes_to_all, noninteractive=noninteractive,
|
|
1160
|
+
)
|
|
1151
1161
|
bootstrap_devkit_container(existing_container, devkit_env)
|
|
1152
1162
|
|
|
1153
1163
|
if len(selected_images) == 1:
|
|
@@ -256,6 +256,28 @@ def _collect_physical_ipv4s_for_certs() -> List[str]:
|
|
|
256
256
|
return hosts
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
def _collect_all_ipv4s_for_certs() -> List[str]:
|
|
260
|
+
"""Every usable IPv4 on the host, INCLUDING VPN/tunnel interfaces.
|
|
261
|
+
|
|
262
|
+
Unlike the advertised-URL selection (which deliberately prefers a physical
|
|
263
|
+
interface), a certificate SAN should be inclusive: list every address the
|
|
264
|
+
browser might connect to so HTTPS is trusted whether or not a VPN is up.
|
|
265
|
+
Without this, a cert minted while a VPN was connected covers only the VPN IP
|
|
266
|
+
(and vice versa), so toggling the VPN flips between "secure" and "not secure".
|
|
267
|
+
"""
|
|
268
|
+
hosts = []
|
|
269
|
+
try:
|
|
270
|
+
import psutil
|
|
271
|
+
except Exception:
|
|
272
|
+
return hosts
|
|
273
|
+
|
|
274
|
+
for _iface, addrs in psutil.net_if_addrs().items():
|
|
275
|
+
for addr in addrs:
|
|
276
|
+
if addr.family == socket.AF_INET and _is_usable_ipv4(addr.address):
|
|
277
|
+
hosts.append(addr.address)
|
|
278
|
+
return hosts
|
|
279
|
+
|
|
280
|
+
|
|
259
281
|
def _collect_cert_hosts(devkit_env: Optional[dict]) -> List[str]:
|
|
260
282
|
hosts = ["localhost", "127.0.0.1"]
|
|
261
283
|
if devkit_env and devkit_env.get("host_ip"):
|
|
@@ -268,6 +290,8 @@ def _collect_cert_hosts(devkit_env: Optional[dict]) -> List[str]:
|
|
|
268
290
|
except Exception:
|
|
269
291
|
pass
|
|
270
292
|
hosts.extend(_collect_physical_ipv4s_for_certs())
|
|
293
|
+
# Include VPN/tunnel IPv4s too so the cert stays valid across VPN on/off.
|
|
294
|
+
hosts.extend(_collect_all_ipv4s_for_certs())
|
|
271
295
|
|
|
272
296
|
seen = set()
|
|
273
297
|
return [host for host in hosts if host and not (host in seen or seen.add(host))]
|
|
@@ -474,6 +498,110 @@ def _ensure_certificates(cert_dir: Path, devkit_env: Optional[dict], yes_to_all:
|
|
|
474
498
|
return cert_file, key_file
|
|
475
499
|
|
|
476
500
|
|
|
501
|
+
def _cert_covers_host(cert_file: Path, host_ip: str) -> bool:
|
|
502
|
+
"""Return True if the cert's SAN already lists ``host_ip`` as an IP address.
|
|
503
|
+
|
|
504
|
+
Used to avoid needlessly re-issuing (and disrupting) the certificate on a
|
|
505
|
+
re-point when the host IP hasn't changed. On any failure to verify (no
|
|
506
|
+
openssl, parse error) returns False so the caller re-issues to be safe.
|
|
507
|
+
"""
|
|
508
|
+
if not host_ip:
|
|
509
|
+
return False
|
|
510
|
+
try:
|
|
511
|
+
result = subprocess.run(
|
|
512
|
+
["openssl", "x509", "-noout", "-ext", "subjectAltName", "-in", str(cert_file)],
|
|
513
|
+
capture_output=True, text=True, check=False,
|
|
514
|
+
)
|
|
515
|
+
text = result.stdout or ""
|
|
516
|
+
if result.returncode != 0 or "IP Address" not in text:
|
|
517
|
+
result = subprocess.run(
|
|
518
|
+
["openssl", "x509", "-noout", "-text", "-in", str(cert_file)],
|
|
519
|
+
capture_output=True, text=True, check=False,
|
|
520
|
+
)
|
|
521
|
+
text = result.stdout or ""
|
|
522
|
+
# Parse the comma-separated "IP Address:<ip>" SAN entries and match
|
|
523
|
+
# exactly. A bare substring check would treat a routed IP that is a
|
|
524
|
+
# *prefix* of an existing entry as covered (e.g. 192.168.1.5 vs
|
|
525
|
+
# IP Address:192.168.1.50), wrongly skipping re-issue and leaving the
|
|
526
|
+
# browser with a SAN mismatch after that re-point.
|
|
527
|
+
san_ips = []
|
|
528
|
+
for token in text.replace("\n", ",").split(","):
|
|
529
|
+
token = token.strip()
|
|
530
|
+
if token.startswith("IP Address:"):
|
|
531
|
+
san_ips.append(token[len("IP Address:"):].strip())
|
|
532
|
+
return host_ip in san_ips
|
|
533
|
+
except OSError:
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _container_sdk_cert_dir(container_name: str) -> Optional[Path]:
|
|
538
|
+
"""Return the host directory bind-mounted to ``/sdk-cert`` in the running
|
|
539
|
+
container, or ``None`` if it can't be determined.
|
|
540
|
+
|
|
541
|
+
The ``/sdk-cert`` mount is fixed at ``docker run`` time to the workspace used
|
|
542
|
+
*then*. We must re-issue into that exact directory, not one reconstructed
|
|
543
|
+
from the current invocation's workspace -- otherwise a user re-running setup
|
|
544
|
+
from a different workspace would write the new cert into an unmounted dir
|
|
545
|
+
while the container keeps serving the old mounted one.
|
|
546
|
+
"""
|
|
547
|
+
try:
|
|
548
|
+
result = subprocess.run(
|
|
549
|
+
["docker", "inspect", "-f",
|
|
550
|
+
'{{range .Mounts}}{{if eq .Destination "/sdk-cert"}}{{.Source}}{{end}}{{end}}',
|
|
551
|
+
container_name],
|
|
552
|
+
capture_output=True, text=True, check=False,
|
|
553
|
+
)
|
|
554
|
+
except OSError:
|
|
555
|
+
return None
|
|
556
|
+
if result.returncode != 0:
|
|
557
|
+
return None
|
|
558
|
+
source = (result.stdout or "").strip()
|
|
559
|
+
return Path(source) if source else None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def refresh_neat_certificates(
|
|
563
|
+
workspace: str,
|
|
564
|
+
container_name: str,
|
|
565
|
+
devkit_env: Optional[dict],
|
|
566
|
+
*,
|
|
567
|
+
yes_to_all: bool = False,
|
|
568
|
+
noninteractive: bool = False,
|
|
569
|
+
) -> bool:
|
|
570
|
+
"""Re-issue the mounted ``/sdk-cert`` certificate to cover the current host IP.
|
|
571
|
+
|
|
572
|
+
On a DevKit re-point (or a host network change) the routed host IP changes,
|
|
573
|
+
but the HTTPS cert mounted into the Neat container was signed once at
|
|
574
|
+
container creation with the *old* IP in its SAN -- so the browser shows
|
|
575
|
+
"Your connection is not private" on the new IP. The host owns the trusted
|
|
576
|
+
mkcert CA, so the cert must be reissued *here* (not inside the container) to
|
|
577
|
+
stay trusted; neat-insight is restarted separately (devkit.sh's refresh hook)
|
|
578
|
+
to pick up the rewritten file from the bind-mounted cert dir.
|
|
579
|
+
|
|
580
|
+
(Re)generates when the cert is **missing** (e.g. the user deleted it) or its
|
|
581
|
+
SAN doesn't cover the current host IP. No-op (returns False) only when there
|
|
582
|
+
is no host IP, or a valid cert already covers it. Returns True when a cert was
|
|
583
|
+
generated.
|
|
584
|
+
"""
|
|
585
|
+
host_ip = (devkit_env or {}).get("host_ip", "")
|
|
586
|
+
if not host_ip:
|
|
587
|
+
return False
|
|
588
|
+
# Use the directory actually bind-mounted to /sdk-cert in the container, so a
|
|
589
|
+
# re-issue lands where neat-insight reads it even if the current invocation's
|
|
590
|
+
# workspace differs from the one the container was created with. Fall back to
|
|
591
|
+
# the workspace-derived path (e.g. container not running / no mount).
|
|
592
|
+
cert_dir = _container_sdk_cert_dir(container_name) \
|
|
593
|
+
or (Path(workspace) / f".{container_name}" / "sdk-cert")
|
|
594
|
+
cert_file = cert_dir / "neat-sdk.pem"
|
|
595
|
+
if cert_file.exists() and _cert_covers_host(cert_file, host_ip):
|
|
596
|
+
return False
|
|
597
|
+
action = "Re-issuing" if cert_file.exists() else "Generating"
|
|
598
|
+
print(f"🔐 {action} Neat SDK certificate for host IP {host_ip} ...")
|
|
599
|
+
_ensure_certificates(
|
|
600
|
+
cert_dir, devkit_env, yes_to_all=yes_to_all, noninteractive=noninteractive
|
|
601
|
+
)
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
|
|
477
605
|
def _write_port_map(config_dir: Path, port_map: Dict) -> Path:
|
|
478
606
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
479
607
|
path = config_dir / "neat-port-map.json"
|
|
@@ -873,6 +873,7 @@ def ensure_model_sdk_extension_installed(
|
|
|
873
873
|
print("ℹ️ Skipping Model Compiler extension install.")
|
|
874
874
|
return
|
|
875
875
|
|
|
876
|
+
internal_sima_cli_env = "export SIMA_CLI_AUTO_ACCEPT_UPDATE=1; "
|
|
876
877
|
print("ℹ️ Logging in to sima-cli before installing the Model Compiler extension...")
|
|
877
878
|
run_command(
|
|
878
879
|
_docker_exec_interactive_prefix() + [
|
|
@@ -881,7 +882,7 @@ def ensure_model_sdk_extension_installed(
|
|
|
881
882
|
sdk_container_name,
|
|
882
883
|
"bash",
|
|
883
884
|
"-lc",
|
|
884
|
-
"sima-cli login",
|
|
885
|
+
f"{internal_sima_cli_env}sima-cli login",
|
|
885
886
|
]
|
|
886
887
|
)
|
|
887
888
|
|
|
@@ -892,6 +893,7 @@ def ensure_model_sdk_extension_installed(
|
|
|
892
893
|
f"export HOME={shlex.quote(home_directory)}; "
|
|
893
894
|
f"export USER={shlex.quote(login_name)}; "
|
|
894
895
|
f"export LOGNAME={shlex.quote(login_name)}; "
|
|
896
|
+
f"{internal_sima_cli_env}"
|
|
895
897
|
"export PATH=\"$HOME/.sima-cli/.venv/bin:$HOME/.local/bin:$PATH\"; "
|
|
896
898
|
"mkdir -p \"$HOME/extension-installation\"; "
|
|
897
899
|
"cd \"$HOME/extension-installation\"; "
|
|
@@ -1206,7 +1208,7 @@ def install_neat_playbooks(sdk_container_name: str, login_name: str) -> None:
|
|
|
1206
1208
|
"-u",
|
|
1207
1209
|
login_name,
|
|
1208
1210
|
"-e",
|
|
1209
|
-
"
|
|
1211
|
+
"SIMA_CLI_AUTO_ACCEPT_UPDATE=1",
|
|
1210
1212
|
"-e",
|
|
1211
1213
|
"GITHUB_TOKEN",
|
|
1212
1214
|
sdk_container_name,
|
|
@@ -9,8 +9,6 @@ import getpass
|
|
|
9
9
|
|
|
10
10
|
from sima_cli.utils.env import get_environment_type
|
|
11
11
|
|
|
12
|
-
# Sentinel to break out of an infinite re-exec loop if `sg docker -c …`
|
|
13
|
-
# somehow lands back here without socket access.
|
|
14
12
|
_REEXEC_ENV_FLAG = "SIMA_CLI_DOCKER_GROUP_REEXEC"
|
|
15
13
|
|
|
16
14
|
|
|
@@ -74,57 +72,92 @@ def _user_in_docker_group(user: str) -> bool:
|
|
|
74
72
|
return user in members
|
|
75
73
|
|
|
76
74
|
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
75
|
+
def _active_user_in_docker_group() -> bool:
|
|
76
|
+
"""Check whether this running process has the docker group active."""
|
|
77
|
+
try:
|
|
78
|
+
import grp
|
|
79
|
+
|
|
80
|
+
group_ids = set(os.getgroups())
|
|
81
|
+
group_ids.add(os.getgid())
|
|
82
|
+
group_ids.add(os.getegid())
|
|
83
|
+
active_group_names = {
|
|
84
|
+
grp.getgrgid(group_id).gr_name
|
|
85
|
+
for group_id in group_ids
|
|
86
|
+
}
|
|
87
|
+
except (ImportError, KeyError, OSError):
|
|
88
|
+
return False
|
|
89
|
+
return "docker" in active_group_names
|
|
90
|
+
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
back to
|
|
92
|
+
def _try_reexec_with_docker_group() -> None:
|
|
93
|
+
"""Best-effort re-run under `sg docker` when it is known to work.
|
|
94
|
+
|
|
95
|
+
Some systems allow `sg docker -c ...` to activate a newly-added docker
|
|
96
|
+
group immediately, while others reject it or prompt for group auth. Probe
|
|
97
|
+
first so failures can fall back to deterministic user instructions.
|
|
86
98
|
"""
|
|
87
99
|
if os.environ.get(_REEXEC_ENV_FLAG) == "1":
|
|
88
|
-
return
|
|
100
|
+
return
|
|
89
101
|
if not shutil.which("sg"):
|
|
90
|
-
return
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
probe_env = os.environ.copy()
|
|
105
|
+
probe_env[_REEXEC_ENV_FLAG] = "1"
|
|
106
|
+
try:
|
|
107
|
+
probe = subprocess.run(
|
|
108
|
+
["sg", "docker", "-c", "docker info >/dev/null 2>&1"],
|
|
109
|
+
stdin=subprocess.DEVNULL,
|
|
110
|
+
stdout=subprocess.DEVNULL,
|
|
111
|
+
stderr=subprocess.DEVNULL,
|
|
112
|
+
env=probe_env,
|
|
113
|
+
timeout=10,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
except (OSError, subprocess.SubprocessError):
|
|
117
|
+
return
|
|
118
|
+
if probe.returncode != 0:
|
|
119
|
+
return
|
|
91
120
|
|
|
92
|
-
new_env = os.environ.copy()
|
|
93
|
-
new_env[_REEXEC_ENV_FLAG] = "1"
|
|
94
121
|
cmdline = shlex.join(sys.argv)
|
|
95
|
-
print("🔁 Activating 'docker' group for this command
|
|
122
|
+
print("🔁 Activating 'docker' group for this command...")
|
|
96
123
|
try:
|
|
97
|
-
os.execvpe("sg", ["sg", "docker", "-c", cmdline],
|
|
98
|
-
except OSError
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
os.execvpe("sg", ["sg", "docker", "-c", cmdline], probe_env)
|
|
125
|
+
except OSError:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _print_docker_group_refresh_instructions() -> None:
|
|
130
|
+
command = shlex.join(sys.argv)
|
|
131
|
+
print("ℹ️ Docker group membership is configured, but this shell session cannot use it yet.")
|
|
132
|
+
print(" Start a shell with the updated group membership, then re-run this command:")
|
|
133
|
+
print(" newgrp docker")
|
|
134
|
+
print(f" {command}")
|
|
135
|
+
print(" If that still does not work, log out and back in before retrying.")
|
|
101
136
|
|
|
102
137
|
|
|
103
138
|
def _handle_docker_socket_permission_denied() -> bool:
|
|
104
139
|
"""Linux: daemon is up but the current user can't access the socket.
|
|
105
140
|
|
|
106
|
-
Offers to add the user to the 'docker' group via sudo
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
re-exec path is unavailable or has already been retried.
|
|
141
|
+
Offers to add the user to the 'docker' group via sudo when needed, then
|
|
142
|
+
exits with clear instructions because newly-added group membership is not
|
|
143
|
+
available to the current shell/process on many Linux systems.
|
|
110
144
|
"""
|
|
111
145
|
user = _current_user()
|
|
112
146
|
print("⚠️ Docker daemon is running, but this user lacks permission to access it.")
|
|
113
147
|
print(f" (Cannot access /var/run/docker.sock as '{user}'.)")
|
|
114
148
|
|
|
115
|
-
already_retried = os.environ.get(_REEXEC_ENV_FLAG) == "1"
|
|
116
|
-
|
|
117
149
|
if _user_in_docker_group(user):
|
|
118
|
-
if
|
|
119
|
-
print(f"ℹ️ User '{user}'
|
|
120
|
-
print("
|
|
121
|
-
print("
|
|
150
|
+
if _active_user_in_docker_group():
|
|
151
|
+
print(f"ℹ️ User '{user}' already has the 'docker' group active in this session.")
|
|
152
|
+
print(" The socket is still not accessible, so this is likely a Docker daemon")
|
|
153
|
+
print(" or /var/run/docker.sock ownership issue rather than group activation.")
|
|
154
|
+
print(" Check: ls -l /var/run/docker.sock")
|
|
155
|
+
print(" Check: sudo systemctl status docker")
|
|
122
156
|
sys.exit(1)
|
|
123
|
-
print(f"ℹ️ User '{user}' is already a member of the 'docker' group, but
|
|
157
|
+
print(f"ℹ️ User '{user}' is already a member of the 'docker' group, but this")
|
|
124
158
|
print(" shell session has not picked up that membership yet.")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
print(" 👉 Run 'newgrp docker' in this shell (or log out and back in), then re-run this command.")
|
|
159
|
+
_try_reexec_with_docker_group()
|
|
160
|
+
_print_docker_group_refresh_instructions()
|
|
128
161
|
sys.exit(1)
|
|
129
162
|
|
|
130
163
|
print(f"ℹ️ User '{user}' is not a member of the 'docker' group.")
|
|
@@ -143,12 +176,8 @@ def _handle_docker_socket_permission_denied() -> bool:
|
|
|
143
176
|
sys.exit(1)
|
|
144
177
|
|
|
145
178
|
print(f"✅ Added '{user}' to the 'docker' group.")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# OSError) and the original install/setup operation did NOT complete.
|
|
149
|
-
# Exit non-zero so parent scripts / CI do not treat this as success.
|
|
150
|
-
print("ℹ️ Group membership only takes effect in NEW shell sessions.")
|
|
151
|
-
print(" 👉 Run 'newgrp docker' in this shell (or log out and back in), then re-run this command.")
|
|
179
|
+
_try_reexec_with_docker_group()
|
|
180
|
+
_print_docker_group_refresh_instructions()
|
|
152
181
|
sys.exit(1)
|
|
153
182
|
|
|
154
183
|
|
|
@@ -54,6 +54,7 @@ class InstallMetadataResult:
|
|
|
54
54
|
environment: str
|
|
55
55
|
base_url: str
|
|
56
56
|
repository: str
|
|
57
|
+
package_path: str
|
|
57
58
|
ref: str
|
|
58
59
|
ref_key: str
|
|
59
60
|
requested_spec: str
|
|
@@ -190,22 +191,36 @@ def read_latest_tag(client: ArtifactClient, base_url: str, repository: str, key:
|
|
|
190
191
|
return latest_tag
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
def
|
|
194
|
+
def _parse_repository_and_package_path(raw: str) -> Tuple[str, str]:
|
|
195
|
+
value = raw.strip().strip("/")
|
|
196
|
+
if not value:
|
|
197
|
+
raise VulcanArtifactError("Install target repository is empty.")
|
|
198
|
+
|
|
199
|
+
parts = [part.strip() for part in value.split("/")]
|
|
200
|
+
if any(not part for part in parts):
|
|
201
|
+
raise VulcanArtifactError("Install target must not contain empty path segments.")
|
|
202
|
+
if any(part == ".." for part in parts):
|
|
203
|
+
raise VulcanArtifactError("Install target package path must not contain '..'.")
|
|
204
|
+
|
|
205
|
+
repository = parts[0]
|
|
206
|
+
package_path = "/".join(parts[1:])
|
|
207
|
+
return repository, package_path
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_install_target(target: str) -> Tuple[str, str, str, str]:
|
|
194
211
|
value = target.strip()
|
|
195
212
|
if not value:
|
|
196
213
|
raise VulcanArtifactError("Install target is empty.")
|
|
197
214
|
|
|
198
|
-
|
|
199
|
-
repository =
|
|
200
|
-
if not repository:
|
|
201
|
-
raise VulcanArtifactError("Install target repository is empty.")
|
|
215
|
+
repository_spec, separator, ref_spec = value.partition("@")
|
|
216
|
+
repository, package_path = _parse_repository_and_package_path(repository_spec)
|
|
202
217
|
|
|
203
218
|
if not separator or not ref_spec.strip():
|
|
204
|
-
return repository, "main", "latest"
|
|
219
|
+
return repository, package_path, "main", "latest"
|
|
205
220
|
|
|
206
221
|
ref_spec = ref_spec.strip().strip("/")
|
|
207
222
|
if not ref_spec:
|
|
208
|
-
return repository, "main", "latest"
|
|
223
|
+
return repository, package_path, "main", "latest"
|
|
209
224
|
|
|
210
225
|
if ":" in ref_spec:
|
|
211
226
|
ref, spec = ref_spec.rsplit(":", 1)
|
|
@@ -215,12 +230,12 @@ def parse_install_target(target: str) -> Tuple[str, str, str]:
|
|
|
215
230
|
raise VulcanArtifactError(
|
|
216
231
|
"Install target must use repo@branch:spec when specifying both branch and spec."
|
|
217
232
|
)
|
|
218
|
-
return repository, ref, spec
|
|
233
|
+
return repository, package_path, ref, spec
|
|
219
234
|
|
|
220
235
|
if ref_spec == "latest" or _looks_like_commit_spec(ref_spec):
|
|
221
|
-
return repository, "main", ref_spec
|
|
236
|
+
return repository, package_path, "main", ref_spec
|
|
222
237
|
|
|
223
|
-
return repository, ref_spec, "latest"
|
|
238
|
+
return repository, package_path, ref_spec, "latest"
|
|
224
239
|
|
|
225
240
|
|
|
226
241
|
def _looks_like_commit_spec(value: str) -> bool:
|
|
@@ -250,17 +265,25 @@ def resolve_install_metadata_url(
|
|
|
250
265
|
) -> InstallMetadataResult:
|
|
251
266
|
client = client or ArtifactClient()
|
|
252
267
|
resolved_base_url = normalize_base_url(base_url or ENV_BASE_URLS[environment])
|
|
253
|
-
repository, ref_name, requested_spec = parse_install_target(target)
|
|
268
|
+
repository, package_path, ref_name, requested_spec = parse_install_target(target)
|
|
254
269
|
key = ref_key(ref_name)
|
|
255
270
|
if requested_spec == "latest":
|
|
256
271
|
resolved_spec = read_latest_tag(client, resolved_base_url, repository, key)
|
|
257
272
|
else:
|
|
258
273
|
resolved_spec = requested_spec
|
|
259
|
-
metadata_url = join_url(
|
|
274
|
+
metadata_url = join_url(
|
|
275
|
+
resolved_base_url,
|
|
276
|
+
repository,
|
|
277
|
+
key,
|
|
278
|
+
resolved_spec,
|
|
279
|
+
package_path,
|
|
280
|
+
metadata_filename(package_type),
|
|
281
|
+
)
|
|
260
282
|
return InstallMetadataResult(
|
|
261
283
|
environment=environment,
|
|
262
284
|
base_url=resolved_base_url,
|
|
263
285
|
repository=repository,
|
|
286
|
+
package_path=package_path,
|
|
264
287
|
ref=ref_name,
|
|
265
288
|
ref_key=key,
|
|
266
289
|
requested_spec=requested_spec,
|