jumpstarter-kubernetes 0.6.0__tar.gz → 0.7.1__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 jumpstarter-kubernetes might be problematic. Click here for more details.
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/PKG-INFO +2 -2
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/__init__.py +20 -1
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/clients.py +27 -0
- jumpstarter_kubernetes-0.7.1/jumpstarter_kubernetes/cluster.py +201 -0
- jumpstarter_kubernetes-0.7.1/jumpstarter_kubernetes/cluster_test.py +395 -0
- jumpstarter_kubernetes-0.7.1/jumpstarter_kubernetes/datetime.py +32 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/exporters.py +54 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/install.py +26 -6
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/leases.py +63 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/pyproject.toml +1 -1
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/.gitignore +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/README.md +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/clients_test.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/exporters_test.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/json.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/list.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/py.typed +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/serialize.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/test_leases.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/util/__init__.py +0 -0
- {jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/util/async_custom_object_api.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jumpstarter-kubernetes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Project-URL: Homepage, https://jumpstarter.dev
|
|
5
|
-
Project-URL: source_archive, https://github.com/jumpstarter-dev/repo/archive/
|
|
5
|
+
Project-URL: source_archive, https://github.com/jumpstarter-dev/repo/archive/7a5a95ea4fab743def1fe84f93165fbd49fc2de7.zip
|
|
6
6
|
Author-email: Kirk Brauer <kbrauer@hatci.com>
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.11
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/__init__.py
RENAMED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
from .clients import ClientsV1Alpha1Api, V1Alpha1Client, V1Alpha1ClientList, V1Alpha1ClientStatus
|
|
2
|
+
from .cluster import (
|
|
3
|
+
create_kind_cluster,
|
|
4
|
+
create_minikube_cluster,
|
|
5
|
+
delete_kind_cluster,
|
|
6
|
+
delete_minikube_cluster,
|
|
7
|
+
kind_installed,
|
|
8
|
+
minikube_installed,
|
|
9
|
+
)
|
|
2
10
|
from .exporters import (
|
|
3
11
|
ExportersV1Alpha1Api,
|
|
4
12
|
V1Alpha1Exporter,
|
|
@@ -6,7 +14,11 @@ from .exporters import (
|
|
|
6
14
|
V1Alpha1ExporterList,
|
|
7
15
|
V1Alpha1ExporterStatus,
|
|
8
16
|
)
|
|
9
|
-
from .install import
|
|
17
|
+
from .install import (
|
|
18
|
+
helm_installed,
|
|
19
|
+
install_helm_chart,
|
|
20
|
+
uninstall_helm_chart,
|
|
21
|
+
)
|
|
10
22
|
from .leases import (
|
|
11
23
|
LeasesV1Alpha1Api,
|
|
12
24
|
V1Alpha1Lease,
|
|
@@ -36,4 +48,11 @@ __all__ = [
|
|
|
36
48
|
"V1Alpha1List",
|
|
37
49
|
"helm_installed",
|
|
38
50
|
"install_helm_chart",
|
|
51
|
+
"uninstall_helm_chart",
|
|
52
|
+
"minikube_installed",
|
|
53
|
+
"kind_installed",
|
|
54
|
+
"create_minikube_cluster",
|
|
55
|
+
"create_kind_cluster",
|
|
56
|
+
"delete_minikube_cluster",
|
|
57
|
+
"delete_kind_cluster",
|
|
39
58
|
]
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/clients.py
RENAMED
|
@@ -53,6 +53,21 @@ class V1Alpha1Client(JsonBaseModel):
|
|
|
53
53
|
else None,
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
+
@classmethod
|
|
57
|
+
def rich_add_columns(cls, table):
|
|
58
|
+
table.add_column("NAME")
|
|
59
|
+
table.add_column("ENDPOINT")
|
|
60
|
+
# table.add_column("AGE")
|
|
61
|
+
|
|
62
|
+
def rich_add_rows(self, table):
|
|
63
|
+
table.add_row(
|
|
64
|
+
self.metadata.name,
|
|
65
|
+
self.status.endpoint if self.status is not None else "",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def rich_add_names(self, names):
|
|
69
|
+
names.append(f"client.jumpstarter.dev/{self.metadata.name}")
|
|
70
|
+
|
|
56
71
|
|
|
57
72
|
class V1Alpha1ClientList(V1Alpha1List[V1Alpha1Client]):
|
|
58
73
|
kind: Literal["ClientList"] = Field(default="ClientList")
|
|
@@ -61,6 +76,18 @@ class V1Alpha1ClientList(V1Alpha1List[V1Alpha1Client]):
|
|
|
61
76
|
def from_dict(dict: dict):
|
|
62
77
|
return V1Alpha1ClientList(items=[V1Alpha1Client.from_dict(c) for c in dict.get("items", [])])
|
|
63
78
|
|
|
79
|
+
@classmethod
|
|
80
|
+
def rich_add_columns(cls, table):
|
|
81
|
+
V1Alpha1Client.rich_add_columns(table)
|
|
82
|
+
|
|
83
|
+
def rich_add_rows(self, table):
|
|
84
|
+
for client in self.items:
|
|
85
|
+
client.rich_add_rows(table)
|
|
86
|
+
|
|
87
|
+
def rich_add_names(self, names):
|
|
88
|
+
for client in self.items:
|
|
89
|
+
client.rich_add_names(names)
|
|
90
|
+
|
|
64
91
|
|
|
65
92
|
class ClientsV1Alpha1Api(AbstractAsyncCustomObjectApi):
|
|
66
93
|
"""Interact with the clients custom resource API"""
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def minikube_installed(minikube: str) -> bool:
|
|
7
|
+
"""Check if Minikube is installed and available in the PATH."""
|
|
8
|
+
return shutil.which(minikube) is not None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def kind_installed(kind: str) -> bool:
|
|
12
|
+
"""Check if Kind is installed and available in the PATH."""
|
|
13
|
+
return shutil.which(kind) is not None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def run_command(cmd: list[str]) -> Tuple[int, str, str]:
|
|
17
|
+
"""Run a command and return exit code, stdout, stderr"""
|
|
18
|
+
try:
|
|
19
|
+
process = await asyncio.create_subprocess_exec(
|
|
20
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
21
|
+
)
|
|
22
|
+
stdout, stderr = await process.communicate()
|
|
23
|
+
return process.returncode, stdout.decode().strip(), stderr.decode().strip()
|
|
24
|
+
except FileNotFoundError as e:
|
|
25
|
+
raise RuntimeError(f"Command not found: {cmd[0]}") from e
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def run_command_with_output(cmd: list[str]) -> int:
|
|
29
|
+
"""Run a command with real-time output streaming and return exit code"""
|
|
30
|
+
try:
|
|
31
|
+
process = await asyncio.create_subprocess_exec(*cmd)
|
|
32
|
+
return await process.wait()
|
|
33
|
+
except FileNotFoundError as e:
|
|
34
|
+
raise RuntimeError(f"Command not found: {cmd[0]}") from e
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def minikube_cluster_exists(minikube: str, cluster_name: str) -> bool:
|
|
38
|
+
"""Check if a Minikube cluster exists."""
|
|
39
|
+
if not minikube_installed(minikube):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
returncode, _, _ = await run_command([minikube, "status", "-p", cluster_name])
|
|
44
|
+
return returncode == 0
|
|
45
|
+
except RuntimeError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def kind_cluster_exists(kind: str, cluster_name: str) -> bool:
|
|
50
|
+
"""Check if a Kind cluster exists."""
|
|
51
|
+
if not kind_installed(kind):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
returncode, _, _ = await run_command([kind, "get", "kubeconfig", "--name", cluster_name])
|
|
56
|
+
return returncode == 0
|
|
57
|
+
except RuntimeError:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def delete_minikube_cluster(minikube: str, cluster_name: str) -> bool:
|
|
62
|
+
"""Delete a Minikube cluster."""
|
|
63
|
+
if not minikube_installed(minikube):
|
|
64
|
+
raise RuntimeError(f"{minikube} is not installed or not found in PATH.")
|
|
65
|
+
|
|
66
|
+
if not await minikube_cluster_exists(minikube, cluster_name):
|
|
67
|
+
return True # Already deleted, consider it successful
|
|
68
|
+
|
|
69
|
+
returncode = await run_command_with_output([minikube, "delete", "-p", cluster_name])
|
|
70
|
+
|
|
71
|
+
if returncode == 0:
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
raise RuntimeError(f"Failed to delete Minikube cluster '{cluster_name}'")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def delete_kind_cluster(kind: str, cluster_name: str) -> bool:
|
|
78
|
+
"""Delete a Kind cluster."""
|
|
79
|
+
if not kind_installed(kind):
|
|
80
|
+
raise RuntimeError(f"{kind} is not installed or not found in PATH.")
|
|
81
|
+
|
|
82
|
+
if not await kind_cluster_exists(kind, cluster_name):
|
|
83
|
+
return True # Already deleted, consider it successful
|
|
84
|
+
|
|
85
|
+
returncode = await run_command_with_output([kind, "delete", "cluster", "--name", cluster_name])
|
|
86
|
+
|
|
87
|
+
if returncode == 0:
|
|
88
|
+
return True
|
|
89
|
+
else:
|
|
90
|
+
raise RuntimeError(f"Failed to delete Kind cluster '{cluster_name}'")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def create_minikube_cluster(
|
|
94
|
+
minikube: str, cluster_name: str, extra_args: Optional[list[str]] = None, force_recreate: bool = False
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""Create a Minikube cluster."""
|
|
97
|
+
if extra_args is None:
|
|
98
|
+
extra_args = []
|
|
99
|
+
|
|
100
|
+
if not minikube_installed(minikube):
|
|
101
|
+
raise RuntimeError(f"{minikube} is not installed or not found in PATH.")
|
|
102
|
+
|
|
103
|
+
# Check if cluster already exists
|
|
104
|
+
cluster_exists = await minikube_cluster_exists(minikube, cluster_name)
|
|
105
|
+
|
|
106
|
+
if cluster_exists:
|
|
107
|
+
if not force_recreate:
|
|
108
|
+
raise RuntimeError(f"Minikube cluster '{cluster_name}' already exists.")
|
|
109
|
+
else:
|
|
110
|
+
if not await delete_minikube_cluster(minikube, cluster_name):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
has_cpus_flag = any(a == "--cpus" or a.startswith("--cpus=") for a in extra_args)
|
|
114
|
+
if not has_cpus_flag:
|
|
115
|
+
try:
|
|
116
|
+
rc, out, _ = await run_command([minikube, "config", "get", "cpus"])
|
|
117
|
+
has_config_cpus = rc == 0 and out.strip().isdigit() and int(out.strip()) > 0
|
|
118
|
+
except RuntimeError:
|
|
119
|
+
# If we cannot query minikube (e.g., not installed in test env), default CPUs
|
|
120
|
+
has_config_cpus = False
|
|
121
|
+
if not has_config_cpus:
|
|
122
|
+
extra_args.append("--cpus=4")
|
|
123
|
+
|
|
124
|
+
command = [
|
|
125
|
+
minikube,
|
|
126
|
+
"start",
|
|
127
|
+
"--profile",
|
|
128
|
+
cluster_name,
|
|
129
|
+
"--extra-config=apiserver.service-node-port-range=8000-9000",
|
|
130
|
+
]
|
|
131
|
+
command.extend(extra_args)
|
|
132
|
+
|
|
133
|
+
returncode = await run_command_with_output(command)
|
|
134
|
+
|
|
135
|
+
if returncode == 0:
|
|
136
|
+
return True
|
|
137
|
+
else:
|
|
138
|
+
raise RuntimeError(f"Failed to create Minikube cluster '{cluster_name}'")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def create_kind_cluster(
|
|
142
|
+
kind: str, cluster_name: str, extra_args: Optional[list[str]] = None, force_recreate: bool = False
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Create a Kind cluster."""
|
|
145
|
+
if extra_args is None:
|
|
146
|
+
extra_args = []
|
|
147
|
+
|
|
148
|
+
if not kind_installed(kind):
|
|
149
|
+
raise RuntimeError(f"{kind} is not installed or not found in PATH.")
|
|
150
|
+
|
|
151
|
+
# Check if cluster already exists
|
|
152
|
+
cluster_exists = await kind_cluster_exists(kind, cluster_name)
|
|
153
|
+
|
|
154
|
+
if cluster_exists:
|
|
155
|
+
if not force_recreate:
|
|
156
|
+
raise RuntimeError(f"Kind cluster '{cluster_name}' already exists.")
|
|
157
|
+
else:
|
|
158
|
+
if not await delete_kind_cluster(kind, cluster_name):
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
cluster_config = """kind: Cluster
|
|
162
|
+
apiVersion: kind.x-k8s.io/v1alpha4
|
|
163
|
+
kubeadmConfigPatches:
|
|
164
|
+
- |
|
|
165
|
+
kind: ClusterConfiguration
|
|
166
|
+
apiServer:
|
|
167
|
+
extraArgs:
|
|
168
|
+
"service-node-port-range": "3000-32767"
|
|
169
|
+
- |
|
|
170
|
+
kind: InitConfiguration
|
|
171
|
+
nodeRegistration:
|
|
172
|
+
kubeletExtraArgs:
|
|
173
|
+
node-labels: "ingress-ready=true"
|
|
174
|
+
nodes:
|
|
175
|
+
- role: control-plane
|
|
176
|
+
extraPortMappings:
|
|
177
|
+
- containerPort: 80
|
|
178
|
+
hostPort: 5080
|
|
179
|
+
protocol: TCP
|
|
180
|
+
- containerPort: 30010
|
|
181
|
+
hostPort: 8082
|
|
182
|
+
protocol: TCP
|
|
183
|
+
- containerPort: 30011
|
|
184
|
+
hostPort: 8083
|
|
185
|
+
protocol: TCP
|
|
186
|
+
- containerPort: 443
|
|
187
|
+
hostPort: 5443
|
|
188
|
+
protocol: TCP
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
command = [kind, "create", "cluster", "--name", cluster_name, "--config=/dev/stdin"]
|
|
192
|
+
command.extend(extra_args)
|
|
193
|
+
|
|
194
|
+
kind_process = await asyncio.create_subprocess_exec(*command, stdin=asyncio.subprocess.PIPE)
|
|
195
|
+
|
|
196
|
+
await kind_process.communicate(input=cluster_config.encode())
|
|
197
|
+
|
|
198
|
+
if kind_process.returncode == 0:
|
|
199
|
+
return True
|
|
200
|
+
else:
|
|
201
|
+
raise RuntimeError(f"Failed to create Kind cluster '{cluster_name}'")
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from jumpstarter_kubernetes.cluster import (
|
|
7
|
+
create_kind_cluster,
|
|
8
|
+
create_minikube_cluster,
|
|
9
|
+
delete_kind_cluster,
|
|
10
|
+
delete_minikube_cluster,
|
|
11
|
+
kind_cluster_exists,
|
|
12
|
+
kind_installed,
|
|
13
|
+
minikube_cluster_exists,
|
|
14
|
+
minikube_installed,
|
|
15
|
+
run_command,
|
|
16
|
+
run_command_with_output,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestClusterDetection:
|
|
21
|
+
"""Test cluster tool detection functions."""
|
|
22
|
+
|
|
23
|
+
@patch("jumpstarter_kubernetes.cluster.shutil.which")
|
|
24
|
+
def test_kind_installed_true(self, mock_which):
|
|
25
|
+
mock_which.return_value = "/usr/local/bin/kind"
|
|
26
|
+
assert kind_installed("kind") is True
|
|
27
|
+
mock_which.assert_called_once_with("kind")
|
|
28
|
+
|
|
29
|
+
@patch("jumpstarter_kubernetes.cluster.shutil.which")
|
|
30
|
+
def test_kind_installed_false(self, mock_which):
|
|
31
|
+
mock_which.return_value = None
|
|
32
|
+
assert kind_installed("kind") is False
|
|
33
|
+
mock_which.assert_called_once_with("kind")
|
|
34
|
+
|
|
35
|
+
@patch("jumpstarter_kubernetes.cluster.shutil.which")
|
|
36
|
+
def test_minikube_installed_true(self, mock_which):
|
|
37
|
+
mock_which.return_value = "/usr/local/bin/minikube"
|
|
38
|
+
assert minikube_installed("minikube") is True
|
|
39
|
+
mock_which.assert_called_once_with("minikube")
|
|
40
|
+
|
|
41
|
+
@patch("jumpstarter_kubernetes.cluster.shutil.which")
|
|
42
|
+
def test_minikube_installed_false(self, mock_which):
|
|
43
|
+
mock_which.return_value = None
|
|
44
|
+
assert minikube_installed("minikube") is False
|
|
45
|
+
mock_which.assert_called_once_with("minikube")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestCommandExecution:
|
|
49
|
+
"""Test command execution utilities."""
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_run_command_success(self):
|
|
53
|
+
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
54
|
+
mock_process = AsyncMock()
|
|
55
|
+
mock_process.communicate.return_value = (b"output\n", b"")
|
|
56
|
+
mock_process.returncode = 0
|
|
57
|
+
mock_subprocess.return_value = mock_process
|
|
58
|
+
|
|
59
|
+
returncode, stdout, stderr = await run_command(["echo", "test"])
|
|
60
|
+
|
|
61
|
+
assert returncode == 0
|
|
62
|
+
assert stdout == "output"
|
|
63
|
+
assert stderr == ""
|
|
64
|
+
mock_subprocess.assert_called_once_with(
|
|
65
|
+
"echo", "test", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_run_command_failure(self):
|
|
70
|
+
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
71
|
+
mock_process = AsyncMock()
|
|
72
|
+
mock_process.communicate.return_value = (b"", b"error message\n")
|
|
73
|
+
mock_process.returncode = 1
|
|
74
|
+
mock_subprocess.return_value = mock_process
|
|
75
|
+
|
|
76
|
+
returncode, stdout, stderr = await run_command(["false"])
|
|
77
|
+
|
|
78
|
+
assert returncode == 1
|
|
79
|
+
assert stdout == ""
|
|
80
|
+
assert stderr == "error message"
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_run_command_not_found(self):
|
|
84
|
+
with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError("command not found")):
|
|
85
|
+
with pytest.raises(RuntimeError, match="Command not found: nonexistent"):
|
|
86
|
+
await run_command(["nonexistent"])
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_run_command_with_output_success(self):
|
|
90
|
+
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
91
|
+
mock_process = AsyncMock()
|
|
92
|
+
mock_process.wait.return_value = 0
|
|
93
|
+
mock_subprocess.return_value = mock_process
|
|
94
|
+
|
|
95
|
+
returncode = await run_command_with_output(["echo", "test"])
|
|
96
|
+
|
|
97
|
+
assert returncode == 0
|
|
98
|
+
mock_subprocess.assert_called_once_with("echo", "test")
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_run_command_with_output_not_found(self):
|
|
102
|
+
with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError("command not found")):
|
|
103
|
+
with pytest.raises(RuntimeError, match="Command not found: nonexistent"):
|
|
104
|
+
await run_command_with_output(["nonexistent"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestClusterExistence:
|
|
108
|
+
"""Test cluster existence checking functions."""
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
112
|
+
@patch("jumpstarter_kubernetes.cluster.run_command")
|
|
113
|
+
async def test_kind_cluster_exists_true(self, mock_run_command, mock_kind_installed):
|
|
114
|
+
mock_kind_installed.return_value = True
|
|
115
|
+
mock_run_command.return_value = (0, "", "")
|
|
116
|
+
|
|
117
|
+
result = await kind_cluster_exists("kind", "test-cluster")
|
|
118
|
+
|
|
119
|
+
assert result is True
|
|
120
|
+
mock_run_command.assert_called_once_with(["kind", "get", "kubeconfig", "--name", "test-cluster"])
|
|
121
|
+
|
|
122
|
+
@pytest.mark.asyncio
|
|
123
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
124
|
+
@patch("jumpstarter_kubernetes.cluster.run_command")
|
|
125
|
+
async def test_kind_cluster_exists_false(self, mock_run_command, mock_kind_installed):
|
|
126
|
+
mock_kind_installed.return_value = True
|
|
127
|
+
mock_run_command.return_value = (1, "", "cluster not found")
|
|
128
|
+
|
|
129
|
+
result = await kind_cluster_exists("kind", "test-cluster")
|
|
130
|
+
|
|
131
|
+
assert result is False
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
135
|
+
async def test_kind_cluster_exists_kind_not_installed(self, mock_kind_installed):
|
|
136
|
+
mock_kind_installed.return_value = False
|
|
137
|
+
|
|
138
|
+
result = await kind_cluster_exists("kind", "test-cluster")
|
|
139
|
+
|
|
140
|
+
assert result is False
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
144
|
+
@patch("jumpstarter_kubernetes.cluster.run_command")
|
|
145
|
+
async def test_kind_cluster_exists_runtime_error(self, mock_run_command, mock_kind_installed):
|
|
146
|
+
mock_kind_installed.return_value = True
|
|
147
|
+
mock_run_command.side_effect = RuntimeError("Command failed")
|
|
148
|
+
|
|
149
|
+
result = await kind_cluster_exists("kind", "test-cluster")
|
|
150
|
+
|
|
151
|
+
assert result is False
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
155
|
+
@patch("jumpstarter_kubernetes.cluster.run_command")
|
|
156
|
+
async def test_minikube_cluster_exists_true(self, mock_run_command, mock_minikube_installed):
|
|
157
|
+
mock_minikube_installed.return_value = True
|
|
158
|
+
mock_run_command.return_value = (0, "", "")
|
|
159
|
+
|
|
160
|
+
result = await minikube_cluster_exists("minikube", "test-cluster")
|
|
161
|
+
|
|
162
|
+
assert result is True
|
|
163
|
+
mock_run_command.assert_called_once_with(["minikube", "status", "-p", "test-cluster"])
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
167
|
+
async def test_minikube_cluster_exists_minikube_not_installed(self, mock_minikube_installed):
|
|
168
|
+
mock_minikube_installed.return_value = False
|
|
169
|
+
|
|
170
|
+
result = await minikube_cluster_exists("minikube", "test-cluster")
|
|
171
|
+
|
|
172
|
+
assert result is False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestKindClusterOperations:
|
|
176
|
+
"""Test Kind cluster creation and deletion."""
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
180
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
181
|
+
@patch("asyncio.create_subprocess_exec")
|
|
182
|
+
async def test_create_kind_cluster_success(self, mock_subprocess, mock_cluster_exists, mock_kind_installed):
|
|
183
|
+
mock_kind_installed.return_value = True
|
|
184
|
+
mock_cluster_exists.return_value = False
|
|
185
|
+
|
|
186
|
+
mock_process = AsyncMock()
|
|
187
|
+
mock_process.returncode = 0
|
|
188
|
+
mock_process.communicate.return_value = (b"", b"")
|
|
189
|
+
mock_subprocess.return_value = mock_process
|
|
190
|
+
|
|
191
|
+
result = await create_kind_cluster("kind", "test-cluster")
|
|
192
|
+
|
|
193
|
+
assert result is True
|
|
194
|
+
mock_subprocess.assert_called_once()
|
|
195
|
+
args, kwargs = mock_subprocess.call_args
|
|
196
|
+
assert args[0] == "kind"
|
|
197
|
+
assert args[1] == "create"
|
|
198
|
+
assert args[2] == "cluster"
|
|
199
|
+
assert "--name" in args
|
|
200
|
+
assert "test-cluster" in args
|
|
201
|
+
assert kwargs["stdin"] == asyncio.subprocess.PIPE
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
205
|
+
async def test_create_kind_cluster_not_installed(self, mock_kind_installed):
|
|
206
|
+
mock_kind_installed.return_value = False
|
|
207
|
+
|
|
208
|
+
with pytest.raises(RuntimeError, match="kind is not installed"):
|
|
209
|
+
await create_kind_cluster("kind", "test-cluster")
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
213
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
214
|
+
async def test_create_kind_cluster_already_exists(self, mock_cluster_exists, mock_kind_installed):
|
|
215
|
+
mock_kind_installed.return_value = True
|
|
216
|
+
mock_cluster_exists.return_value = True
|
|
217
|
+
|
|
218
|
+
with pytest.raises(RuntimeError, match="Kind cluster 'test-cluster' already exists"):
|
|
219
|
+
await create_kind_cluster("kind", "test-cluster")
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
223
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
224
|
+
@patch("jumpstarter_kubernetes.cluster.delete_kind_cluster")
|
|
225
|
+
@patch("asyncio.create_subprocess_exec")
|
|
226
|
+
async def test_create_kind_cluster_force_recreate(
|
|
227
|
+
self, mock_subprocess, mock_delete, mock_cluster_exists, mock_kind_installed
|
|
228
|
+
):
|
|
229
|
+
mock_kind_installed.return_value = True
|
|
230
|
+
mock_cluster_exists.return_value = True
|
|
231
|
+
mock_delete.return_value = True
|
|
232
|
+
|
|
233
|
+
mock_process = AsyncMock()
|
|
234
|
+
mock_process.returncode = 0
|
|
235
|
+
mock_process.communicate.return_value = (b"", b"")
|
|
236
|
+
mock_subprocess.return_value = mock_process
|
|
237
|
+
|
|
238
|
+
result = await create_kind_cluster("kind", "test-cluster", force_recreate=True)
|
|
239
|
+
|
|
240
|
+
assert result is True
|
|
241
|
+
mock_delete.assert_called_once_with("kind", "test-cluster")
|
|
242
|
+
|
|
243
|
+
@pytest.mark.asyncio
|
|
244
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
245
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
246
|
+
@patch("asyncio.create_subprocess_exec")
|
|
247
|
+
async def test_create_kind_cluster_with_extra_args(self, mock_subprocess, mock_cluster_exists, mock_kind_installed):
|
|
248
|
+
mock_kind_installed.return_value = True
|
|
249
|
+
mock_cluster_exists.return_value = False
|
|
250
|
+
|
|
251
|
+
mock_process = AsyncMock()
|
|
252
|
+
mock_process.returncode = 0
|
|
253
|
+
mock_process.communicate.return_value = (b"", b"")
|
|
254
|
+
mock_subprocess.return_value = mock_process
|
|
255
|
+
|
|
256
|
+
result = await create_kind_cluster("kind", "test-cluster", extra_args=["--verbosity=1"])
|
|
257
|
+
|
|
258
|
+
assert result is True
|
|
259
|
+
args, _ = mock_subprocess.call_args
|
|
260
|
+
assert "--verbosity=1" in args
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
264
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
265
|
+
@patch("jumpstarter_kubernetes.cluster.run_command_with_output")
|
|
266
|
+
async def test_delete_kind_cluster_success(self, mock_run_command, mock_cluster_exists, mock_kind_installed):
|
|
267
|
+
mock_kind_installed.return_value = True
|
|
268
|
+
mock_cluster_exists.return_value = True
|
|
269
|
+
mock_run_command.return_value = 0
|
|
270
|
+
|
|
271
|
+
result = await delete_kind_cluster("kind", "test-cluster")
|
|
272
|
+
|
|
273
|
+
assert result is True
|
|
274
|
+
mock_run_command.assert_called_once_with(["kind", "delete", "cluster", "--name", "test-cluster"])
|
|
275
|
+
|
|
276
|
+
@pytest.mark.asyncio
|
|
277
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
278
|
+
async def test_delete_kind_cluster_not_installed(self, mock_kind_installed):
|
|
279
|
+
mock_kind_installed.return_value = False
|
|
280
|
+
|
|
281
|
+
with pytest.raises(RuntimeError, match="kind is not installed"):
|
|
282
|
+
await delete_kind_cluster("kind", "test-cluster")
|
|
283
|
+
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
@patch("jumpstarter_kubernetes.cluster.kind_installed")
|
|
286
|
+
@patch("jumpstarter_kubernetes.cluster.kind_cluster_exists")
|
|
287
|
+
async def test_delete_kind_cluster_already_deleted(self, mock_cluster_exists, mock_kind_installed):
|
|
288
|
+
mock_kind_installed.return_value = True
|
|
289
|
+
mock_cluster_exists.return_value = False
|
|
290
|
+
|
|
291
|
+
result = await delete_kind_cluster("kind", "test-cluster")
|
|
292
|
+
|
|
293
|
+
assert result is True # Already deleted, consider successful
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestMinikubeClusterOperations:
|
|
297
|
+
"""Test Minikube cluster creation and deletion."""
|
|
298
|
+
|
|
299
|
+
@pytest.mark.asyncio
|
|
300
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
301
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
302
|
+
@patch("jumpstarter_kubernetes.cluster.run_command_with_output")
|
|
303
|
+
async def test_create_minikube_cluster_success(
|
|
304
|
+
self, mock_run_command, mock_cluster_exists, mock_minikube_installed
|
|
305
|
+
):
|
|
306
|
+
mock_minikube_installed.return_value = True
|
|
307
|
+
mock_cluster_exists.return_value = False
|
|
308
|
+
mock_run_command.return_value = 0
|
|
309
|
+
|
|
310
|
+
result = await create_minikube_cluster("minikube", "test-cluster")
|
|
311
|
+
|
|
312
|
+
assert result is True
|
|
313
|
+
mock_run_command.assert_called_once()
|
|
314
|
+
args = mock_run_command.call_args[0][0]
|
|
315
|
+
assert args[0] == "minikube"
|
|
316
|
+
assert args[1] == "start"
|
|
317
|
+
assert "--profile" in args
|
|
318
|
+
assert "test-cluster" in args
|
|
319
|
+
assert "--extra-config=apiserver.service-node-port-range=8000-9000" in args
|
|
320
|
+
|
|
321
|
+
@pytest.mark.asyncio
|
|
322
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
323
|
+
async def test_create_minikube_cluster_not_installed(self, mock_minikube_installed):
|
|
324
|
+
mock_minikube_installed.return_value = False
|
|
325
|
+
|
|
326
|
+
with pytest.raises(RuntimeError, match="minikube is not installed"):
|
|
327
|
+
await create_minikube_cluster("minikube", "test-cluster")
|
|
328
|
+
|
|
329
|
+
@pytest.mark.asyncio
|
|
330
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
331
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
332
|
+
async def test_create_minikube_cluster_already_exists(self, mock_cluster_exists, mock_minikube_installed):
|
|
333
|
+
mock_minikube_installed.return_value = True
|
|
334
|
+
mock_cluster_exists.return_value = True
|
|
335
|
+
|
|
336
|
+
with pytest.raises(RuntimeError, match="Minikube cluster 'test-cluster' already exists"):
|
|
337
|
+
await create_minikube_cluster("minikube", "test-cluster")
|
|
338
|
+
|
|
339
|
+
@pytest.mark.asyncio
|
|
340
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
341
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
342
|
+
@patch("jumpstarter_kubernetes.cluster.run_command_with_output")
|
|
343
|
+
async def test_create_minikube_cluster_with_extra_args(
|
|
344
|
+
self, mock_run_command, mock_cluster_exists, mock_minikube_installed
|
|
345
|
+
):
|
|
346
|
+
mock_minikube_installed.return_value = True
|
|
347
|
+
mock_cluster_exists.return_value = False
|
|
348
|
+
mock_run_command.return_value = 0
|
|
349
|
+
|
|
350
|
+
result = await create_minikube_cluster("minikube", "test-cluster", extra_args=["--memory=4096"])
|
|
351
|
+
|
|
352
|
+
assert result is True
|
|
353
|
+
args = mock_run_command.call_args[0][0]
|
|
354
|
+
assert "--memory=4096" in args
|
|
355
|
+
|
|
356
|
+
@pytest.mark.asyncio
|
|
357
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
358
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
359
|
+
@patch("jumpstarter_kubernetes.cluster.run_command_with_output")
|
|
360
|
+
async def test_delete_minikube_cluster_success(
|
|
361
|
+
self, mock_run_command, mock_cluster_exists, mock_minikube_installed
|
|
362
|
+
):
|
|
363
|
+
mock_minikube_installed.return_value = True
|
|
364
|
+
mock_cluster_exists.return_value = True
|
|
365
|
+
mock_run_command.return_value = 0
|
|
366
|
+
|
|
367
|
+
result = await delete_minikube_cluster("minikube", "test-cluster")
|
|
368
|
+
|
|
369
|
+
assert result is True
|
|
370
|
+
mock_run_command.assert_called_once_with(["minikube", "delete", "-p", "test-cluster"])
|
|
371
|
+
|
|
372
|
+
@pytest.mark.asyncio
|
|
373
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
374
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
375
|
+
async def test_delete_minikube_cluster_already_deleted(self, mock_cluster_exists, mock_minikube_installed):
|
|
376
|
+
mock_minikube_installed.return_value = True
|
|
377
|
+
mock_cluster_exists.return_value = False
|
|
378
|
+
|
|
379
|
+
result = await delete_minikube_cluster("minikube", "test-cluster")
|
|
380
|
+
|
|
381
|
+
assert result is True # Already deleted, consider successful
|
|
382
|
+
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_installed")
|
|
385
|
+
@patch("jumpstarter_kubernetes.cluster.minikube_cluster_exists")
|
|
386
|
+
@patch("jumpstarter_kubernetes.cluster.run_command_with_output")
|
|
387
|
+
async def test_delete_minikube_cluster_failure(
|
|
388
|
+
self, mock_run_command, mock_cluster_exists, mock_minikube_installed
|
|
389
|
+
):
|
|
390
|
+
mock_minikube_installed.return_value = True
|
|
391
|
+
mock_cluster_exists.return_value = True
|
|
392
|
+
mock_run_command.return_value = 1
|
|
393
|
+
|
|
394
|
+
with pytest.raises(RuntimeError, match="Failed to delete Minikube cluster 'test-cluster'"):
|
|
395
|
+
await delete_minikube_cluster("minikube", "test-cluster")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def time_since(t_str: str):
|
|
5
|
+
# Format the elapsed time in a readable way
|
|
6
|
+
t = datetime.strptime(t_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
7
|
+
now = datetime.now(timezone.utc)
|
|
8
|
+
elapsed = now - t
|
|
9
|
+
|
|
10
|
+
# Format the elapsed time in a readable way
|
|
11
|
+
if elapsed.total_seconds() < 60:
|
|
12
|
+
return f"{int(elapsed.total_seconds())}s"
|
|
13
|
+
elif elapsed.total_seconds() < 3600:
|
|
14
|
+
minutes = int(elapsed.total_seconds() // 60)
|
|
15
|
+
seconds = int(elapsed.total_seconds() % 60)
|
|
16
|
+
return f"{minutes}m{seconds}s" if seconds > 0 else f"{minutes}m"
|
|
17
|
+
elif elapsed.total_seconds() < 86400:
|
|
18
|
+
hours = int(elapsed.total_seconds() // 3600)
|
|
19
|
+
minutes = int((elapsed.total_seconds() % 3600) // 60)
|
|
20
|
+
return f"{hours}h{minutes}m" if minutes > 0 and hours < 2 else f"{hours}h"
|
|
21
|
+
elif elapsed.total_seconds() < 2592000:
|
|
22
|
+
days = elapsed.days
|
|
23
|
+
hours = int((elapsed.total_seconds() % 86400) // 3600)
|
|
24
|
+
return f"{days}d{hours}h" if hours > 0 else f"{days}d"
|
|
25
|
+
elif elapsed.total_seconds() < 31536000:
|
|
26
|
+
months = int(elapsed.days / 30)
|
|
27
|
+
days = elapsed.days % 30
|
|
28
|
+
return f"{months}mo{days}d" if days > 0 else f"{months}mo"
|
|
29
|
+
else:
|
|
30
|
+
years = int(elapsed.days / 365)
|
|
31
|
+
months = int((elapsed.days % 365) / 30)
|
|
32
|
+
return f"{years}y{months}mo" if months > 0 else f"{years}y"
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/exporters.py
RENAMED
|
@@ -5,6 +5,7 @@ from typing import Literal
|
|
|
5
5
|
from kubernetes_asyncio.client.models import V1ObjectMeta, V1ObjectReference
|
|
6
6
|
from pydantic import Field
|
|
7
7
|
|
|
8
|
+
from .datetime import time_since
|
|
8
9
|
from .json import JsonBaseModel
|
|
9
10
|
from .list import V1Alpha1List
|
|
10
11
|
from .serialize import SerializeV1ObjectMeta, SerializeV1ObjectReference
|
|
@@ -57,6 +58,47 @@ class V1Alpha1Exporter(JsonBaseModel):
|
|
|
57
58
|
),
|
|
58
59
|
)
|
|
59
60
|
|
|
61
|
+
@classmethod
|
|
62
|
+
def rich_add_columns(cls, table, devices: bool = False):
|
|
63
|
+
if devices:
|
|
64
|
+
table.add_column("NAME")
|
|
65
|
+
table.add_column("ENDPOINT")
|
|
66
|
+
table.add_column("AGE")
|
|
67
|
+
table.add_column("LABELS")
|
|
68
|
+
table.add_column("UUID")
|
|
69
|
+
else:
|
|
70
|
+
table.add_column("NAME")
|
|
71
|
+
table.add_column("ENDPOINT")
|
|
72
|
+
table.add_column("DEVICES")
|
|
73
|
+
table.add_column("AGE")
|
|
74
|
+
|
|
75
|
+
def rich_add_rows(self, table, devices: bool = False):
|
|
76
|
+
if devices:
|
|
77
|
+
if self.status is not None:
|
|
78
|
+
for d in self.status.devices:
|
|
79
|
+
labels = []
|
|
80
|
+
if d.labels is not None:
|
|
81
|
+
for label in d.labels:
|
|
82
|
+
labels.append(f"{label}:{str(d.labels[label])}")
|
|
83
|
+
table.add_row(
|
|
84
|
+
self.metadata.name,
|
|
85
|
+
self.status.endpoint,
|
|
86
|
+
time_since(self.metadata.creation_timestamp),
|
|
87
|
+
",".join(labels),
|
|
88
|
+
d.uuid,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
table.add_row(
|
|
93
|
+
self.metadata.name,
|
|
94
|
+
self.status.endpoint,
|
|
95
|
+
str(len(self.status.devices) if self.status and self.status.devices else 0),
|
|
96
|
+
time_since(self.metadata.creation_timestamp),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def rich_add_names(self, names):
|
|
100
|
+
names.append(f"exporter.jumpstarter.dev/{self.metadata.name}")
|
|
101
|
+
|
|
60
102
|
|
|
61
103
|
class V1Alpha1ExporterList(V1Alpha1List[V1Alpha1Exporter]):
|
|
62
104
|
kind: Literal["ExporterList"] = Field(default="ExporterList")
|
|
@@ -65,6 +107,18 @@ class V1Alpha1ExporterList(V1Alpha1List[V1Alpha1Exporter]):
|
|
|
65
107
|
def from_dict(dict: dict):
|
|
66
108
|
return V1Alpha1ExporterList(items=[V1Alpha1Exporter.from_dict(c) for c in dict["items"]])
|
|
67
109
|
|
|
110
|
+
@classmethod
|
|
111
|
+
def rich_add_columns(cls, table, **kwargs):
|
|
112
|
+
V1Alpha1Exporter.rich_add_columns(table, **kwargs)
|
|
113
|
+
|
|
114
|
+
def rich_add_rows(self, table, **kwargs):
|
|
115
|
+
for exporter in self.items:
|
|
116
|
+
exporter.rich_add_rows(table, **kwargs)
|
|
117
|
+
|
|
118
|
+
def rich_add_names(self, names):
|
|
119
|
+
for exporter in self.items:
|
|
120
|
+
exporter.rich_add_names(names)
|
|
121
|
+
|
|
68
122
|
|
|
69
123
|
class ExportersV1Alpha1Api(AbstractAsyncCustomObjectApi):
|
|
70
124
|
"""Interact with the exporters custom resource API"""
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/install.py
RENAMED
|
@@ -4,6 +4,7 @@ from typing import Literal, Optional
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def helm_installed(name: str) -> bool:
|
|
7
|
+
"""Check if Helm is installed and available in the PATH."""
|
|
7
8
|
return shutil.which(name) is not None
|
|
8
9
|
|
|
9
10
|
|
|
@@ -20,8 +21,6 @@ async def install_helm_chart(
|
|
|
20
21
|
context: Optional[str],
|
|
21
22
|
helm: Optional[str] = "helm",
|
|
22
23
|
):
|
|
23
|
-
grpc_port = grpc_endpoint.split(":")[1]
|
|
24
|
-
router_port = router_endpoint.split(":")[1]
|
|
25
24
|
args = [
|
|
26
25
|
helm,
|
|
27
26
|
"upgrade",
|
|
@@ -42,10 +41,6 @@ async def install_helm_chart(
|
|
|
42
41
|
"--set",
|
|
43
42
|
f"jumpstarter-controller.grpc.nodeport.enabled={'true' if mode == 'nodeport' else 'false'}",
|
|
44
43
|
"--set",
|
|
45
|
-
f"jumpstarter-controller.grpc.nodeport.port={grpc_port}",
|
|
46
|
-
"--set",
|
|
47
|
-
f"jumpstarter-controller.grpc.nodeport.routerPort={router_port}",
|
|
48
|
-
"--set",
|
|
49
44
|
f"jumpstarter-controller.grpc.mode={mode}",
|
|
50
45
|
"--version",
|
|
51
46
|
version,
|
|
@@ -63,3 +58,28 @@ async def install_helm_chart(
|
|
|
63
58
|
# Attempt to install Jumpstarter using Helm
|
|
64
59
|
process = await asyncio.create_subprocess_exec(*args)
|
|
65
60
|
await process.wait()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def uninstall_helm_chart(
|
|
64
|
+
name: str, namespace: str, kubeconfig: Optional[str], context: Optional[str], helm: Optional[str] = "helm"
|
|
65
|
+
):
|
|
66
|
+
args = [
|
|
67
|
+
helm,
|
|
68
|
+
"uninstall",
|
|
69
|
+
name,
|
|
70
|
+
"--namespace",
|
|
71
|
+
namespace,
|
|
72
|
+
"--wait",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
if kubeconfig is not None:
|
|
76
|
+
args.append("--kubeconfig")
|
|
77
|
+
args.append(kubeconfig)
|
|
78
|
+
|
|
79
|
+
if context is not None:
|
|
80
|
+
args.append("--kube-context")
|
|
81
|
+
args.append(context)
|
|
82
|
+
|
|
83
|
+
# Attempt to install Jumpstarter using Helm
|
|
84
|
+
process = await asyncio.create_subprocess_exec(*args)
|
|
85
|
+
await process.wait()
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/leases.py
RENAMED
|
@@ -3,6 +3,7 @@ from typing import Literal, Optional
|
|
|
3
3
|
from kubernetes_asyncio.client.models import V1Condition, V1ObjectMeta, V1ObjectReference
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
|
+
from .datetime import time_since
|
|
6
7
|
from .json import JsonBaseModel
|
|
7
8
|
from .list import V1Alpha1List
|
|
8
9
|
from .serialize import SerializeV1Condition, SerializeV1ObjectMeta, SerializeV1ObjectReference
|
|
@@ -76,6 +77,56 @@ class V1Alpha1Lease(JsonBaseModel):
|
|
|
76
77
|
),
|
|
77
78
|
)
|
|
78
79
|
|
|
80
|
+
@classmethod
|
|
81
|
+
def rich_add_columns(cls, table):
|
|
82
|
+
table.add_column("NAME")
|
|
83
|
+
table.add_column("CLIENT")
|
|
84
|
+
table.add_column("SELECTOR")
|
|
85
|
+
table.add_column("EXPORTER")
|
|
86
|
+
table.add_column("DURATION")
|
|
87
|
+
table.add_column("STATUS")
|
|
88
|
+
table.add_column("REASON")
|
|
89
|
+
table.add_column("BEGIN")
|
|
90
|
+
table.add_column("END")
|
|
91
|
+
table.add_column("AGE")
|
|
92
|
+
|
|
93
|
+
def get_reason(self):
|
|
94
|
+
condition = self.status.conditions[-1] if len(self.status.conditions) > 0 else None
|
|
95
|
+
reason = condition.reason if condition is not None else "Unknown"
|
|
96
|
+
status = condition.status if condition is not None else "False"
|
|
97
|
+
if reason == "Ready":
|
|
98
|
+
if status == "True":
|
|
99
|
+
return "Ready"
|
|
100
|
+
else:
|
|
101
|
+
return "Waiting"
|
|
102
|
+
elif reason == "Expired":
|
|
103
|
+
if status == "True":
|
|
104
|
+
return "Expired"
|
|
105
|
+
else:
|
|
106
|
+
return "Complete"
|
|
107
|
+
else:
|
|
108
|
+
return reason
|
|
109
|
+
|
|
110
|
+
def rich_add_rows(self, table):
|
|
111
|
+
selectors = []
|
|
112
|
+
for label in self.spec.selector.match_labels:
|
|
113
|
+
selectors.append(f"{label}:{str(self.spec.selector.match_labels[label])}")
|
|
114
|
+
table.add_row(
|
|
115
|
+
self.metadata.name,
|
|
116
|
+
self.spec.client.name if self.spec.client is not None else "",
|
|
117
|
+
",".join(selectors) if len(selectors) > 0 else "*",
|
|
118
|
+
self.status.exporter.name if self.status.exporter is not None else "",
|
|
119
|
+
self.spec.duration,
|
|
120
|
+
"Ended" if self.status.ended else "InProgress",
|
|
121
|
+
self.get_reason(),
|
|
122
|
+
self.status.begin_time if self.status.begin_time is not None else "",
|
|
123
|
+
self.status.end_time if self.status.end_time is not None else "",
|
|
124
|
+
time_since(self.metadata.creation_timestamp),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def rich_add_names(self, names):
|
|
128
|
+
names.append(f"lease.jumpstarter.dev/{self.metadata.name}")
|
|
129
|
+
|
|
79
130
|
|
|
80
131
|
class V1Alpha1LeaseList(V1Alpha1List[V1Alpha1Lease]):
|
|
81
132
|
kind: Literal["LeaseList"] = Field(default="LeaseList")
|
|
@@ -84,6 +135,18 @@ class V1Alpha1LeaseList(V1Alpha1List[V1Alpha1Lease]):
|
|
|
84
135
|
def from_dict(dict: dict):
|
|
85
136
|
return V1Alpha1LeaseList(items=[V1Alpha1Lease.from_dict(c) for c in dict["items"]])
|
|
86
137
|
|
|
138
|
+
@classmethod
|
|
139
|
+
def rich_add_columns(cls, table):
|
|
140
|
+
V1Alpha1Lease.rich_add_columns(table)
|
|
141
|
+
|
|
142
|
+
def rich_add_rows(self, table):
|
|
143
|
+
for lease in self.items:
|
|
144
|
+
lease.rich_add_rows(table)
|
|
145
|
+
|
|
146
|
+
def rich_add_names(self, names):
|
|
147
|
+
for lease in self.items:
|
|
148
|
+
lease.rich_add_names(names)
|
|
149
|
+
|
|
87
150
|
|
|
88
151
|
class LeasesV1Alpha1Api(AbstractAsyncCustomObjectApi):
|
|
89
152
|
"""Interact with the leases custom resource API"""
|
|
File without changes
|
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/clients_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/json.py
RENAMED
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/list.py
RENAMED
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/py.typed
RENAMED
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/serialize.py
RENAMED
|
File without changes
|
{jumpstarter_kubernetes-0.6.0 → jumpstarter_kubernetes-0.7.1}/jumpstarter_kubernetes/test_leases.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|