konduktor-nightly 0.1.0.dev20250903104451__tar.gz → 0.1.0.dev20250905104548__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.
Potentially problematic release.
This version of konduktor-nightly might be problematic. Click here for more details.
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/PKG-INFO +1 -1
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/__init__.py +2 -2
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/jobset_utils.py +2 -2
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/cli.py +3 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/apoxy-setup.yaml +1 -1
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/resource.py +9 -1
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/task.py +6 -1
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/templates/pod.yaml.j2 +2 -0
- konduktor_nightly-0.1.0.dev20250905104548/konduktor/utils/validator.py +449 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/pyproject.toml +1 -1
- konduktor_nightly-0.1.0.dev20250903104451/konduktor/utils/validator.py +0 -126
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/LICENSE +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/README.md +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/adaptors/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/adaptors/aws.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/adaptors/common.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/adaptors/gcp.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/authentication.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/backend.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/deployment.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/deployment_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/jobset.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/backends/pod_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/check.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/config.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/controller/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/controller/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/controller/launch.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/controller/node.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/controller/parse.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/README.md +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/backend/main.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/backend/sockets.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/.eslintrc.json +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/.gitignore +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/api/jobs/route.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/api/namespaces/route.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/Grafana.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/JobsData.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/LogsData.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/NavMenu.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/NavTabs.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/NavTabs2.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/SelectBtn.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/lib/utils.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/ui/chip-select.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/ui/input.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/ui/navigation-menu.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/components/ui/select.jsx +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/favicon.ico +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/globals.css +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/jobs/page.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/layout.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/logs/page.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/app/page.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/jsconfig.json +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/next.config.mjs +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/package-lock.json +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/package.json +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/postcss.config.mjs +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/server.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/dashboard/frontend/tailwind.config.js +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/aws/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/aws/s3.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/data_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/gcp/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/gcp/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/gcp/gcs.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/gcp/utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/registry.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/storage.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/data/storage_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/execution.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/kube_client.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/logging.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/apoxy-setup2.yaml +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/controller_deployment.yaml +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/dashboard_deployment.yaml +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/dmesg_daemonset.yaml +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/manifests/pod_cleanup_controller.yaml +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/serving.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/templates/apoxy-deployment.yaml.j2 +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/templates/deployment.yaml.j2 +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/templates/jobset.yaml.j2 +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/usage/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/usage/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/__init__.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/accelerator_registry.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/annotations.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/base64_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/common_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/constants.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/env_options.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/exceptions.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/kubernetes_enums.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/kubernetes_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/log_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/loki_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/rich_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/schemas.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/subprocess_utils.py +0 -0
- {konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/konduktor/utils/ux_utils.py +0 -0
|
@@ -11,7 +11,7 @@ from konduktor.task import Task
|
|
|
11
11
|
__all__ = ['launch', 'Resources', 'Task', 'Serving']
|
|
12
12
|
|
|
13
13
|
# Replaced with the current commit when building the wheels.
|
|
14
|
-
_KONDUKTOR_COMMIT_SHA = '
|
|
14
|
+
_KONDUKTOR_COMMIT_SHA = 'fc389d2cefc1394a2bb1f1c5e03cb900f3ce8406'
|
|
15
15
|
os.makedirs(os.path.expanduser('~/.konduktor'), exist_ok=True)
|
|
16
16
|
|
|
17
17
|
|
|
@@ -45,5 +45,5 @@ def _get_git_commit():
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
__commit__ = _get_git_commit()
|
|
48
|
-
__version__ = '1.0.0.dev0.1.0.
|
|
48
|
+
__version__ = '1.0.0.dev0.1.0.dev20250905104548'
|
|
49
49
|
__root_dir__ = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -578,8 +578,8 @@ def show_status_table(
|
|
|
578
578
|
num_accelerators = job['metadata']['labels'].get(
|
|
579
579
|
JOBSET_NUM_ACCELERATORS_LABEL, None
|
|
580
580
|
)
|
|
581
|
-
if accelerator:
|
|
582
|
-
if num_accelerators:
|
|
581
|
+
if accelerator and accelerator != 'None':
|
|
582
|
+
if num_accelerators and num_accelerators != '0':
|
|
583
583
|
accelerator_with_count = f'{accelerator}:{num_accelerators}'
|
|
584
584
|
else:
|
|
585
585
|
accelerator_with_count = accelerator
|
|
@@ -58,6 +58,7 @@ from konduktor.utils import (
|
|
|
58
58
|
kubernetes_utils,
|
|
59
59
|
log_utils,
|
|
60
60
|
ux_utils,
|
|
61
|
+
validator,
|
|
61
62
|
)
|
|
62
63
|
|
|
63
64
|
_CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
@@ -419,6 +420,8 @@ def _parse_override_params(
|
|
|
419
420
|
if image_id.lower() == 'none':
|
|
420
421
|
override_params['image_id'] = None
|
|
421
422
|
else:
|
|
423
|
+
# Validate Docker image before adding to override params
|
|
424
|
+
validator.validate_and_warn_image(image_id, 'task')
|
|
422
425
|
override_params['image_id'] = image_id
|
|
423
426
|
if disk_size is not None:
|
|
424
427
|
override_params['disk_size'] = disk_size
|
|
@@ -16,7 +16,13 @@ import functools
|
|
|
16
16
|
from typing import Any, Dict, List, Optional, Union
|
|
17
17
|
|
|
18
18
|
from konduktor import logging
|
|
19
|
-
from konduktor.utils import
|
|
19
|
+
from konduktor.utils import (
|
|
20
|
+
accelerator_registry,
|
|
21
|
+
common_utils,
|
|
22
|
+
schemas,
|
|
23
|
+
ux_utils,
|
|
24
|
+
validator,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
logger = logging.get_logger(__name__)
|
|
22
28
|
|
|
@@ -117,6 +123,8 @@ class Resources:
|
|
|
117
123
|
self._image_id = image_id
|
|
118
124
|
if isinstance(image_id, str):
|
|
119
125
|
self._image_id = image_id.strip()
|
|
126
|
+
# Validate Docker image format and existence
|
|
127
|
+
validator.validate_and_warn_image(self._image_id, 'task')
|
|
120
128
|
|
|
121
129
|
self._labels = labels
|
|
122
130
|
self._cluster_config_overrides = _cluster_config_overrides
|
|
@@ -29,7 +29,7 @@ import konduktor
|
|
|
29
29
|
from konduktor import constants, logging
|
|
30
30
|
from konduktor.data import data_utils
|
|
31
31
|
from konduktor.data import storage as storage_lib
|
|
32
|
-
from konduktor.utils import common_utils, exceptions, schemas, ux_utils
|
|
32
|
+
from konduktor.utils import common_utils, exceptions, schemas, ux_utils, validator
|
|
33
33
|
|
|
34
34
|
logger = logging.get_logger(__name__)
|
|
35
35
|
|
|
@@ -387,6 +387,11 @@ class Task:
|
|
|
387
387
|
'experimental.config_overrides'
|
|
388
388
|
)
|
|
389
389
|
resources_config['_cluster_config_overrides'] = cluster_config_override
|
|
390
|
+
|
|
391
|
+
# Validate Docker image if specified in resources
|
|
392
|
+
if 'image_id' in resources_config and resources_config['image_id']:
|
|
393
|
+
validator.validate_and_warn_image(resources_config['image_id'], 'task')
|
|
394
|
+
|
|
390
395
|
task.set_resources(konduktor.Resources.from_yaml_config(resources_config))
|
|
391
396
|
|
|
392
397
|
# Parse serving field.
|
|
@@ -77,6 +77,8 @@ kubernetes:
|
|
|
77
77
|
# flush logs immediately to stdout for more reactive log streaming
|
|
78
78
|
- name: PYTHONUNBUFFERED
|
|
79
79
|
value: "0"
|
|
80
|
+
- name: KONDUKTOR_JOB_NAME
|
|
81
|
+
value: "{{ job_name }}"
|
|
80
82
|
- name: NODE_HOST_IPS
|
|
81
83
|
value: "{{ node_hostnames }}"
|
|
82
84
|
- name: MASTER_ADDR
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""This module contains a custom validator for the JSON Schema specification.
|
|
2
|
+
|
|
3
|
+
The main motivation behind extending the existing JSON Schema validator is to
|
|
4
|
+
allow for case-insensitive enum matching since this is currently not supported
|
|
5
|
+
by the JSON Schema specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
|
|
17
|
+
import jsonschema
|
|
18
|
+
import requests
|
|
19
|
+
from colorama import Fore, Style
|
|
20
|
+
from filelock import FileLock
|
|
21
|
+
|
|
22
|
+
from konduktor import logging
|
|
23
|
+
|
|
24
|
+
SCHEMA_VERSION = 'v1.32.0-standalone-strict'
|
|
25
|
+
SCHEMA_CACHE_PATH = Path.home() / '.konduktor/schemas'
|
|
26
|
+
SCHEMA_LOCK_PATH = SCHEMA_CACHE_PATH / '.lock'
|
|
27
|
+
CACHE_MAX_AGE_SECONDS = 86400 # 24 hours
|
|
28
|
+
|
|
29
|
+
# Schema URLs for different Kubernetes resources
|
|
30
|
+
SCHEMA_URLS = {
|
|
31
|
+
'podspec': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/podspec.json',
|
|
32
|
+
'deployment': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/deployment.json',
|
|
33
|
+
'service': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/service.json',
|
|
34
|
+
'horizontalpodautoscaler': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/horizontalpodautoscaler-autoscaling-v2.json',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger = logging.get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def case_insensitive_enum(validator, enums, instance, schema):
|
|
41
|
+
del validator, schema # Unused.
|
|
42
|
+
if instance.lower() not in [enum.lower() for enum in enums]:
|
|
43
|
+
yield jsonschema.ValidationError(f'{instance!r} is not one of {enums!r}')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
SchemaValidator = jsonschema.validators.extend(
|
|
47
|
+
jsonschema.Draft7Validator,
|
|
48
|
+
validators={'case_insensitive_enum': case_insensitive_enum},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_cached_schema(schema_type: str) -> dict:
|
|
53
|
+
"""Get cached schema for a specific Kubernetes resource type."""
|
|
54
|
+
schema_url = SCHEMA_URLS.get(schema_type)
|
|
55
|
+
if not schema_url:
|
|
56
|
+
raise ValueError(f'Unknown schema type: {schema_type}')
|
|
57
|
+
|
|
58
|
+
schema_file = SCHEMA_CACHE_PATH / f'{schema_type}.json'
|
|
59
|
+
lock = FileLock(str(SCHEMA_LOCK_PATH))
|
|
60
|
+
|
|
61
|
+
with lock:
|
|
62
|
+
# Check if schema file exists and is fresh
|
|
63
|
+
if schema_file.exists():
|
|
64
|
+
age = time.time() - schema_file.stat().st_mtime
|
|
65
|
+
# if fresh
|
|
66
|
+
if age < CACHE_MAX_AGE_SECONDS:
|
|
67
|
+
with open(schema_file, 'r') as f:
|
|
68
|
+
return json.load(f)
|
|
69
|
+
|
|
70
|
+
# Download schema
|
|
71
|
+
resp = requests.get(schema_url)
|
|
72
|
+
resp.raise_for_status()
|
|
73
|
+
|
|
74
|
+
SCHEMA_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
with open(schema_file, 'w') as f:
|
|
76
|
+
f.write(resp.text)
|
|
77
|
+
|
|
78
|
+
return resp.json()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _validate_k8s_spec(spec: dict, schema_type: str, resource_name: str) -> None:
|
|
82
|
+
"""Generic validation function for Kubernetes specs."""
|
|
83
|
+
schema = get_cached_schema(schema_type)
|
|
84
|
+
|
|
85
|
+
validator = jsonschema.Draft7Validator(schema)
|
|
86
|
+
errors = sorted(validator.iter_errors(spec), key=lambda e: e.path)
|
|
87
|
+
|
|
88
|
+
if not errors:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
formatted = [
|
|
92
|
+
f'- {error.message}'
|
|
93
|
+
+ (f" at path: {' → '.join(str(p) for p in error.path)}" if error.path else '')
|
|
94
|
+
for error in errors
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Clean log
|
|
98
|
+
logger.debug('Invalid k8s %s spec/config:\n%s', resource_name, '\n'.join(formatted))
|
|
99
|
+
|
|
100
|
+
# Color only in CLI
|
|
101
|
+
formatted_colored = [
|
|
102
|
+
f'{Fore.RED}- {error.message}'
|
|
103
|
+
+ (f" at path: {' → '.join(str(p) for p in error.path)}" if error.path else '')
|
|
104
|
+
+ Style.RESET_ALL
|
|
105
|
+
for error in errors
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f'\n{Fore.RED}Invalid k8s {resource_name} spec/config: {Style.RESET_ALL}\n'
|
|
110
|
+
+ '\n'.join(formatted_colored)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_pod_spec(pod_spec: dict) -> None:
|
|
115
|
+
"""Validate a Kubernetes pod spec."""
|
|
116
|
+
_validate_k8s_spec(pod_spec, 'podspec', 'pod')
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def validate_deployment_spec(deployment_spec: dict) -> None:
|
|
120
|
+
"""Validate a Kubernetes deployment spec."""
|
|
121
|
+
_validate_k8s_spec(deployment_spec, 'deployment', 'deployment')
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def validate_service_spec(service_spec: dict) -> None:
|
|
125
|
+
"""Validate a Kubernetes service spec."""
|
|
126
|
+
_validate_k8s_spec(service_spec, 'service', 'service')
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_horizontalpodautoscaler_spec(hpa_spec: dict) -> None:
|
|
130
|
+
"""Validate a Kubernetes HorizontalPodAutoscaler spec."""
|
|
131
|
+
_validate_k8s_spec(hpa_spec, 'horizontalpodautoscaler', 'horizontalpodautoscaler')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_docker_image(image_id: str) -> Tuple[str, str]:
|
|
135
|
+
"""Validate if a Docker image exists and is accessible.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
image_id: The Docker image ID to validate
|
|
139
|
+
(e.g., 'ubuntu:latest', 'gcr.io/project/image:tag')
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (status, message) where status is:
|
|
143
|
+
- 'valid': Image definitely exists
|
|
144
|
+
- 'warning': Couldn't validate, but might be valid
|
|
145
|
+
- 'invalid': Image definitely doesn't exist
|
|
146
|
+
"""
|
|
147
|
+
if not image_id or not isinstance(image_id, str):
|
|
148
|
+
return 'invalid', 'Image ID must be a non-empty string'
|
|
149
|
+
|
|
150
|
+
# Basic format validation
|
|
151
|
+
if not _is_valid_docker_image_format(image_id):
|
|
152
|
+
return 'invalid', f'Invalid Docker image format: {image_id}'
|
|
153
|
+
|
|
154
|
+
# Try registry API validation first (works without Docker daemon)
|
|
155
|
+
registry_result = _validate_image_in_registry(image_id)
|
|
156
|
+
if registry_result[0] in ['valid', 'invalid']:
|
|
157
|
+
return registry_result
|
|
158
|
+
|
|
159
|
+
# If registry validation couldn't determine, try local Docker as fallback
|
|
160
|
+
if _can_pull_image_locally(image_id):
|
|
161
|
+
return 'valid', f"Docker image '{image_id}' validated locally"
|
|
162
|
+
|
|
163
|
+
# Return the registry result (warning)
|
|
164
|
+
return registry_result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_valid_docker_image_format(image_id: str) -> bool:
|
|
168
|
+
"""Check if the image ID follows valid Docker image naming conventions."""
|
|
169
|
+
# Basic regex for Docker image names
|
|
170
|
+
# Supports: name:tag, registry/name:tag, registry/namespace/name:tag
|
|
171
|
+
pattern = (
|
|
172
|
+
r'^[a-zA-Z0-9][a-zA-Z0-9._-]*'
|
|
173
|
+
r'(?:\/[a-zA-Z0-9][a-zA-Z0-9._-]*)*'
|
|
174
|
+
r'(?::[a-zA-Z0-9._-]+)?$'
|
|
175
|
+
)
|
|
176
|
+
return bool(re.match(pattern, image_id))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _can_pull_image_locally(image_id: str) -> bool:
|
|
180
|
+
"""Try to inspect the image manifest locally to check if it exists."""
|
|
181
|
+
try:
|
|
182
|
+
# Use docker manifest inspect instead of pull for faster validation
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
['docker', 'manifest', 'inspect', image_id],
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=30, # 30 second timeout
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Debug logging
|
|
191
|
+
logger.debug(
|
|
192
|
+
f'Local Docker manifest inspect for {image_id}: '
|
|
193
|
+
f'returncode={result.returncode}, '
|
|
194
|
+
f"stdout='{result.stdout}', "
|
|
195
|
+
f"stderr='{result.stderr}'"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return result.returncode == 0
|
|
199
|
+
except (
|
|
200
|
+
subprocess.TimeoutExpired,
|
|
201
|
+
FileNotFoundError,
|
|
202
|
+
subprocess.SubprocessError,
|
|
203
|
+
) as e:
|
|
204
|
+
# Docker not available or timeout
|
|
205
|
+
logger.debug(f'Local Docker manifest inspect failed for {image_id}: {e}')
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _validate_image_in_registry(image_id: str) -> Tuple[str, str]:
|
|
210
|
+
"""Validate image exists in registry using API calls."""
|
|
211
|
+
try:
|
|
212
|
+
registry, repo, tag = _parse_image_components(image_id)
|
|
213
|
+
|
|
214
|
+
if registry == 'docker.io':
|
|
215
|
+
return _validate_dockerhub_image(repo, tag)
|
|
216
|
+
elif registry.endswith('gcr.io'):
|
|
217
|
+
return _validate_gcr_image(registry, repo, tag)
|
|
218
|
+
elif registry.endswith('ecr.') and '.amazonaws.com' in registry:
|
|
219
|
+
return _validate_ecr_image(registry, repo, tag)
|
|
220
|
+
elif registry == 'nvcr.io':
|
|
221
|
+
return _validate_nvcr_image(registry, repo, tag)
|
|
222
|
+
elif registry == 'ghcr.io':
|
|
223
|
+
return _validate_ghcr_image(registry, repo, tag)
|
|
224
|
+
elif registry == 'quay.io':
|
|
225
|
+
return _validate_quay_image(registry, repo, tag)
|
|
226
|
+
else:
|
|
227
|
+
# For other registries, we can't easily validate without credentials
|
|
228
|
+
# Return warning that we couldn't verify
|
|
229
|
+
return (
|
|
230
|
+
'warning',
|
|
231
|
+
f"Could not validate '{image_id}' in registry {registry} "
|
|
232
|
+
f'(not supported)',
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.debug(f'Error validating image {image_id}: {e}')
|
|
237
|
+
return 'warning', f"Could not validate '{image_id}' due to validation error"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _parse_image_components(image_id: str) -> Tuple[str, str, str]:
|
|
241
|
+
"""Parse image ID into registry, repository, and tag components."""
|
|
242
|
+
# Default to Docker Hub
|
|
243
|
+
if '/' not in image_id or '.' not in image_id.split('/')[0]:
|
|
244
|
+
registry = 'docker.io'
|
|
245
|
+
# For Docker Hub official images (single word), add 'library/' prefix
|
|
246
|
+
if ':' in image_id:
|
|
247
|
+
repo, tag = image_id.rsplit(':', 1)
|
|
248
|
+
else:
|
|
249
|
+
repo = image_id
|
|
250
|
+
tag = 'latest'
|
|
251
|
+
# Only add 'library/' prefix for single-word official images
|
|
252
|
+
if '/' not in repo:
|
|
253
|
+
repo = f'library/{repo}'
|
|
254
|
+
else:
|
|
255
|
+
parts = image_id.split('/')
|
|
256
|
+
if '.' in parts[0] or parts[0] in ['localhost']:
|
|
257
|
+
registry = parts[0]
|
|
258
|
+
repo = '/'.join(parts[1:])
|
|
259
|
+
else:
|
|
260
|
+
registry = 'docker.io'
|
|
261
|
+
repo = image_id
|
|
262
|
+
|
|
263
|
+
# Split repository and tag
|
|
264
|
+
if ':' in repo:
|
|
265
|
+
repo, tag = repo.rsplit(':', 1)
|
|
266
|
+
else:
|
|
267
|
+
tag = 'latest'
|
|
268
|
+
|
|
269
|
+
return registry, repo, tag
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _validate_dockerhub_image(repo: str, tag: str) -> Tuple[str, str]:
|
|
273
|
+
"""Validate image exists in Docker Hub using the official API."""
|
|
274
|
+
try:
|
|
275
|
+
# Use Docker Hub's official API v2 endpoint
|
|
276
|
+
# This endpoint checks if a specific tag exists for a repository
|
|
277
|
+
url = f'https://registry.hub.docker.com/v2/repositories/{repo}/tags/{tag}'
|
|
278
|
+
|
|
279
|
+
# Add User-Agent to avoid being blocked
|
|
280
|
+
headers = {'User-Agent': 'Konduktor-Docker-Validator/1.0'}
|
|
281
|
+
|
|
282
|
+
response = requests.get(url, headers=headers, timeout=10)
|
|
283
|
+
|
|
284
|
+
if response.status_code == 200:
|
|
285
|
+
return 'valid', f"Docker image '{repo}:{tag}' validated via Docker Hub"
|
|
286
|
+
else:
|
|
287
|
+
# API error, can't determine
|
|
288
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in Docker Hub")
|
|
289
|
+
|
|
290
|
+
except requests.RequestException:
|
|
291
|
+
# Network error, can't determine
|
|
292
|
+
return (
|
|
293
|
+
'warning',
|
|
294
|
+
f"Could not validate '{repo}:{tag}' in Docker Hub " f'(network error)',
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _validate_gcr_image(registry: str, repo: str, tag: str) -> Tuple[str, str]:
|
|
299
|
+
"""Validate image exists in Google Container Registry."""
|
|
300
|
+
try:
|
|
301
|
+
# GCR manifest endpoint
|
|
302
|
+
url = f'https://{registry}/v2/{repo}/manifests/{tag}'
|
|
303
|
+
response = requests.get(url, timeout=10)
|
|
304
|
+
|
|
305
|
+
if response.status_code == 200:
|
|
306
|
+
return 'valid', f"Docker image '{repo}:{tag}' validated via {registry}"
|
|
307
|
+
else:
|
|
308
|
+
# API error, can't determine
|
|
309
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in {registry} ")
|
|
310
|
+
|
|
311
|
+
except requests.RequestException:
|
|
312
|
+
# Network error, can't determine
|
|
313
|
+
return (
|
|
314
|
+
'warning',
|
|
315
|
+
f"Could not validate '{repo}:{tag}' in {registry} " f'(network error)',
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _validate_ecr_image(registry: str, repo: str, tag: str) -> Tuple[str, str]:
|
|
320
|
+
"""Validate image exists in Amazon ECR."""
|
|
321
|
+
# ECR requires AWS credentials and is complex to validate
|
|
322
|
+
# For now, return warning that we couldn't verify
|
|
323
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in {registry}")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _validate_nvcr_image(registry: str, repo: str, tag: str) -> Tuple[str, str]:
|
|
327
|
+
"""Validate image exists in NVIDIA Container Registry."""
|
|
328
|
+
# NVCR requires NVIDIA credentials and is complex to validate
|
|
329
|
+
# For now, return warning that we couldn't verify
|
|
330
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in {registry}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _validate_ghcr_image(registry: str, repo: str, tag: str) -> Tuple[str, str]:
|
|
334
|
+
"""Validate image exists in GitHub Container Registry."""
|
|
335
|
+
try:
|
|
336
|
+
# Check if GITHUB_TOKEN is available
|
|
337
|
+
github_token = os.environ.get('GITHUB_TOKEN')
|
|
338
|
+
|
|
339
|
+
# If not in environment, try to get from konduktor secrets
|
|
340
|
+
if not github_token:
|
|
341
|
+
try:
|
|
342
|
+
# these imports are inside the try block to avoid circular import error
|
|
343
|
+
from konduktor.backends import constants as backend_constants
|
|
344
|
+
from konduktor.utils import common_utils, kubernetes_utils
|
|
345
|
+
|
|
346
|
+
context = kubernetes_utils.get_current_kube_config_context_name()
|
|
347
|
+
namespace = kubernetes_utils.get_kube_config_context_namespace(context)
|
|
348
|
+
user_hash = common_utils.get_user_hash()
|
|
349
|
+
label_selector = f'{backend_constants.SECRET_OWNER_LABEL}={user_hash}'
|
|
350
|
+
user_secrets = kubernetes_utils.list_secrets(
|
|
351
|
+
namespace, context, label_filter=label_selector
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
for secret in user_secrets:
|
|
355
|
+
kind = kubernetes_utils.get_secret_kind(secret)
|
|
356
|
+
if kind == 'env' and secret.data and 'GITHUB_TOKEN' in secret.data:
|
|
357
|
+
# Decode the base64 encoded token
|
|
358
|
+
github_token = base64.b64decode(
|
|
359
|
+
secret.data['GITHUB_TOKEN']
|
|
360
|
+
).decode()
|
|
361
|
+
logger.debug('GITHUB_TOKEN found in konduktor secret')
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.debug(f'Failed to check konduktor secrets: {e}')
|
|
366
|
+
|
|
367
|
+
if not github_token:
|
|
368
|
+
return (
|
|
369
|
+
'warning',
|
|
370
|
+
'GITHUB_TOKEN unset, cannot verify this image. '
|
|
371
|
+
'To enable validation, either:\n'
|
|
372
|
+
' 1. Set GITHUB_TOKEN locally: export GITHUB_TOKEN=<token>\n'
|
|
373
|
+
' 2. Create a secret: konduktor secret create --kind=env '
|
|
374
|
+
'--inline GITHUB_TOKEN=<token> <name>\n'
|
|
375
|
+
'See: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry',
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Base64 encode the token
|
|
379
|
+
ghcr_token = base64.b64encode(github_token.encode()).decode()
|
|
380
|
+
|
|
381
|
+
# GHCR manifest endpoint
|
|
382
|
+
url = f'https://{registry}/v2/{repo}/manifests/{tag}'
|
|
383
|
+
headers = {'Authorization': f'Bearer {ghcr_token}'}
|
|
384
|
+
response = requests.get(url, headers=headers, timeout=10)
|
|
385
|
+
|
|
386
|
+
if response.status_code == 200:
|
|
387
|
+
return 'valid', f"Docker image '{repo}:{tag}' validated via {registry}"
|
|
388
|
+
else:
|
|
389
|
+
# API error, can't determine
|
|
390
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in {registry}")
|
|
391
|
+
|
|
392
|
+
except requests.RequestException:
|
|
393
|
+
# Network error, can't determine
|
|
394
|
+
return (
|
|
395
|
+
'warning',
|
|
396
|
+
f"Could not validate '{repo}:{tag}' in {registry} " f'(network error)',
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _validate_quay_image(registry: str, repo: str, tag: str) -> Tuple[str, str]:
|
|
401
|
+
"""Validate image exists in Quay.io Container Registry."""
|
|
402
|
+
# Quay.io requires authentication and is complex to validate
|
|
403
|
+
# For now, return warning that we couldn't verify
|
|
404
|
+
return ('warning', f"Could not validate '{repo}:{tag}' in {registry}")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Track which images we've already warned about to avoid duplicate warnings
|
|
408
|
+
_warned_images = set()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def validate_and_warn_image(image_id: str, context: str = 'task') -> None:
|
|
412
|
+
"""Validate Docker image and show appropriate warnings.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
image_id: The Docker image ID to validate
|
|
416
|
+
context: Context for the validation (e.g., "task", "deployment")
|
|
417
|
+
|
|
418
|
+
"""
|
|
419
|
+
if not image_id:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
status, message = validate_docker_image(image_id)
|
|
423
|
+
|
|
424
|
+
if status == 'invalid':
|
|
425
|
+
# Invalid images should fail - they definitely don't exist
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f'{message}\n'
|
|
428
|
+
f'This Docker image does not exist and will cause the {context} to fail.\n'
|
|
429
|
+
f"Please check that the image '{image_id}' is correct and accessible.\n"
|
|
430
|
+
)
|
|
431
|
+
elif status == 'warning':
|
|
432
|
+
# Only warn once per image per session for warnings
|
|
433
|
+
if image_id not in _warned_images:
|
|
434
|
+
_warned_images.add(image_id)
|
|
435
|
+
|
|
436
|
+
logger.warning(
|
|
437
|
+
f'⚠️ Basic public image validation using Docker Daemon failed. ⚠️\n'
|
|
438
|
+
f'⚠️ {message} ⚠️\n'
|
|
439
|
+
f'⚠️ The {context} will be submitted anyway, but may be stuck '
|
|
440
|
+
f'PENDING forever. ⚠️\n'
|
|
441
|
+
f"⚠️ Check for 'ErrImagePull' or 'ImagePullBackOff' in "
|
|
442
|
+
f'kubectl get pods if issues occur. ⚠️'
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Add info about private registries
|
|
446
|
+
logger.info(
|
|
447
|
+
'⚠️ If pulling from a private registry, using ecr/nvcr, or not '
|
|
448
|
+
'logged into Docker, this is safe to ignore. ⚠️'
|
|
449
|
+
)
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""This module contains a custom validator for the JSON Schema specification.
|
|
2
|
-
|
|
3
|
-
The main motivation behind extending the existing JSON Schema validator is to
|
|
4
|
-
allow for case-insensitive enum matching since this is currently not supported
|
|
5
|
-
by the JSON Schema specification.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import time
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
import jsonschema
|
|
13
|
-
import requests
|
|
14
|
-
from colorama import Fore, Style
|
|
15
|
-
from filelock import FileLock
|
|
16
|
-
|
|
17
|
-
from konduktor import logging
|
|
18
|
-
|
|
19
|
-
SCHEMA_VERSION = 'v1.32.0-standalone-strict'
|
|
20
|
-
SCHEMA_CACHE_PATH = Path.home() / '.konduktor/schemas'
|
|
21
|
-
SCHEMA_LOCK_PATH = SCHEMA_CACHE_PATH / '.lock'
|
|
22
|
-
CACHE_MAX_AGE_SECONDS = 86400 # 24 hours
|
|
23
|
-
|
|
24
|
-
# Schema URLs for different Kubernetes resources
|
|
25
|
-
SCHEMA_URLS = {
|
|
26
|
-
'podspec': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/podspec.json',
|
|
27
|
-
'deployment': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/deployment.json',
|
|
28
|
-
'service': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/service.json',
|
|
29
|
-
'horizontalpodautoscaler': f'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{SCHEMA_VERSION}/horizontalpodautoscaler-autoscaling-v2.json',
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
logger = logging.get_logger(__name__)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def case_insensitive_enum(validator, enums, instance, schema):
|
|
36
|
-
del validator, schema # Unused.
|
|
37
|
-
if instance.lower() not in [enum.lower() for enum in enums]:
|
|
38
|
-
yield jsonschema.ValidationError(f'{instance!r} is not one of {enums!r}')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
SchemaValidator = jsonschema.validators.extend(
|
|
42
|
-
jsonschema.Draft7Validator,
|
|
43
|
-
validators={'case_insensitive_enum': case_insensitive_enum},
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_cached_schema(schema_type: str) -> dict:
|
|
48
|
-
"""Get cached schema for a specific Kubernetes resource type."""
|
|
49
|
-
schema_url = SCHEMA_URLS.get(schema_type)
|
|
50
|
-
if not schema_url:
|
|
51
|
-
raise ValueError(f'Unknown schema type: {schema_type}')
|
|
52
|
-
|
|
53
|
-
schema_file = SCHEMA_CACHE_PATH / f'{schema_type}.json'
|
|
54
|
-
lock = FileLock(str(SCHEMA_LOCK_PATH))
|
|
55
|
-
|
|
56
|
-
with lock:
|
|
57
|
-
# Check if schema file exists and is fresh
|
|
58
|
-
if schema_file.exists():
|
|
59
|
-
age = time.time() - schema_file.stat().st_mtime
|
|
60
|
-
# if fresh
|
|
61
|
-
if age < CACHE_MAX_AGE_SECONDS:
|
|
62
|
-
with open(schema_file, 'r') as f:
|
|
63
|
-
return json.load(f)
|
|
64
|
-
|
|
65
|
-
# Download schema
|
|
66
|
-
resp = requests.get(schema_url)
|
|
67
|
-
resp.raise_for_status()
|
|
68
|
-
|
|
69
|
-
SCHEMA_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
|
70
|
-
with open(schema_file, 'w') as f:
|
|
71
|
-
f.write(resp.text)
|
|
72
|
-
|
|
73
|
-
return resp.json()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _validate_k8s_spec(spec: dict, schema_type: str, resource_name: str) -> None:
|
|
77
|
-
"""Generic validation function for Kubernetes specs."""
|
|
78
|
-
schema = get_cached_schema(schema_type)
|
|
79
|
-
|
|
80
|
-
validator = jsonschema.Draft7Validator(schema)
|
|
81
|
-
errors = sorted(validator.iter_errors(spec), key=lambda e: e.path)
|
|
82
|
-
|
|
83
|
-
if not errors:
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
formatted = [
|
|
87
|
-
f'- {error.message}'
|
|
88
|
-
+ (f" at path: {' → '.join(str(p) for p in error.path)}" if error.path else '')
|
|
89
|
-
for error in errors
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
# Clean log
|
|
93
|
-
logger.debug('Invalid k8s %s spec/config:\n%s', resource_name, '\n'.join(formatted))
|
|
94
|
-
|
|
95
|
-
# Color only in CLI
|
|
96
|
-
formatted_colored = [
|
|
97
|
-
f'{Fore.RED}- {error.message}'
|
|
98
|
-
+ (f" at path: {' → '.join(str(p) for p in error.path)}" if error.path else '')
|
|
99
|
-
+ Style.RESET_ALL
|
|
100
|
-
for error in errors
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
raise ValueError(
|
|
104
|
-
f'\n{Fore.RED}Invalid k8s {resource_name} spec/config: {Style.RESET_ALL}\n'
|
|
105
|
-
+ '\n'.join(formatted_colored)
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def validate_pod_spec(pod_spec: dict) -> None:
|
|
110
|
-
"""Validate a Kubernetes pod spec."""
|
|
111
|
-
_validate_k8s_spec(pod_spec, 'podspec', 'pod')
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def validate_deployment_spec(deployment_spec: dict) -> None:
|
|
115
|
-
"""Validate a Kubernetes deployment spec."""
|
|
116
|
-
_validate_k8s_spec(deployment_spec, 'deployment', 'deployment')
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def validate_service_spec(service_spec: dict) -> None:
|
|
120
|
-
"""Validate a Kubernetes service spec."""
|
|
121
|
-
_validate_k8s_spec(service_spec, 'service', 'service')
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def validate_horizontalpodautoscaler_spec(hpa_spec: dict) -> None:
|
|
125
|
-
"""Validate a Kubernetes HorizontalPodAutoscaler spec."""
|
|
126
|
-
_validate_k8s_spec(hpa_spec, 'horizontalpodautoscaler', 'horizontalpodautoscaler')
|
{konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/LICENSE
RENAMED
|
File without changes
|
{konduktor_nightly-0.1.0.dev20250903104451 → konduktor_nightly-0.1.0.dev20250905104548}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|