hiverge-cli 0.1.0__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.
- hiverge_cli-0.1.0/LICENSE +19 -0
- hiverge_cli-0.1.0/PKG-INFO +101 -0
- hiverge_cli-0.1.0/README.md +79 -0
- hiverge_cli-0.1.0/pyproject.toml +48 -0
- hiverge_cli-0.1.0/setup.cfg +4 -0
- hiverge_cli-0.1.0/src/hive_cli/__init__.py +0 -0
- hiverge_cli-0.1.0/src/hive_cli/config.py +129 -0
- hiverge_cli-0.1.0/src/hive_cli/main.py +216 -0
- hiverge_cli-0.1.0/src/hive_cli/platform/__init__.py +0 -0
- hiverge_cli-0.1.0/src/hive_cli/platform/base.py +170 -0
- hiverge_cli-0.1.0/src/hive_cli/platform/k8s.py +232 -0
- hiverge_cli-0.1.0/src/hive_cli/platform/onprem.py +26 -0
- hiverge_cli-0.1.0/src/hive_cli/runtime/__init__.py +0 -0
- hiverge_cli-0.1.0/src/hive_cli/runtime/runtime.py +28 -0
- hiverge_cli-0.1.0/src/hive_cli/utils/__init__.py +0 -0
- hiverge_cli-0.1.0/src/hive_cli/utils/git.py +25 -0
- hiverge_cli-0.1.0/src/hive_cli/utils/image.py +44 -0
- hiverge_cli-0.1.0/src/hive_cli/utils/logger.py +32 -0
- hiverge_cli-0.1.0/src/hive_cli/utils/time.py +19 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/PKG-INFO +101 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/SOURCES.txt +24 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/dependency_links.txt +1 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/entry_points.txt +2 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/requires.txt +11 -0
- hiverge_cli-0.1.0/src/hiverge_cli.egg-info/top_level.txt +1 -0
- hiverge_cli-0.1.0/tests/test_main.py +2 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiverge-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Command Line Interface for Hive agent.
|
|
5
|
+
Author: Hiverge Team
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYAML>=5.1
|
|
13
|
+
Requires-Dist: pydantic>=1.8.2
|
|
14
|
+
Requires-Dist: gitpython>=3.1.24
|
|
15
|
+
Requires-Dist: python-dotenv>=0.19.2
|
|
16
|
+
Requires-Dist: rich>=12.5.1
|
|
17
|
+
Requires-Dist: kubernetes>=32.0.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Hive-CLI
|
|
24
|
+
|
|
25
|
+
Hive-CLI is a command-line interface for managing and deploying Hive agent and experiments on Kubernetes and other platforms.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
███ █████ █████ ███
|
|
29
|
+
░░░███ ░░███ ░░███ ░░░
|
|
30
|
+
░░░███ ░███ ░███ ████ █████ █████ ██████
|
|
31
|
+
░░░███ ░███████████ ░░███ ░░███ ░░███ ███░░███
|
|
32
|
+
███░ ░███░░░░░███ ░███ ░███ ░███ ░███████
|
|
33
|
+
███░ ░███ ░███ ░███ ░░███ ███ ░███░░░
|
|
34
|
+
███░ █████ █████ █████ ░░█████ ░░██████
|
|
35
|
+
░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### Install via pip (Not-Available Yet)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install hive-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Install from source
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
source start.sh
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How to run
|
|
53
|
+
|
|
54
|
+
**Note**: Hive-CLI reads the configuration from a yaml file, by default it will look for the `~/.hive/sandbox-config.yaml`. You can also specify a different configuration file using the `-f` option. Refer to the [config.yaml](./config.yaml) for examples.
|
|
55
|
+
|
|
56
|
+
Below we assume that you have a `~/.hive/sandbox-config.yaml` file.
|
|
57
|
+
|
|
58
|
+
### Edit the experiment
|
|
59
|
+
|
|
60
|
+
`Edit` command will open the configuration file in your default editor (e.g., vim, nano, etc.) for you to modify the experiment configuration. You can also specify a different editor using the `EDITOR` environment variable, by default it will use `vim`.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
hive edit config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Create an experiment
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
hive create exp my-experiment
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### List experiments
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
hive show exps
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Visit Dashboard
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
hive dashboard
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Delete an experiment
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hive delete exp my-experiment
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### More
|
|
92
|
+
|
|
93
|
+
See `hive -h` for more details.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
**Note**: Hive-CLI will read the `.env` file to load logging configurations. Refer to the `.env.example` file for examples.
|
|
98
|
+
|
|
99
|
+
### Debugging
|
|
100
|
+
|
|
101
|
+
Change the log level in `.env` file to `DEBUG` to see more detailed logs.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Hive-CLI
|
|
2
|
+
|
|
3
|
+
Hive-CLI is a command-line interface for managing and deploying Hive agent and experiments on Kubernetes and other platforms.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
███ █████ █████ ███
|
|
7
|
+
░░░███ ░░███ ░░███ ░░░
|
|
8
|
+
░░░███ ░███ ░███ ████ █████ █████ ██████
|
|
9
|
+
░░░███ ░███████████ ░░███ ░░███ ░░███ ███░░███
|
|
10
|
+
███░ ░███░░░░░███ ░███ ░███ ░███ ░███████
|
|
11
|
+
███░ ░███ ░███ ░███ ░░███ ███ ░███░░░
|
|
12
|
+
███░ █████ █████ █████ ░░█████ ░░██████
|
|
13
|
+
░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Install via pip (Not-Available Yet)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install hive-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Install from source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
source start.sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to run
|
|
31
|
+
|
|
32
|
+
**Note**: Hive-CLI reads the configuration from a yaml file, by default it will look for the `~/.hive/sandbox-config.yaml`. You can also specify a different configuration file using the `-f` option. Refer to the [config.yaml](./config.yaml) for examples.
|
|
33
|
+
|
|
34
|
+
Below we assume that you have a `~/.hive/sandbox-config.yaml` file.
|
|
35
|
+
|
|
36
|
+
### Edit the experiment
|
|
37
|
+
|
|
38
|
+
`Edit` command will open the configuration file in your default editor (e.g., vim, nano, etc.) for you to modify the experiment configuration. You can also specify a different editor using the `EDITOR` environment variable, by default it will use `vim`.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
hive edit config
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Create an experiment
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
hive create exp my-experiment
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### List experiments
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
hive show exps
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Visit Dashboard
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
hive dashboard
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Delete an experiment
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
hive delete exp my-experiment
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### More
|
|
70
|
+
|
|
71
|
+
See `hive -h` for more details.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
**Note**: Hive-CLI will read the `.env` file to load logging configurations. Refer to the `.env.example` file for examples.
|
|
76
|
+
|
|
77
|
+
### Debugging
|
|
78
|
+
|
|
79
|
+
Change the log level in `.env` file to `DEBUG` to see more detailed logs.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hiverge-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Hiverge Team" },
|
|
10
|
+
]
|
|
11
|
+
description = "Universal Command Line Interface for Hive agent."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"PyYAML>=5.1",
|
|
22
|
+
"pydantic>=1.8.2",
|
|
23
|
+
"gitpython>=3.1.24",
|
|
24
|
+
"python-dotenv>=0.19.2",
|
|
25
|
+
"rich>=12.5.1",
|
|
26
|
+
"kubernetes>=32.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = [
|
|
31
|
+
]
|
|
32
|
+
dev = [
|
|
33
|
+
"ruff>=0.12",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
hive = "hive_cli.main:main"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
exclude = ["libs"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "I"]
|
|
45
|
+
ignore = ["E501"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.format]
|
|
48
|
+
quote-style = "double"
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlatformType(str, Enum):
|
|
10
|
+
K8S = "k8s"
|
|
11
|
+
ON_PREM = "on-prem"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResourceConfig(BaseModel):
|
|
15
|
+
requests: Optional[dict] = None
|
|
16
|
+
limits: Optional[dict] = None
|
|
17
|
+
accelerators: Optional[str] = None # e.g., "a100-80gb:8"
|
|
18
|
+
shmsize: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnvConfig(BaseModel):
|
|
22
|
+
name: str
|
|
23
|
+
value: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SandboxConfig(BaseModel):
|
|
27
|
+
image: Optional[str] = None
|
|
28
|
+
replicas: int = 1
|
|
29
|
+
timeout: int = 60
|
|
30
|
+
resources: Optional[ResourceConfig] = None
|
|
31
|
+
envs: Optional[list[EnvConfig]] = None
|
|
32
|
+
pre_processor: Optional[str] = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="The pre-processing script to run before the experiment. Use the `/data` directory to load/store datasets.",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RepoConfig(BaseModel):
|
|
39
|
+
url: str
|
|
40
|
+
branch: str = "main"
|
|
41
|
+
evaluation_script: str = "evaluator.py"
|
|
42
|
+
evolve_files_and_ranges: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WanDBConfig(BaseModel):
|
|
46
|
+
enabled: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GCPConfig(BaseModel):
|
|
50
|
+
enabled: bool = False
|
|
51
|
+
project_id: str = Field(
|
|
52
|
+
default="runsandbox-449400",
|
|
53
|
+
description="The GCP project ID to use for the experiment.",
|
|
54
|
+
)
|
|
55
|
+
image_registry: str | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="The GCP image registry to use for the experiment images. If not set, will use the default GCP registry.",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AWSConfig(BaseModel):
|
|
62
|
+
enabled: bool = False
|
|
63
|
+
image_registry: str | None = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
description="The AWS image registry to use for the experiment images. If not set, will use the default AWS ECR registry.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CloudProviderConfig(BaseModel):
|
|
70
|
+
spot: bool = False
|
|
71
|
+
gcp: Optional[GCPConfig] = None
|
|
72
|
+
aws: Optional[AWSConfig] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class HiveConfig(BaseModel):
|
|
76
|
+
project_name: str = Field(
|
|
77
|
+
description="The name of the project. Must be all lowercase.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
token_path: str = Field(
|
|
81
|
+
default=os.path.expandvars("$HOME/.kube/config"),
|
|
82
|
+
description="Path to the auth token file, default to ~/.kube/config",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
coordinator_config_name: str = Field(
|
|
86
|
+
default="default-coordinator-config",
|
|
87
|
+
description="The name of the coordinator config to use for the experiment. Default to 'default-coordinator-config'.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
platform: PlatformType = PlatformType.K8S
|
|
91
|
+
|
|
92
|
+
repo: RepoConfig
|
|
93
|
+
sandbox: SandboxConfig
|
|
94
|
+
wandb: Optional[WanDBConfig] = None
|
|
95
|
+
|
|
96
|
+
# cloud vendor configuration
|
|
97
|
+
cloud_provider: CloudProviderConfig
|
|
98
|
+
|
|
99
|
+
@field_validator("project_name")
|
|
100
|
+
def must_be_lowercase(cls, v):
|
|
101
|
+
if not v.islower():
|
|
102
|
+
raise ValueError("project_name must be all lowercase")
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
def model_post_init(self, __context):
|
|
106
|
+
if (
|
|
107
|
+
self.cloud_provider.gcp
|
|
108
|
+
and self.cloud_provider.gcp.enabled
|
|
109
|
+
and not self.cloud_provider.gcp.image_registry
|
|
110
|
+
):
|
|
111
|
+
self.cloud_provider.gcp.image_registry = (
|
|
112
|
+
f"gcr.io/{self.cloud_provider.gcp.project_id}/{self.project_name}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
self.cloud_provider.aws
|
|
117
|
+
and self.cloud_provider.aws.enabled
|
|
118
|
+
and not self.cloud_provider.aws.image_registry
|
|
119
|
+
):
|
|
120
|
+
self.cloud_provider.aws.image_registry = (
|
|
121
|
+
f"621302123805.dkr.ecr.eu-north-1.amazonaws.com/hiverge/{self.project_name}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def load_config(file_path: str) -> HiveConfig:
|
|
126
|
+
"""Load configuration from a YAML file."""
|
|
127
|
+
with open(file_path, "r") as file:
|
|
128
|
+
config_data = yaml.safe_load(file)
|
|
129
|
+
return HiveConfig(**config_data)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from hive_cli.config import load_config
|
|
9
|
+
from hive_cli.platform.k8s import K8sPlatform
|
|
10
|
+
|
|
11
|
+
PLATFORMS = {
|
|
12
|
+
"k8s": K8sPlatform,
|
|
13
|
+
# "on-prem": OnPremPlatform,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init(args):
|
|
18
|
+
print("(Unimplemented) Initializing hive...")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_experiment(args):
|
|
22
|
+
BLUE = "\033[94m"
|
|
23
|
+
RESET = "\033[0m"
|
|
24
|
+
|
|
25
|
+
ascii_art = r"""
|
|
26
|
+
███ █████ █████ ███
|
|
27
|
+
░░░███ ░░███ ░░███ ░░░
|
|
28
|
+
░░░███ ░███ ░███ ████ █████ █████ ██████
|
|
29
|
+
░░░███ ░███████████ ░░███ ░░███ ░░███ ███░░███
|
|
30
|
+
███░ ░███░░░░░███ ░███ ░███ ░███ ░███████
|
|
31
|
+
███░ ░███ ░███ ░███ ░░███ ███ ░███░░░
|
|
32
|
+
███░ █████ █████ █████ ░░█████ ░░██████
|
|
33
|
+
░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
print(f"{BLUE}{ascii_art}{RESET}")
|
|
37
|
+
|
|
38
|
+
config = load_config(args.config)
|
|
39
|
+
# Init the platform based on the config.
|
|
40
|
+
platform = PLATFORMS[config.platform.value](args.name, config.token_path)
|
|
41
|
+
|
|
42
|
+
platform.create(config=config)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_experiment(args):
|
|
46
|
+
config = load_config(args.config)
|
|
47
|
+
# Init the platform based on the config.
|
|
48
|
+
platform = PLATFORMS[config.platform.value](args.name, config.token_path)
|
|
49
|
+
|
|
50
|
+
platform.update(args.name, config=config)
|
|
51
|
+
|
|
52
|
+
console = Console()
|
|
53
|
+
msg = Text(f"Experiment {args.name} updated successfully.", style="bold green")
|
|
54
|
+
console.print(msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def delete_experiment(args):
|
|
58
|
+
config = load_config(args.config)
|
|
59
|
+
|
|
60
|
+
platform = PLATFORMS[args.platform](args.platform, config.token_path)
|
|
61
|
+
platform.delete(args.name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def show_experiment(args):
|
|
65
|
+
config = load_config(args.config)
|
|
66
|
+
|
|
67
|
+
platform = PLATFORMS[args.platform](args.platform, config.token_path)
|
|
68
|
+
platform.show_experiments(args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def edit(args):
|
|
72
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
73
|
+
subprocess.run([editor, args.config])
|
|
74
|
+
|
|
75
|
+
console = Console()
|
|
76
|
+
msg = Text(args.config, style="bold magenta")
|
|
77
|
+
msg.append(" edited successfully.", style="bold green")
|
|
78
|
+
console.print(msg)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def show_dashboard(args):
|
|
82
|
+
config = load_config(args.config)
|
|
83
|
+
platform = PLATFORMS[args.platform](args.platform, config.token_path)
|
|
84
|
+
platform.show_dashboard(args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
parser = argparse.ArgumentParser(description="Hive CLI")
|
|
89
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
90
|
+
|
|
91
|
+
# TODO:
|
|
92
|
+
# # init command
|
|
93
|
+
# parser_init = subparsers.add_parser("init", help="Initialize a repository")
|
|
94
|
+
# parser_init.set_defaults(func=init)
|
|
95
|
+
|
|
96
|
+
# create command
|
|
97
|
+
parser_create = subparsers.add_parser("create", help="Create resources")
|
|
98
|
+
create_subparsers = parser_create.add_subparsers(dest="create_target")
|
|
99
|
+
|
|
100
|
+
parser_create_exp = create_subparsers.add_parser(
|
|
101
|
+
"experiment", aliases=["exp"], help="Create a new experiment"
|
|
102
|
+
)
|
|
103
|
+
parser_create_exp.add_argument(
|
|
104
|
+
"name",
|
|
105
|
+
help="Name of the experiment, if it ends with '-', a timestamp will be appended. Example: 'exp-' will become 'exp-2023-10-01-123456'",
|
|
106
|
+
)
|
|
107
|
+
parser_create_exp.add_argument(
|
|
108
|
+
"-f",
|
|
109
|
+
"--config",
|
|
110
|
+
default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
111
|
+
help="Path to the config file, default to ~/.hive/sandbox-config.yaml",
|
|
112
|
+
)
|
|
113
|
+
parser_create_exp.set_defaults(func=create_experiment)
|
|
114
|
+
|
|
115
|
+
# TODO:
|
|
116
|
+
# update command
|
|
117
|
+
# parser_update = subparsers.add_parser("update", help="Update resources")
|
|
118
|
+
# update_subparsers = parser_update.add_subparsers(dest="update_target")
|
|
119
|
+
|
|
120
|
+
# parser_update_exp = update_subparsers.add_parser(
|
|
121
|
+
# "experiment", aliases=["exp"], help="Update an experiment"
|
|
122
|
+
# )
|
|
123
|
+
# parser_update_exp.add_argument("name", help="Name of the experiment")
|
|
124
|
+
# parser_update_exp.add_argument(
|
|
125
|
+
# "-f",
|
|
126
|
+
# "--config",
|
|
127
|
+
# default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
128
|
+
# help="Path to the config file, default to ~/.hive/sandbox-config.yaml",
|
|
129
|
+
# )
|
|
130
|
+
# parser_update_exp.set_defaults(func=update_experiment)
|
|
131
|
+
|
|
132
|
+
# delete command
|
|
133
|
+
parser_delete = subparsers.add_parser("delete", help="Delete resources")
|
|
134
|
+
delete_subparsers = parser_delete.add_subparsers(dest="delete_target")
|
|
135
|
+
parser_delete_exp = delete_subparsers.add_parser(
|
|
136
|
+
"experiment", aliases=["exp"], help="Delete an experiment"
|
|
137
|
+
)
|
|
138
|
+
parser_delete_exp.add_argument("name", help="Name of the experiment")
|
|
139
|
+
parser_delete_exp.add_argument(
|
|
140
|
+
"-p",
|
|
141
|
+
"--platform",
|
|
142
|
+
default="k8s",
|
|
143
|
+
choices=PLATFORMS.keys(),
|
|
144
|
+
help="Platform to use, k8s or on-prem, default to use k8s",
|
|
145
|
+
)
|
|
146
|
+
parser_delete_exp.add_argument(
|
|
147
|
+
"-f",
|
|
148
|
+
"--config",
|
|
149
|
+
default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
150
|
+
help="Path to the config file, default to ~/.hive/sandbox-config.yaml",
|
|
151
|
+
)
|
|
152
|
+
parser_delete_exp.set_defaults(func=delete_experiment)
|
|
153
|
+
|
|
154
|
+
# show command
|
|
155
|
+
parser_show = subparsers.add_parser("show", help="Show resources")
|
|
156
|
+
show_subparsers = parser_show.add_subparsers(dest="show_target")
|
|
157
|
+
parser_show_exp = show_subparsers.add_parser(
|
|
158
|
+
"experiments", aliases=["exp", "exps"], help="Show experiments"
|
|
159
|
+
)
|
|
160
|
+
parser_show_exp.add_argument(
|
|
161
|
+
"-p",
|
|
162
|
+
"--platform",
|
|
163
|
+
default="k8s",
|
|
164
|
+
choices=PLATFORMS.keys(),
|
|
165
|
+
help="Platform to use, k8s or on-prem, default to use k8s",
|
|
166
|
+
)
|
|
167
|
+
parser_show_exp.add_argument(
|
|
168
|
+
"-f",
|
|
169
|
+
"--config",
|
|
170
|
+
default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
171
|
+
help="Path to the config file, default to ~/.hive/sandbox-config.yaml",
|
|
172
|
+
)
|
|
173
|
+
parser_show_exp.set_defaults(func=show_experiment)
|
|
174
|
+
|
|
175
|
+
# edit command
|
|
176
|
+
parser_edit = subparsers.add_parser("edit", help="Edit Hive configuration")
|
|
177
|
+
edit_subparsers = parser_edit.add_subparsers(dest="edit_target")
|
|
178
|
+
parser_edit_config = edit_subparsers.add_parser(
|
|
179
|
+
"config", help="Edit the Hive configuration file"
|
|
180
|
+
)
|
|
181
|
+
parser_edit_config.add_argument(
|
|
182
|
+
"-f",
|
|
183
|
+
"--config",
|
|
184
|
+
default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
185
|
+
help="Path to the config file, defaults to ~/.hive/sandbox-config.yaml",
|
|
186
|
+
)
|
|
187
|
+
parser_edit_config.set_defaults(func=edit)
|
|
188
|
+
|
|
189
|
+
# dashboard command
|
|
190
|
+
parser_dashboard = subparsers.add_parser("dashboard", help="Open the Hive dashboard")
|
|
191
|
+
parser_dashboard.add_argument(
|
|
192
|
+
"--port",
|
|
193
|
+
default=8080,
|
|
194
|
+
type=int,
|
|
195
|
+
help="Port to run the dashboard on, default to 8080",
|
|
196
|
+
)
|
|
197
|
+
parser_dashboard.add_argument(
|
|
198
|
+
"-f",
|
|
199
|
+
"--config",
|
|
200
|
+
default=os.path.expandvars("$HOME/.hive/sandbox-config.yaml"),
|
|
201
|
+
help="Path to the config file, default to ~/.hive/sandbox-config.yaml",
|
|
202
|
+
)
|
|
203
|
+
parser_dashboard.add_argument(
|
|
204
|
+
"-p",
|
|
205
|
+
"--platform",
|
|
206
|
+
default="k8s",
|
|
207
|
+
choices=PLATFORMS.keys(),
|
|
208
|
+
help="Platform to use, k8s or on-prem, default to use k8s",
|
|
209
|
+
)
|
|
210
|
+
parser_dashboard.set_defaults(func=show_dashboard)
|
|
211
|
+
|
|
212
|
+
args = parser.parse_args()
|
|
213
|
+
if hasattr(args, "func"):
|
|
214
|
+
args.func(args)
|
|
215
|
+
else:
|
|
216
|
+
parser.print_help()
|
|
File without changes
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import tempfile
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hive_cli.config import HiveConfig
|
|
8
|
+
from hive_cli.runtime.runtime import Runtime
|
|
9
|
+
from hive_cli.utils import git
|
|
10
|
+
from hive_cli.utils.image import build_image
|
|
11
|
+
from hive_cli.utils.logger import logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Platform(Runtime, ABC):
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def create(self, config: HiveConfig):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def update(self, name: str, config: HiveConfig):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def delete(self, name: str):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def login(self, args):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def show_experiments(self, args):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def show_dashboard(self, args):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str, token_path: str = None):
|
|
40
|
+
super().__init__(name, token_path)
|
|
41
|
+
|
|
42
|
+
# setup_environment function can be used to prepare the environment for the experiment,
|
|
43
|
+
# shared logic for both K8s and OnPrem platforms.
|
|
44
|
+
def setup_environment(self, config: HiveConfig) -> HiveConfig:
|
|
45
|
+
"""
|
|
46
|
+
Set up the environment for the experiment.
|
|
47
|
+
This includes building the Docker image and preparing any necessary resources.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config (HiveConfig): The configuration for the experiment.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
HiveConfig: The updated configuration with the image name set.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
logger.info(f"Setting up environment for experiment '{self.experiment_name}'")
|
|
57
|
+
logger.debug(f"The HiveConfig: {config}")
|
|
58
|
+
|
|
59
|
+
# Here you can add more setup logic, like initializing Kubernetes resources
|
|
60
|
+
# or configuring the environment based on the HiveConfig.
|
|
61
|
+
with tempfile.TemporaryDirectory(dir="./tmp") as temp_dir:
|
|
62
|
+
image_name = self.prepare_images(config, temp_dir, push=True)
|
|
63
|
+
|
|
64
|
+
# Populate related fields to the config, only allow to update here.
|
|
65
|
+
config.sandbox.image = image_name
|
|
66
|
+
|
|
67
|
+
logger.debug(f"The updated HiveConfig: {config}")
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
def prepare_images(self, config: HiveConfig, temp_dir: str, push: bool = False) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Build the Docker image for the experiment.
|
|
73
|
+
If `push` is True, it will push the image to the registry.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
config (HiveConfig): The configuration for the experiment.
|
|
77
|
+
temp_dir (str): The temporary directory to use for building the image.
|
|
78
|
+
push (bool): Whether to push the image to the registry.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: The name of the built image.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
logger.debug(f"Preparing images for experiment '{self.experiment_name}' in {temp_dir}")
|
|
85
|
+
|
|
86
|
+
# TODO: refactor this part to use an image by default rather than build from the scratch.
|
|
87
|
+
shutil.copytree(
|
|
88
|
+
"./libs",
|
|
89
|
+
Path("./tmp") / temp_dir,
|
|
90
|
+
dirs_exist_ok=True,
|
|
91
|
+
)
|
|
92
|
+
dest = Path(temp_dir) / "repo"
|
|
93
|
+
|
|
94
|
+
git.clone_repo(config.repo.url, dest, config.repo.branch)
|
|
95
|
+
logger.debug(
|
|
96
|
+
f"Cloning repository {config.repo.url} to {dest}, the tree structure of the directory: {os.listdir('.')}, the tree structure of the {dest} directory: {os.listdir(dest)}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not (dest / "Dockerfile").exists():
|
|
100
|
+
logger.debug(f"No Dockerfile found in {dest}, generating one.")
|
|
101
|
+
# Generate Dockerfile for the experiment
|
|
102
|
+
generate_dockerfile(dest)
|
|
103
|
+
|
|
104
|
+
logger.debug(f"Building temporary repo image in {dest}")
|
|
105
|
+
# build the repository image first
|
|
106
|
+
build_image(
|
|
107
|
+
image="repo-image:latest",
|
|
108
|
+
context=dest,
|
|
109
|
+
dockerfile=dest / "Dockerfile",
|
|
110
|
+
# this is a temporary image, so we don't push it
|
|
111
|
+
push=False,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if config.cloud_provider.gcp and config.cloud_provider.gcp.enabled:
|
|
115
|
+
image_registry = config.cloud_provider.gcp.image_registry
|
|
116
|
+
elif config.cloud_provider.aws and config.cloud_provider.aws.enabled:
|
|
117
|
+
image_registry = config.cloud_provider.aws.image_registry
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError("Unsupported cloud provider configuration. Please enable GCP or AWS.")
|
|
120
|
+
|
|
121
|
+
image_name = f"{image_registry}:{self.experiment_name}"
|
|
122
|
+
|
|
123
|
+
logger.debug(f"Building sandbox image {image_name} in {temp_dir} with push={push}")
|
|
124
|
+
# build the sandbox image
|
|
125
|
+
build_image(
|
|
126
|
+
image=image_name,
|
|
127
|
+
context=temp_dir,
|
|
128
|
+
dockerfile=f"{temp_dir}/Dockerfile",
|
|
129
|
+
push=push,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Images {image_name} prepared for experiment '{self.experiment_name}' successfully."
|
|
134
|
+
)
|
|
135
|
+
return image_name
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# copied from the original hiverge project.
|
|
139
|
+
def generate_dockerfile(dest: Path) -> None:
|
|
140
|
+
"""Create a Dockerfile inside `dest`."""
|
|
141
|
+
lines = [
|
|
142
|
+
"FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim",
|
|
143
|
+
"",
|
|
144
|
+
"RUN apt-get update && apt-get install --no-install-recommends -y \\",
|
|
145
|
+
"cmake \\",
|
|
146
|
+
"build-essential \\",
|
|
147
|
+
"pkg-config \\",
|
|
148
|
+
"&& rm -rf /var/lib/apt/lists/*",
|
|
149
|
+
"",
|
|
150
|
+
"WORKDIR /app",
|
|
151
|
+
"",
|
|
152
|
+
"# Install sandbox server dependencies",
|
|
153
|
+
]
|
|
154
|
+
if (dest / "pyproject.toml").exists():
|
|
155
|
+
lines.append("# Install repository dependencies from pyproject.toml")
|
|
156
|
+
lines.append("COPY pyproject.toml .")
|
|
157
|
+
lines.append("RUN uv pip install --system --requirement pyproject.toml")
|
|
158
|
+
elif (dest / "requirements.txt").exists():
|
|
159
|
+
lines.append("# Install repository dependencies from requirements.txt")
|
|
160
|
+
lines.append("COPY requirements.txt .")
|
|
161
|
+
lines.append("RUN uv pip install --system --requirement requirements.txt")
|
|
162
|
+
|
|
163
|
+
lines.extend(
|
|
164
|
+
[
|
|
165
|
+
"",
|
|
166
|
+
"# Copy server code and evaluation file",
|
|
167
|
+
"COPY . repo",
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
(dest / "Dockerfile").write_text("\n".join(lines), encoding="utf-8")
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from kubernetes import client
|
|
4
|
+
from kubernetes import config as k8s_config
|
|
5
|
+
from kubernetes.client.api_client import ApiClient
|
|
6
|
+
from kubernetes.client.rest import ApiException
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from hive_cli.config import HiveConfig
|
|
12
|
+
from hive_cli.platform.base import Platform
|
|
13
|
+
from hive_cli.utils.logger import logger
|
|
14
|
+
from hive_cli.utils.time import humanize_time
|
|
15
|
+
|
|
16
|
+
GROUP = "core.hiverge.ai"
|
|
17
|
+
VERSION = "v1alpha1"
|
|
18
|
+
RESOURCE = "Experiment"
|
|
19
|
+
RESOURCE_PLURAL = "experiments"
|
|
20
|
+
# TODO: remove this once we support custom namespace
|
|
21
|
+
NAMESPACE = "default"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class K8sPlatform(Platform):
|
|
25
|
+
def __init__(self, name: str, token_path: str = None):
|
|
26
|
+
super().__init__(name, token_path)
|
|
27
|
+
|
|
28
|
+
k8s_config.load_kube_config(config_file=token_path)
|
|
29
|
+
self.client = client.CustomObjectsApi()
|
|
30
|
+
|
|
31
|
+
def create(self, config: HiveConfig):
|
|
32
|
+
logger.info(f"Creating experiment '{self.experiment_name}' on Kubernetes...")
|
|
33
|
+
config = self.setup_environment(config)
|
|
34
|
+
deploy("CREATE", self.client, self.experiment_name, config)
|
|
35
|
+
|
|
36
|
+
def update(self, name: str, config: HiveConfig):
|
|
37
|
+
logger.info(f"Updating experiment '{name}' on Kubernetes...")
|
|
38
|
+
deploy("UPDATE", self.client, name, config)
|
|
39
|
+
|
|
40
|
+
def delete(self, name: str):
|
|
41
|
+
logger.info(f"Deleting experiment '{name}' on Kubernetes...")
|
|
42
|
+
try:
|
|
43
|
+
# Attempt to delete the experiment by its name
|
|
44
|
+
self.client.delete_namespaced_custom_object(
|
|
45
|
+
group=GROUP,
|
|
46
|
+
version=VERSION,
|
|
47
|
+
namespace=NAMESPACE,
|
|
48
|
+
plural=RESOURCE_PLURAL,
|
|
49
|
+
name=name,
|
|
50
|
+
)
|
|
51
|
+
logger.info(f"Experiment '{name}' deleted successfully on Kubernetes.")
|
|
52
|
+
except ApiException as e:
|
|
53
|
+
logger.error(f"Failed to delete experiment '{name}' on Kubernetes: {e}")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"An unexpected error occurred while deleting experiment '{name}': {e}")
|
|
56
|
+
|
|
57
|
+
def login(self, args):
|
|
58
|
+
logger.info(f"Logging in to hive on {args.platform} platform...")
|
|
59
|
+
|
|
60
|
+
def show_experiments(self, args):
|
|
61
|
+
resp = self.client.list_namespaced_custom_object(
|
|
62
|
+
group=GROUP,
|
|
63
|
+
version=VERSION,
|
|
64
|
+
namespace=NAMESPACE,
|
|
65
|
+
plural=RESOURCE_PLURAL,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
table = Table(show_header=True, header_style="bold", box=None, show_lines=False)
|
|
69
|
+
table.add_column("Name")
|
|
70
|
+
table.add_column("Status")
|
|
71
|
+
table.add_column("Sandboxes")
|
|
72
|
+
table.add_column("Age")
|
|
73
|
+
|
|
74
|
+
for item in resp.get("items", []):
|
|
75
|
+
metadata = item.get("metadata", {})
|
|
76
|
+
age = humanize_time(metadata.get("creationTimestamp"))
|
|
77
|
+
status = item.get("status", {}).get("phase", "Unknown")
|
|
78
|
+
replicas = item.get("status", {}).get("sandboxReplicas", 0)
|
|
79
|
+
unavailable_replicas = item.get("status", {}).get("sandboxUnavailableReplicas", 0)
|
|
80
|
+
|
|
81
|
+
table.add_row(
|
|
82
|
+
metadata.get("name", "Unknown"),
|
|
83
|
+
status,
|
|
84
|
+
f"{replicas - unavailable_replicas}/{replicas}",
|
|
85
|
+
age if age else "N/A",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
console = Console()
|
|
89
|
+
console.print(table)
|
|
90
|
+
|
|
91
|
+
def show_dashboard(self, args):
|
|
92
|
+
resp = self.client.list_namespaced_custom_object(
|
|
93
|
+
group=GROUP,
|
|
94
|
+
version=VERSION,
|
|
95
|
+
namespace=NAMESPACE,
|
|
96
|
+
plural=RESOURCE_PLURAL,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for item in resp.get("items", []):
|
|
100
|
+
metadata = item.get("metadata", {})
|
|
101
|
+
name = metadata.get("name", "Unknown")
|
|
102
|
+
|
|
103
|
+
console = Console()
|
|
104
|
+
url = f"http://localhost:{args.port}"
|
|
105
|
+
msg = Text("Open Hive dashboard at ", style="bold green")
|
|
106
|
+
msg.append(url, style="bold magenta")
|
|
107
|
+
msg.append(" ...", style="dim")
|
|
108
|
+
console.print(msg)
|
|
109
|
+
|
|
110
|
+
commands = [
|
|
111
|
+
"kubectl",
|
|
112
|
+
f"--kubeconfig={self.token_path}",
|
|
113
|
+
"port-forward",
|
|
114
|
+
f"svc/{name}-dashboard-frontend",
|
|
115
|
+
f"{str(args.port)}:8080",
|
|
116
|
+
]
|
|
117
|
+
process = subprocess.Popen(
|
|
118
|
+
commands, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
process.wait()
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def deploy(op: str, client: ApiClient, name: str, config: HiveConfig):
|
|
129
|
+
logger.info(f"Applying experiment '{name}' on Kubernetes...")
|
|
130
|
+
|
|
131
|
+
body = construct_experiment(name, NAMESPACE, config)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
if op == "CREATE":
|
|
135
|
+
resp = client.create_namespaced_custom_object(
|
|
136
|
+
group=GROUP, version=VERSION, namespace=NAMESPACE, plural=RESOURCE_PLURAL, body=body
|
|
137
|
+
)
|
|
138
|
+
logger.info(
|
|
139
|
+
f"Experiment '{name}' created successfully on Kubernetes with name {resp['metadata']['name']}."
|
|
140
|
+
)
|
|
141
|
+
# TODO: add validation for op, only replicas can be updated
|
|
142
|
+
elif op == "UPDATE":
|
|
143
|
+
current_exp = client.get_namespaced_custom_object(
|
|
144
|
+
group=GROUP, version=VERSION, namespace=NAMESPACE, plural=RESOURCE_PLURAL, name=name
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Populate some fields manually because they're generated in creation.
|
|
148
|
+
if body["spec"]["sandbox"].get("image") is None:
|
|
149
|
+
body["spec"]["sandbox"]["image"] = current_exp["spec"]["sandbox"]["image"]
|
|
150
|
+
|
|
151
|
+
resp = client.patch_namespaced_custom_object(
|
|
152
|
+
group=GROUP,
|
|
153
|
+
version=VERSION,
|
|
154
|
+
namespace=NAMESPACE,
|
|
155
|
+
plural=RESOURCE_PLURAL,
|
|
156
|
+
name=name,
|
|
157
|
+
body=body,
|
|
158
|
+
)
|
|
159
|
+
logger.info(
|
|
160
|
+
f"Experiment '{name}' updated successfully on Kubernetes with name {resp['metadata']['name']}."
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Unsupported operation: {op}. Supported operations are 'CREATE' and 'UPDATE'."
|
|
165
|
+
)
|
|
166
|
+
except ApiException as e:
|
|
167
|
+
logger.error(f"Failed to deploy experiment '{name}' on Kubernetes: {e}")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"An unexpected error occurred while deploying experiment '{name}': {e}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def construct_experiment(name: str, namespace: str, config: HiveConfig) -> dict:
|
|
173
|
+
"""
|
|
174
|
+
Constructs a Kubernetes custom resource definition (CRD) for an experiment.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name (str): The name of the experiment.
|
|
178
|
+
namespace (str): The Kubernetes namespace where the experiment will be deployed.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
dict: A dictionary representing the CRD for the experiment.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if config.cloud_provider.gcp and config.cloud_provider.gcp.enabled:
|
|
185
|
+
cloud_provider_name = "gcp"
|
|
186
|
+
elif config.cloud_provider.aws and config.cloud_provider.aws.enabled:
|
|
187
|
+
cloud_provider_name = "aws"
|
|
188
|
+
else:
|
|
189
|
+
cloud_provider_name = "unknown"
|
|
190
|
+
|
|
191
|
+
if config.sandbox.envs is not None:
|
|
192
|
+
envs = [env.model_dump() for env in config.sandbox.envs]
|
|
193
|
+
else:
|
|
194
|
+
envs = None
|
|
195
|
+
|
|
196
|
+
if config.sandbox.resources is not None:
|
|
197
|
+
resources = config.sandbox.resources.model_dump()
|
|
198
|
+
else:
|
|
199
|
+
resources = {}
|
|
200
|
+
|
|
201
|
+
result = {
|
|
202
|
+
"apiVersion": f"{GROUP}/{VERSION}",
|
|
203
|
+
"kind": RESOURCE,
|
|
204
|
+
"metadata": {
|
|
205
|
+
"name": name,
|
|
206
|
+
"namespace": namespace,
|
|
207
|
+
},
|
|
208
|
+
"spec": {
|
|
209
|
+
"projectName": config.project_name,
|
|
210
|
+
"coordinatorConfigName": config.coordinator_config_name,
|
|
211
|
+
"sandbox": {
|
|
212
|
+
"image": config.sandbox.image,
|
|
213
|
+
"replicas": config.sandbox.replicas,
|
|
214
|
+
"timeout": config.sandbox.timeout,
|
|
215
|
+
"resources": resources,
|
|
216
|
+
"envs": envs,
|
|
217
|
+
"preprocessor": config.sandbox.pre_processor,
|
|
218
|
+
},
|
|
219
|
+
"repo": {
|
|
220
|
+
"url": config.repo.url,
|
|
221
|
+
"branch": config.repo.branch,
|
|
222
|
+
"evaluationScript": config.repo.evaluation_script,
|
|
223
|
+
"evolveFilesAndRanges": config.repo.evolve_files_and_ranges,
|
|
224
|
+
},
|
|
225
|
+
"cloudProvider": {
|
|
226
|
+
"spot": config.cloud_provider.spot,
|
|
227
|
+
"name": cloud_provider_name,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from hive_cli.config import HiveConfig
|
|
2
|
+
|
|
3
|
+
from .base import Platform
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OnPremPlatform(Platform):
|
|
7
|
+
def __init__(self, name: str):
|
|
8
|
+
super().__init__(name)
|
|
9
|
+
|
|
10
|
+
def create(self, config: HiveConfig):
|
|
11
|
+
print(f"Creating hive on-premise with name: {self.experiment_name} and config: {config}")
|
|
12
|
+
|
|
13
|
+
def update(self, name: str, config: HiveConfig):
|
|
14
|
+
print(f"Updating hive on-premise with name: {name} and config: {config}")
|
|
15
|
+
|
|
16
|
+
def delete(self, name: str):
|
|
17
|
+
print("Deleting hive on-premise...")
|
|
18
|
+
|
|
19
|
+
def login(self, args):
|
|
20
|
+
print("Logging in to hive on-premise...")
|
|
21
|
+
|
|
22
|
+
def show_experiments(self, args):
|
|
23
|
+
print("Showing experiments on-premise...")
|
|
24
|
+
|
|
25
|
+
def show_dashboard(self, args):
|
|
26
|
+
print("Showing dashboard on-premise...")
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Runtime:
|
|
6
|
+
def __init__(self, name: str, token_path: str = None):
|
|
7
|
+
"""Initialize the Runtime with a name.
|
|
8
|
+
This can be used to set up any necessary runtime configurations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
self.token_path = token_path
|
|
12
|
+
self.experiment_name = generate_experiment_name(name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_experiment_name(base_name: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Generate a unique experiment name based on the base name and current timestamp.
|
|
18
|
+
If the base name ends with '-', it will be suffixed with a timestamp.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
experiment_name = base_name
|
|
22
|
+
|
|
23
|
+
if base_name.endswith("-"):
|
|
24
|
+
timestamp = str(int(datetime.now(timezone.utc).timestamp()))
|
|
25
|
+
unique_hash = hashlib.sha1(timestamp.encode()).hexdigest()[:12]
|
|
26
|
+
experiment_name = f"{base_name}{unique_hash}"
|
|
27
|
+
|
|
28
|
+
return experiment_name
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import git
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def clone_repo(repo_dir: str, output_dir: str, branch: str = "main") -> None:
|
|
9
|
+
"""Clone a repository into the output directory."""
|
|
10
|
+
dest = Path(output_dir)
|
|
11
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
if repo_dir.startswith("https://"):
|
|
13
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
14
|
+
if token:
|
|
15
|
+
# Inject token into the URL for authentication
|
|
16
|
+
repo_dir = repo_dir.replace("https://", f"https://x-access-token:{token}@")
|
|
17
|
+
repo = git.Repo.clone_from(repo_dir, dest)
|
|
18
|
+
repo.git.checkout(branch)
|
|
19
|
+
else: # We assume `repo_dir` is a directory in this machine.
|
|
20
|
+
repo_path = Path(repo_dir).resolve()
|
|
21
|
+
if not repo_path.exists():
|
|
22
|
+
raise FileNotFoundError(f"Repository directory {repo_dir} does not exist")
|
|
23
|
+
if not repo_path.is_dir():
|
|
24
|
+
raise NotADirectoryError(f"{repo_dir} is not a directory")
|
|
25
|
+
shutil.copytree(repo_path, dest, dirs_exist_ok=True)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from hive_cli.utils.logger import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_image(
|
|
8
|
+
image: str,
|
|
9
|
+
platforms: str = "linux/amd64,linux/arm64",
|
|
10
|
+
context: str = ".",
|
|
11
|
+
dockerfile: str = "Dockerfile",
|
|
12
|
+
push: bool = False,
|
|
13
|
+
):
|
|
14
|
+
cmd = [
|
|
15
|
+
"docker",
|
|
16
|
+
"buildx",
|
|
17
|
+
"build",
|
|
18
|
+
"--platform",
|
|
19
|
+
platforms,
|
|
20
|
+
"--file",
|
|
21
|
+
dockerfile,
|
|
22
|
+
"--tag",
|
|
23
|
+
image,
|
|
24
|
+
"--load",
|
|
25
|
+
context,
|
|
26
|
+
]
|
|
27
|
+
if push:
|
|
28
|
+
cmd.append("--push")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
32
|
+
capture_output = False
|
|
33
|
+
else:
|
|
34
|
+
capture_output = True
|
|
35
|
+
|
|
36
|
+
subprocess.run(
|
|
37
|
+
cmd,
|
|
38
|
+
check=True,
|
|
39
|
+
capture_output=capture_output,
|
|
40
|
+
text=True,
|
|
41
|
+
)
|
|
42
|
+
except subprocess.CalledProcessError as e:
|
|
43
|
+
print("Build STDERR:\n", e.stderr)
|
|
44
|
+
raise
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from rich.logging import RichHandler
|
|
6
|
+
|
|
7
|
+
# load the .env file.
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_logging(name: str) -> logging.Logger:
|
|
12
|
+
"""
|
|
13
|
+
Set up a logger with the specified name and logging level.
|
|
14
|
+
If no level is provided, it defaults to INFO.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
log_level = os.getenv("LOG_LEVEL", "INFO").upper() # Default log level
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=log_level,
|
|
21
|
+
format="%(name)s %(message)s",
|
|
22
|
+
handlers=[
|
|
23
|
+
RichHandler(
|
|
24
|
+
show_time=True, show_level=True, show_path=False, markup=True, rich_tracebacks=True
|
|
25
|
+
)
|
|
26
|
+
],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return logging.getLogger(name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = setup_logging("hive-cli")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def humanize_time(timestamp: str) -> str:
|
|
5
|
+
creation_time = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(
|
|
6
|
+
tzinfo=datetime.UTC
|
|
7
|
+
)
|
|
8
|
+
t = datetime.datetime.now(datetime.UTC) - creation_time
|
|
9
|
+
|
|
10
|
+
if t.days > 0:
|
|
11
|
+
age = f"{t.days}d"
|
|
12
|
+
elif t.seconds >= 3600:
|
|
13
|
+
age = f"{t.seconds // 3600}h"
|
|
14
|
+
elif t.seconds >= 60:
|
|
15
|
+
age = f"{t.seconds // 60}m"
|
|
16
|
+
else:
|
|
17
|
+
age = f"{t.seconds}s"
|
|
18
|
+
|
|
19
|
+
return age
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiverge-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Command Line Interface for Hive agent.
|
|
5
|
+
Author: Hiverge Team
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYAML>=5.1
|
|
13
|
+
Requires-Dist: pydantic>=1.8.2
|
|
14
|
+
Requires-Dist: gitpython>=3.1.24
|
|
15
|
+
Requires-Dist: python-dotenv>=0.19.2
|
|
16
|
+
Requires-Dist: rich>=12.5.1
|
|
17
|
+
Requires-Dist: kubernetes>=32.0.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Hive-CLI
|
|
24
|
+
|
|
25
|
+
Hive-CLI is a command-line interface for managing and deploying Hive agent and experiments on Kubernetes and other platforms.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
███ █████ █████ ███
|
|
29
|
+
░░░███ ░░███ ░░███ ░░░
|
|
30
|
+
░░░███ ░███ ░███ ████ █████ █████ ██████
|
|
31
|
+
░░░███ ░███████████ ░░███ ░░███ ░░███ ███░░███
|
|
32
|
+
███░ ░███░░░░░███ ░███ ░███ ░███ ░███████
|
|
33
|
+
███░ ░███ ░███ ░███ ░░███ ███ ░███░░░
|
|
34
|
+
███░ █████ █████ █████ ░░█████ ░░██████
|
|
35
|
+
░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### Install via pip (Not-Available Yet)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install hive-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Install from source
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
source start.sh
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How to run
|
|
53
|
+
|
|
54
|
+
**Note**: Hive-CLI reads the configuration from a yaml file, by default it will look for the `~/.hive/sandbox-config.yaml`. You can also specify a different configuration file using the `-f` option. Refer to the [config.yaml](./config.yaml) for examples.
|
|
55
|
+
|
|
56
|
+
Below we assume that you have a `~/.hive/sandbox-config.yaml` file.
|
|
57
|
+
|
|
58
|
+
### Edit the experiment
|
|
59
|
+
|
|
60
|
+
`Edit` command will open the configuration file in your default editor (e.g., vim, nano, etc.) for you to modify the experiment configuration. You can also specify a different editor using the `EDITOR` environment variable, by default it will use `vim`.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
hive edit config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Create an experiment
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
hive create exp my-experiment
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### List experiments
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
hive show exps
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Visit Dashboard
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
hive dashboard
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Delete an experiment
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hive delete exp my-experiment
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### More
|
|
92
|
+
|
|
93
|
+
See `hive -h` for more details.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
**Note**: Hive-CLI will read the `.env` file to load logging configurations. Refer to the `.env.example` file for examples.
|
|
98
|
+
|
|
99
|
+
### Debugging
|
|
100
|
+
|
|
101
|
+
Change the log level in `.env` file to `DEBUG` to see more detailed logs.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/hive_cli/__init__.py
|
|
5
|
+
src/hive_cli/config.py
|
|
6
|
+
src/hive_cli/main.py
|
|
7
|
+
src/hive_cli/platform/__init__.py
|
|
8
|
+
src/hive_cli/platform/base.py
|
|
9
|
+
src/hive_cli/platform/k8s.py
|
|
10
|
+
src/hive_cli/platform/onprem.py
|
|
11
|
+
src/hive_cli/runtime/__init__.py
|
|
12
|
+
src/hive_cli/runtime/runtime.py
|
|
13
|
+
src/hive_cli/utils/__init__.py
|
|
14
|
+
src/hive_cli/utils/git.py
|
|
15
|
+
src/hive_cli/utils/image.py
|
|
16
|
+
src/hive_cli/utils/logger.py
|
|
17
|
+
src/hive_cli/utils/time.py
|
|
18
|
+
src/hiverge_cli.egg-info/PKG-INFO
|
|
19
|
+
src/hiverge_cli.egg-info/SOURCES.txt
|
|
20
|
+
src/hiverge_cli.egg-info/dependency_links.txt
|
|
21
|
+
src/hiverge_cli.egg-info/entry_points.txt
|
|
22
|
+
src/hiverge_cli.egg-info/requires.txt
|
|
23
|
+
src/hiverge_cli.egg-info/top_level.txt
|
|
24
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hive_cli
|