jumpstarter-kubernetes 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jumpstarter-kubernetes might be problematic. Click here for more details.

@@ -0,0 +1,39 @@
1
+ from .clients import ClientsV1Alpha1Api, V1Alpha1Client, V1Alpha1ClientList, V1Alpha1ClientStatus
2
+ from .exporters import (
3
+ ExportersV1Alpha1Api,
4
+ V1Alpha1Exporter,
5
+ V1Alpha1ExporterDevice,
6
+ V1Alpha1ExporterList,
7
+ V1Alpha1ExporterStatus,
8
+ )
9
+ from .install import helm_installed, install_helm_chart
10
+ from .leases import (
11
+ LeasesV1Alpha1Api,
12
+ V1Alpha1Lease,
13
+ V1Alpha1LeaseList,
14
+ V1Alpha1LeaseSelector,
15
+ V1Alpha1LeaseSpec,
16
+ V1Alpha1LeaseStatus,
17
+ )
18
+ from .list import V1Alpha1List
19
+
20
+ __all__ = [
21
+ "ClientsV1Alpha1Api",
22
+ "V1Alpha1Client",
23
+ "V1Alpha1ClientList",
24
+ "V1Alpha1ClientStatus",
25
+ "ExportersV1Alpha1Api",
26
+ "V1Alpha1Exporter",
27
+ "V1Alpha1ExporterList",
28
+ "V1Alpha1ExporterStatus",
29
+ "V1Alpha1ExporterDevice",
30
+ "LeasesV1Alpha1Api",
31
+ "V1Alpha1Lease",
32
+ "V1Alpha1LeaseStatus",
33
+ "V1Alpha1LeaseList",
34
+ "V1Alpha1LeaseSelector",
35
+ "V1Alpha1LeaseSpec",
36
+ "V1Alpha1List",
37
+ "helm_installed",
38
+ "install_helm_chart",
39
+ ]
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ import base64
3
+ import logging
4
+ from typing import Literal, Optional
5
+
6
+ from kubernetes_asyncio.client.models import V1ObjectMeta, V1ObjectReference
7
+ from pydantic import Field
8
+
9
+ from .json import JsonBaseModel
10
+ from .list import V1Alpha1List
11
+ from .serialize import SerializeV1ObjectMeta, SerializeV1ObjectReference
12
+ from .util import AbstractAsyncCustomObjectApi
13
+ from jumpstarter.config.client import ClientConfigV1Alpha1, ClientConfigV1Alpha1Drivers
14
+ from jumpstarter.config.common import ObjectMeta
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ CREATE_CLIENT_DELAY = 1
19
+ CREATE_CLIENT_COUNT = 10
20
+
21
+
22
+ class V1Alpha1ClientStatus(JsonBaseModel):
23
+ credential: Optional[SerializeV1ObjectReference] = None
24
+ endpoint: str
25
+
26
+
27
+ class V1Alpha1Client(JsonBaseModel):
28
+ api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1")
29
+ kind: Literal["Client"] = Field(default="Client")
30
+ metadata: SerializeV1ObjectMeta
31
+ status: Optional[V1Alpha1ClientStatus]
32
+
33
+ @staticmethod
34
+ def from_dict(dict: dict):
35
+ return V1Alpha1Client(
36
+ api_version=dict["apiVersion"],
37
+ kind=dict["kind"],
38
+ metadata=V1ObjectMeta(
39
+ creation_timestamp=dict["metadata"]["creationTimestamp"],
40
+ generation=dict["metadata"]["generation"],
41
+ name=dict["metadata"]["name"],
42
+ namespace=dict["metadata"]["namespace"],
43
+ resource_version=dict["metadata"]["resourceVersion"],
44
+ uid=dict["metadata"]["uid"],
45
+ ),
46
+ status=V1Alpha1ClientStatus(
47
+ credential=V1ObjectReference(name=dict["status"]["credential"]["name"])
48
+ if "credential" in dict["status"]
49
+ else None,
50
+ endpoint=dict["status"].get("endpoint", ""),
51
+ )
52
+ if "status" in dict
53
+ else None,
54
+ )
55
+
56
+
57
+ class V1Alpha1ClientList(V1Alpha1List[V1Alpha1Client]):
58
+ kind: Literal["ClientList"] = Field(default="ClientList")
59
+
60
+ @staticmethod
61
+ def from_dict(dict: dict):
62
+ return V1Alpha1ClientList(items=[V1Alpha1Client.from_dict(c) for c in dict.get("items", [])])
63
+
64
+
65
+ class ClientsV1Alpha1Api(AbstractAsyncCustomObjectApi):
66
+ """Interact with the clients custom resource API"""
67
+
68
+ async def create_client(
69
+ self, name: str, labels: dict[str, str] | None = None, oidc_username: str | None = None
70
+ ) -> V1Alpha1Client:
71
+ """Create a client object in the cluster async"""
72
+ # Create the namespaced client object
73
+ await self.api.create_namespaced_custom_object(
74
+ namespace=self.namespace,
75
+ group="jumpstarter.dev",
76
+ plural="clients",
77
+ version="v1alpha1",
78
+ body={
79
+ "apiVersion": "jumpstarter.dev/v1alpha1",
80
+ "kind": "Client",
81
+ "metadata": {"name": name} | {"labels": labels} if labels is not None else {},
82
+ "spec": {"username": oidc_username} if oidc_username is not None else {},
83
+ },
84
+ )
85
+ # Wait for the credentials to become available
86
+ # NOTE: Watch is not working here with the Python kubernetes library
87
+ count = 0
88
+ updated_client = {}
89
+ # Retry for a maximum of 10s
90
+ while count < CREATE_CLIENT_COUNT:
91
+ # Try to get the updated client resource
92
+ updated_client = await self.api.get_namespaced_custom_object(
93
+ namespace=self.namespace, group="jumpstarter.dev", plural="clients", version="v1alpha1", name=name
94
+ )
95
+ # check if the client status is updated with the credentials
96
+ if "status" in updated_client:
97
+ if "credential" in updated_client["status"]:
98
+ return V1Alpha1Client.from_dict(updated_client)
99
+ count += 1
100
+ await asyncio.sleep(CREATE_CLIENT_DELAY)
101
+ raise Exception("Timeout waiting for client credentials")
102
+
103
+ async def list_clients(self) -> V1Alpha1List[V1Alpha1Client]:
104
+ """List the client objects in the cluster async"""
105
+ res = await self.api.list_namespaced_custom_object(
106
+ namespace=self.namespace, group="jumpstarter.dev", plural="clients", version="v1alpha1"
107
+ )
108
+ return V1Alpha1ClientList.from_dict(res)
109
+
110
+ async def get_client(self, name: str) -> V1Alpha1Client:
111
+ """Get a single client object from the cluster async"""
112
+ result = await self.api.get_namespaced_custom_object(
113
+ namespace=self.namespace, group="jumpstarter.dev", plural="clients", version="v1alpha1", name=name
114
+ )
115
+ return V1Alpha1Client.from_dict(result)
116
+
117
+ async def get_client_config(self, name: str, allow: list[str], unsafe=False) -> ClientConfigV1Alpha1:
118
+ """Get a client config for a specified client name"""
119
+ client = await self.get_client(name)
120
+ secret = await self.core_api.read_namespaced_secret(client.status.credential.name, self.namespace)
121
+ endpoint = client.status.endpoint
122
+ token = base64.b64decode(secret.data["token"]).decode("utf8")
123
+ return ClientConfigV1Alpha1(
124
+ alias=name,
125
+ metadata=ObjectMeta(
126
+ namespace=client.metadata.namespace,
127
+ name=client.metadata.name,
128
+ ),
129
+ endpoint=endpoint,
130
+ token=token,
131
+ drivers=ClientConfigV1Alpha1Drivers(allow=allow, unsafe=unsafe),
132
+ )
133
+
134
+ async def delete_client(self, name: str):
135
+ """Delete a client object"""
136
+ await self.api.delete_namespaced_custom_object(
137
+ namespace=self.namespace, group="jumpstarter.dev", plural="clients", version="v1alpha1", name=name
138
+ )
@@ -0,0 +1,58 @@
1
+ from kubernetes_asyncio.client.models import V1ObjectMeta
2
+
3
+ from jumpstarter_kubernetes import V1Alpha1Client, V1Alpha1ClientStatus
4
+
5
+ TEST_CLIENT = V1Alpha1Client(
6
+ api_version="jumpstarter.dev/v1alpha1",
7
+ kind="Client",
8
+ metadata=V1ObjectMeta(
9
+ creation_timestamp="2021-10-01T00:00:00Z",
10
+ generation=1,
11
+ name="test-client",
12
+ namespace="default",
13
+ resource_version="1",
14
+ uid="7a25eb81-6443-47ec-a62f-50165bffede8",
15
+ ),
16
+ status=V1Alpha1ClientStatus(credential=None, endpoint="https://test-client"),
17
+ )
18
+
19
+
20
+ def test_client_dump_json():
21
+ assert (
22
+ TEST_CLIENT.dump_json()
23
+ == """{
24
+ "apiVersion": "jumpstarter.dev/v1alpha1",
25
+ "kind": "Client",
26
+ "metadata": {
27
+ "creationTimestamp": "2021-10-01T00:00:00Z",
28
+ "generation": 1,
29
+ "name": "test-client",
30
+ "namespace": "default",
31
+ "resourceVersion": "1",
32
+ "uid": "7a25eb81-6443-47ec-a62f-50165bffede8"
33
+ },
34
+ "status": {
35
+ "credential": null,
36
+ "endpoint": "https://test-client"
37
+ }
38
+ }"""
39
+ )
40
+
41
+
42
+ def test_client_dump_yaml():
43
+ assert (
44
+ TEST_CLIENT.dump_yaml()
45
+ == """apiVersion: jumpstarter.dev/v1alpha1
46
+ kind: Client
47
+ metadata:
48
+ creationTimestamp: '2021-10-01T00:00:00Z'
49
+ generation: 1
50
+ name: test-client
51
+ namespace: default
52
+ resourceVersion: '1'
53
+ uid: 7a25eb81-6443-47ec-a62f-50165bffede8
54
+ status:
55
+ credential: null
56
+ endpoint: https://test-client
57
+ """
58
+ )
@@ -0,0 +1,146 @@
1
+ import asyncio
2
+ import base64
3
+ from typing import Literal
4
+
5
+ from kubernetes_asyncio.client.models import V1ObjectMeta, V1ObjectReference
6
+ from pydantic import Field
7
+
8
+ from .json import JsonBaseModel
9
+ from .list import V1Alpha1List
10
+ from .serialize import SerializeV1ObjectMeta, SerializeV1ObjectReference
11
+ from .util import AbstractAsyncCustomObjectApi
12
+ from jumpstarter.config.common import ObjectMeta
13
+ from jumpstarter.config.exporter import ExporterConfigV1Alpha1
14
+
15
+ CREATE_EXPORTER_DELAY = 1
16
+ CREATE_EXPORTER_COUNT = 10
17
+
18
+
19
+ class V1Alpha1ExporterDevice(JsonBaseModel):
20
+ labels: dict[str, str]
21
+ uuid: str
22
+
23
+
24
+ class V1Alpha1ExporterStatus(JsonBaseModel):
25
+ credential: SerializeV1ObjectReference
26
+ devices: list[V1Alpha1ExporterDevice]
27
+ endpoint: str
28
+
29
+
30
+ class V1Alpha1Exporter(JsonBaseModel):
31
+ api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1")
32
+ kind: Literal["Exporter"] = Field(default="Exporter")
33
+ metadata: SerializeV1ObjectMeta
34
+ status: V1Alpha1ExporterStatus
35
+
36
+ @staticmethod
37
+ def from_dict(dict: dict):
38
+ return V1Alpha1Exporter(
39
+ api_version=dict["apiVersion"],
40
+ kind=dict["kind"],
41
+ metadata=V1ObjectMeta(
42
+ creation_timestamp=dict["metadata"]["creationTimestamp"],
43
+ generation=dict["metadata"]["generation"],
44
+ name=dict["metadata"]["name"],
45
+ namespace=dict["metadata"]["namespace"],
46
+ resource_version=dict["metadata"]["resourceVersion"],
47
+ uid=dict["metadata"]["uid"],
48
+ ),
49
+ status=V1Alpha1ExporterStatus(
50
+ credential=V1ObjectReference(name=dict["status"]["credential"]["name"])
51
+ if "credential" in dict["status"]
52
+ else None,
53
+ endpoint=dict["status"]["endpoint"],
54
+ devices=[V1Alpha1ExporterDevice(labels=d["labels"], uuid=d["uuid"]) for d in dict["status"]["devices"]]
55
+ if "devices" in dict["status"]
56
+ else [],
57
+ ),
58
+ )
59
+
60
+
61
+ class V1Alpha1ExporterList(V1Alpha1List[V1Alpha1Exporter]):
62
+ kind: Literal["ExporterList"] = Field(default="ExporterList")
63
+
64
+ @staticmethod
65
+ def from_dict(dict: dict):
66
+ return V1Alpha1ExporterList(items=[V1Alpha1Exporter.from_dict(c) for c in dict["items"]])
67
+
68
+
69
+ class ExportersV1Alpha1Api(AbstractAsyncCustomObjectApi):
70
+ """Interact with the exporters custom resource API"""
71
+
72
+ async def list_exporters(self) -> V1Alpha1List[V1Alpha1Exporter]:
73
+ """List the exporter objects in the cluster"""
74
+ res = await self.api.list_namespaced_custom_object(
75
+ namespace=self.namespace, group="jumpstarter.dev", plural="exporters", version="v1alpha1"
76
+ )
77
+ return V1Alpha1ExporterList.from_dict(res)
78
+
79
+ async def get_exporter(self, name: str) -> V1Alpha1Exporter:
80
+ """Get a single exporter object from the cluster"""
81
+ result = await self.api.get_namespaced_custom_object(
82
+ namespace=self.namespace, group="jumpstarter.dev", plural="exporters", version="v1alpha1", name=name
83
+ )
84
+ return V1Alpha1Exporter.from_dict(result)
85
+
86
+ async def create_exporter(
87
+ self, name: str, labels: dict[str, str] | None = None, oidc_username: str | None = None
88
+ ) -> V1Alpha1Exporter:
89
+ """Create an exporter in the cluster"""
90
+ # Create the namespaced exporter object
91
+ await self.api.create_namespaced_custom_object(
92
+ namespace=self.namespace,
93
+ group="jumpstarter.dev",
94
+ plural="exporters",
95
+ version="v1alpha1",
96
+ body={
97
+ "apiVersion": "jumpstarter.dev/v1alpha1",
98
+ "kind": "Exporter",
99
+ "metadata": {"name": name} | {"labels": labels} if labels is not None else {},
100
+ "spec": {"username": oidc_username} if oidc_username is not None else {},
101
+ },
102
+ )
103
+ # Wait for the credentials to become available
104
+ # NOTE: Watch is not working here with the Python kubernetes library
105
+ count = 0
106
+ updated_exporter = {}
107
+ # Retry for a maximum of 10s
108
+ while count < CREATE_EXPORTER_COUNT:
109
+ # Try to get the updated client resource
110
+ updated_exporter = await self.api.get_namespaced_custom_object(
111
+ namespace=self.namespace, group="jumpstarter.dev", plural="exporters", version="v1alpha1", name=name
112
+ )
113
+ # check if the client status is updated with the credentials
114
+ if "status" in updated_exporter:
115
+ if "credential" in updated_exporter["status"]:
116
+ return V1Alpha1Exporter.from_dict(updated_exporter)
117
+ count += 1
118
+ await asyncio.sleep(CREATE_EXPORTER_DELAY)
119
+ raise Exception("Timeout waiting for exporter credentials")
120
+
121
+ async def get_exporter_config(self, name: str) -> ExporterConfigV1Alpha1:
122
+ """Get an exporter config for a specified exporter name"""
123
+ exporter = await self.get_exporter(name)
124
+ secret = await self.core_api.read_namespaced_secret(exporter.status.credential.name, self.namespace)
125
+ endpoint = exporter.status.endpoint
126
+ token = base64.b64decode(secret.data["token"]).decode("utf8")
127
+ return ExporterConfigV1Alpha1(
128
+ alias=name,
129
+ metadata=ObjectMeta(
130
+ namespace=exporter.metadata.namespace,
131
+ name=exporter.metadata.name,
132
+ ),
133
+ endpoint=endpoint,
134
+ token=token,
135
+ export={},
136
+ )
137
+
138
+ async def delete_exporter(self, name: str):
139
+ """Delete an exporter object"""
140
+ await self.api.delete_namespaced_custom_object(
141
+ namespace=self.namespace,
142
+ name=name,
143
+ group="jumpstarter.dev",
144
+ plural="exporters",
145
+ version="v1alpha1",
146
+ )
@@ -0,0 +1,77 @@
1
+ from kubernetes_asyncio.client.models import V1ObjectMeta, V1ObjectReference
2
+
3
+ from jumpstarter_kubernetes.exporters import V1Alpha1Exporter, V1Alpha1ExporterDevice, V1Alpha1ExporterStatus
4
+
5
+ TEST_EXPORTER = V1Alpha1Exporter(
6
+ api_version="jumpstarter.dev/v1alpha1",
7
+ kind="Exporter",
8
+ metadata=V1ObjectMeta(
9
+ creation_timestamp="2021-10-01T00:00:00Z",
10
+ generation=1,
11
+ name="test-exporter",
12
+ namespace="default",
13
+ resource_version="1",
14
+ uid="7a25eb81-6443-47ec-a62f-50165bffede8",
15
+ ),
16
+ status=V1Alpha1ExporterStatus(
17
+ credential=V1ObjectReference(name="test-credential"),
18
+ devices=[V1Alpha1ExporterDevice(labels={"test": "label"}, uuid="f4cf49ab-fc64-46c6-94e7-a40502eb77b1")],
19
+ endpoint="https://test-exporter",
20
+ ),
21
+ )
22
+
23
+
24
+ def test_exporter_dump_json():
25
+ assert (
26
+ TEST_EXPORTER.dump_json()
27
+ == """{
28
+ "apiVersion": "jumpstarter.dev/v1alpha1",
29
+ "kind": "Exporter",
30
+ "metadata": {
31
+ "creationTimestamp": "2021-10-01T00:00:00Z",
32
+ "generation": 1,
33
+ "name": "test-exporter",
34
+ "namespace": "default",
35
+ "resourceVersion": "1",
36
+ "uid": "7a25eb81-6443-47ec-a62f-50165bffede8"
37
+ },
38
+ "status": {
39
+ "credential": {
40
+ "name": "test-credential"
41
+ },
42
+ "devices": [
43
+ {
44
+ "labels": {
45
+ "test": "label"
46
+ },
47
+ "uuid": "f4cf49ab-fc64-46c6-94e7-a40502eb77b1"
48
+ }
49
+ ],
50
+ "endpoint": "https://test-exporter"
51
+ }
52
+ }"""
53
+ )
54
+
55
+
56
+ def test_exporter_dump_yaml():
57
+ assert (
58
+ TEST_EXPORTER.dump_yaml()
59
+ == """apiVersion: jumpstarter.dev/v1alpha1
60
+ kind: Exporter
61
+ metadata:
62
+ creationTimestamp: '2021-10-01T00:00:00Z'
63
+ generation: 1
64
+ name: test-exporter
65
+ namespace: default
66
+ resourceVersion: '1'
67
+ uid: 7a25eb81-6443-47ec-a62f-50165bffede8
68
+ status:
69
+ credential:
70
+ name: test-credential
71
+ devices:
72
+ - labels:
73
+ test: label
74
+ uuid: f4cf49ab-fc64-46c6-94e7-a40502eb77b1
75
+ endpoint: https://test-exporter
76
+ """
77
+ )
@@ -0,0 +1,65 @@
1
+ import asyncio
2
+ import shutil
3
+ from typing import Literal, Optional
4
+
5
+
6
+ def helm_installed(name: str) -> bool:
7
+ return shutil.which(name) is not None
8
+
9
+
10
+ async def install_helm_chart(
11
+ chart: str,
12
+ name: str,
13
+ namespace: str,
14
+ basedomain: str,
15
+ grpc_endpoint: str,
16
+ router_endpoint: str,
17
+ mode: Literal["nodeport"] | Literal["ingress"] | Literal["route"],
18
+ version: str,
19
+ kubeconfig: Optional[str],
20
+ context: Optional[str],
21
+ helm: Optional[str] = "helm",
22
+ ):
23
+ grpc_port = grpc_endpoint.split(":")[1]
24
+ router_port = router_endpoint.split(":")[1]
25
+ args = [
26
+ helm,
27
+ "upgrade",
28
+ name,
29
+ "--install",
30
+ chart,
31
+ "--create-namespace",
32
+ "--namespace",
33
+ namespace,
34
+ "--set",
35
+ f"global.baseDomain={basedomain}",
36
+ "--set",
37
+ f"jumpstarter-controller.grpc.endpoint={grpc_endpoint}",
38
+ "--set",
39
+ f"jumpstarter-controller.grpc.routerEndpoint={router_endpoint}",
40
+ "--set",
41
+ "global.metrics.enabled=false",
42
+ "--set",
43
+ f"jumpstarter-controller.grpc.nodeport.enabled={'true' if mode == 'nodeport' else 'false'}",
44
+ "--set",
45
+ f"jumpstarter-controller.grpc.nodeport.port={grpc_port}",
46
+ "--set",
47
+ f"jumpstarter-controller.grpc.nodeport.routerPort={router_port}",
48
+ "--set",
49
+ f"jumpstarter-controller.grpc.mode={mode}",
50
+ "--version",
51
+ version,
52
+ "--wait",
53
+ ]
54
+
55
+ if kubeconfig is not None:
56
+ args.append("--kubeconfig")
57
+ args.append(kubeconfig)
58
+
59
+ if context is not None:
60
+ args.append("--kube-context")
61
+ args.append(context)
62
+
63
+ # Attempt to install Jumpstarter using Helm
64
+ process = await asyncio.create_subprocess_exec(*args)
65
+ await process.wait()
@@ -0,0 +1,14 @@
1
+ import yaml
2
+ from pydantic import BaseModel, ConfigDict
3
+
4
+
5
+ class JsonBaseModel(BaseModel):
6
+ """A Pydantic BaseModel with additional Jumpstarter JSON options applied."""
7
+
8
+ def dump_json(self):
9
+ return self.model_dump_json(indent=4, by_alias=True)
10
+
11
+ def dump_yaml(self):
12
+ return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2)
13
+
14
+ model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
@@ -0,0 +1,103 @@
1
+ from typing import Literal, Optional
2
+
3
+ from kubernetes_asyncio.client.models import V1Condition, V1ObjectMeta, V1ObjectReference
4
+ from pydantic import Field
5
+
6
+ from .json import JsonBaseModel
7
+ from .list import V1Alpha1List
8
+ from .serialize import SerializeV1Condition, SerializeV1ObjectMeta, SerializeV1ObjectReference
9
+ from .util import AbstractAsyncCustomObjectApi
10
+
11
+
12
+ class V1Alpha1LeaseStatus(JsonBaseModel):
13
+ begin_time: Optional[str] = Field(alias="beginTime")
14
+ conditions: list[SerializeV1Condition]
15
+ end_time: Optional[str] = Field(alias="endTime")
16
+ ended: bool
17
+ exporter: Optional[SerializeV1ObjectReference]
18
+
19
+
20
+ class V1Alpha1LeaseSelector(JsonBaseModel):
21
+ match_labels: dict[str, str] = Field(alias="matchLabels")
22
+
23
+
24
+ class V1Alpha1LeaseSpec(JsonBaseModel):
25
+ client: SerializeV1ObjectReference
26
+ duration: Optional[str]
27
+ selector: V1Alpha1LeaseSelector
28
+
29
+
30
+ class V1Alpha1Lease(JsonBaseModel):
31
+ api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1")
32
+ kind: Literal["Lease"] = Field(default="Lease")
33
+ metadata: SerializeV1ObjectMeta
34
+ spec: V1Alpha1LeaseSpec
35
+ status: V1Alpha1LeaseStatus
36
+
37
+ @staticmethod
38
+ def from_dict(dict: dict):
39
+ return V1Alpha1Lease(
40
+ api_version=dict["apiVersion"],
41
+ kind=dict["kind"],
42
+ metadata=V1ObjectMeta(
43
+ creation_timestamp=dict["metadata"]["creationTimestamp"],
44
+ generation=dict["metadata"]["generation"],
45
+ managed_fields=dict["metadata"]["managedFields"],
46
+ name=dict["metadata"]["name"],
47
+ namespace=dict["metadata"]["namespace"],
48
+ resource_version=dict["metadata"]["resourceVersion"],
49
+ uid=dict["metadata"]["uid"],
50
+ ),
51
+ status=V1Alpha1LeaseStatus(
52
+ begin_time=dict["status"]["beginTime"] if "beginTime" in dict["status"] else None,
53
+ end_time=dict["status"]["endTime"] if "endTime" in dict["status"] else None,
54
+ ended=dict["status"]["ended"],
55
+ exporter=V1ObjectReference(name=dict["status"]["exporterRef"]["name"])
56
+ if "exporterRef" in dict["status"]
57
+ else None,
58
+ conditions=[
59
+ V1Condition(
60
+ last_transition_time=cond["lastTransitionTime"],
61
+ message=cond["message"],
62
+ observed_generation=cond["observedGeneration"],
63
+ reason=cond["reason"],
64
+ status=cond["status"],
65
+ type=cond["type"],
66
+ )
67
+ for cond in dict["status"]["conditions"]
68
+ ],
69
+ ),
70
+ spec=V1Alpha1LeaseSpec(
71
+ client=V1ObjectReference(name=dict["spec"]["clientRef"]["name"])
72
+ if "clientRef" in dict["spec"]
73
+ else None,
74
+ duration=dict["spec"]["duration"] if "duration" in dict["spec"] else None,
75
+ selector=V1Alpha1LeaseSelector(match_labels=dict["spec"]["selector"]["matchLabels"]),
76
+ ),
77
+ )
78
+
79
+
80
+ class V1Alpha1LeaseList(V1Alpha1List[V1Alpha1Lease]):
81
+ kind: Literal["LeaseList"] = Field(default="LeaseList")
82
+
83
+ @staticmethod
84
+ def from_dict(dict: dict):
85
+ return V1Alpha1LeaseList(items=[V1Alpha1Lease.from_dict(c) for c in dict["items"]])
86
+
87
+
88
+ class LeasesV1Alpha1Api(AbstractAsyncCustomObjectApi):
89
+ """Interact with the leases custom resource API"""
90
+
91
+ async def list_leases(self) -> V1Alpha1List[V1Alpha1Lease]:
92
+ """List the lease objects in the cluster async"""
93
+ result = await self.api.list_namespaced_custom_object(
94
+ namespace=self.namespace, group="jumpstarter.dev", plural="leases", version="v1alpha1"
95
+ )
96
+ return V1Alpha1LeaseList.from_dict(result)
97
+
98
+ async def get_lease(self, name: str) -> V1Alpha1Lease:
99
+ """Get a single lease object from the cluster async"""
100
+ result = await self.api.get_namespaced_custom_object(
101
+ namespace=self.namespace, group="jumpstarter.dev", plural="leases", version="v1alpha1", name=name
102
+ )
103
+ return V1Alpha1Lease.from_dict(result)
@@ -0,0 +1,15 @@
1
+ from typing import Generic, Literal, TypeVar
2
+
3
+ from pydantic import Field
4
+
5
+ from .json import JsonBaseModel
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class V1Alpha1List(JsonBaseModel, Generic[T]):
11
+ """A generic list result type."""
12
+
13
+ api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1")
14
+ items: list[T]
15
+ kind: Literal["List"] = Field(default="List")
File without changes
@@ -0,0 +1,14 @@
1
+ from typing import Annotated, Any, Dict
2
+
3
+ from kubernetes_asyncio.client.models import V1Condition, V1ObjectMeta, V1ObjectReference
4
+ from pydantic import WrapSerializer
5
+
6
+
7
+ def k8s_obj_to_dict(value: Any, handler, info) -> Dict[str, Any]:
8
+ result = value.to_dict(serialize=True)
9
+ return {k: v for k, v in result.items() if v is not None}
10
+
11
+
12
+ SerializeV1Condition = Annotated[V1Condition, WrapSerializer(k8s_obj_to_dict)]
13
+ SerializeV1ObjectMeta = Annotated[V1ObjectMeta, WrapSerializer(k8s_obj_to_dict)]
14
+ SerializeV1ObjectReference = Annotated[V1ObjectReference, WrapSerializer(k8s_obj_to_dict)]
@@ -0,0 +1,117 @@
1
+ from kubernetes_asyncio.client.models import V1Condition, V1ObjectMeta, V1ObjectReference
2
+
3
+ from jumpstarter_kubernetes import V1Alpha1Lease, V1Alpha1LeaseSelector, V1Alpha1LeaseSpec, V1Alpha1LeaseStatus
4
+
5
+ TEST_LEASE = V1Alpha1Lease(
6
+ api_version="jumpstarter.dev/v1alpha1",
7
+ kind="Lease",
8
+ metadata=V1ObjectMeta(
9
+ creation_timestamp="2021-10-01T00:00:00Z",
10
+ generation=1,
11
+ name="test-lease",
12
+ namespace="default",
13
+ resource_version="1",
14
+ uid="7a25eb81-6443-47ec-a62f-50165bffede8",
15
+ ),
16
+ spec=V1Alpha1LeaseSpec(
17
+ client=V1ObjectReference(name="test-client"),
18
+ duration="1h",
19
+ selector=V1Alpha1LeaseSelector(match_labels={"test": "label", "another": "something"}),
20
+ ),
21
+ status=V1Alpha1LeaseStatus(
22
+ begin_time="2021-10-01T00:00:00Z",
23
+ conditions=[
24
+ V1Condition(
25
+ last_transition_time="2021-10-01T00:00:00Z", status="True", type="Active", message="", reason=""
26
+ )
27
+ ],
28
+ end_time="2021-10-01T01:00:00Z",
29
+ ended=False,
30
+ exporter=V1ObjectReference(name="test-exporter"),
31
+ ),
32
+ )
33
+
34
+
35
+ def test_lease_dump_json():
36
+ print(TEST_LEASE.dump_json())
37
+ assert (
38
+ TEST_LEASE.dump_json()
39
+ == """{
40
+ "apiVersion": "jumpstarter.dev/v1alpha1",
41
+ "kind": "Lease",
42
+ "metadata": {
43
+ "creationTimestamp": "2021-10-01T00:00:00Z",
44
+ "generation": 1,
45
+ "name": "test-lease",
46
+ "namespace": "default",
47
+ "resourceVersion": "1",
48
+ "uid": "7a25eb81-6443-47ec-a62f-50165bffede8"
49
+ },
50
+ "spec": {
51
+ "client": {
52
+ "name": "test-client"
53
+ },
54
+ "duration": "1h",
55
+ "selector": {
56
+ "matchLabels": {
57
+ "test": "label",
58
+ "another": "something"
59
+ }
60
+ }
61
+ },
62
+ "status": {
63
+ "beginTime": "2021-10-01T00:00:00Z",
64
+ "conditions": [
65
+ {
66
+ "lastTransitionTime": "2021-10-01T00:00:00Z",
67
+ "message": "",
68
+ "reason": "",
69
+ "status": "True",
70
+ "type": "Active"
71
+ }
72
+ ],
73
+ "endTime": "2021-10-01T01:00:00Z",
74
+ "ended": false,
75
+ "exporter": {
76
+ "name": "test-exporter"
77
+ }
78
+ }
79
+ }"""
80
+ )
81
+
82
+
83
+ def test_lease_dump_yaml():
84
+ print(TEST_LEASE.dump_yaml())
85
+ assert (
86
+ TEST_LEASE.dump_yaml()
87
+ == """apiVersion: jumpstarter.dev/v1alpha1
88
+ kind: Lease
89
+ metadata:
90
+ creationTimestamp: '2021-10-01T00:00:00Z'
91
+ generation: 1
92
+ name: test-lease
93
+ namespace: default
94
+ resourceVersion: '1'
95
+ uid: 7a25eb81-6443-47ec-a62f-50165bffede8
96
+ spec:
97
+ client:
98
+ name: test-client
99
+ duration: 1h
100
+ selector:
101
+ matchLabels:
102
+ another: something
103
+ test: label
104
+ status:
105
+ beginTime: '2021-10-01T00:00:00Z'
106
+ conditions:
107
+ - lastTransitionTime: '2021-10-01T00:00:00Z'
108
+ message: ''
109
+ reason: ''
110
+ status: 'True'
111
+ type: Active
112
+ endTime: '2021-10-01T01:00:00Z'
113
+ ended: false
114
+ exporter:
115
+ name: test-exporter
116
+ """
117
+ )
@@ -0,0 +1,3 @@
1
+ from .async_custom_object_api import AbstractAsyncCustomObjectApi
2
+
3
+ __all__ = ["AbstractAsyncCustomObjectApi"]
@@ -0,0 +1,43 @@
1
+ from contextlib import AbstractAsyncContextManager
2
+ from typing import Optional, Self
3
+
4
+ from kubernetes_asyncio import config
5
+ from kubernetes_asyncio.client.api import CoreV1Api, CustomObjectsApi
6
+ from kubernetes_asyncio.client.api_client import ApiClient
7
+
8
+
9
+ class AbstractAsyncCustomObjectApi(AbstractAsyncContextManager):
10
+ """An abstract async custom object API client"""
11
+
12
+ _client: ApiClient
13
+ config_file: Optional[str]
14
+ context: Optional[str]
15
+ namespace: str
16
+ api: CustomObjectsApi
17
+ core_api: CoreV1Api
18
+
19
+ def __init__(self, namespace: str, config_file: Optional[str] = None, context: Optional[str] = None):
20
+ self.config_file = config_file
21
+ self.context = context
22
+ self.namespace = namespace
23
+
24
+ async def __aenter__(self) -> Self:
25
+ # Load the kubeconfig
26
+ await self._load_kube_config()
27
+ # Construct the API client and enter context
28
+ self._client = ApiClient()
29
+ await self._client.__aenter__()
30
+ # Construct the custom objects API client
31
+ self.api = CustomObjectsApi(self._client)
32
+ self.core_api = CoreV1Api(self._client)
33
+ return self
34
+
35
+ async def _load_kube_config(self):
36
+ await config.load_kube_config(self.config_file, self.context)
37
+
38
+ async def __aexit__(self, exc_type, exc_value, traceback):
39
+ await self._client.__aexit__(exc_type, exc_value, traceback)
40
+ self._client = None
41
+ self.api = None
42
+ self.core_api = None
43
+ return None
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: jumpstarter-kubernetes
3
+ Version: 0.6.0
4
+ Project-URL: Homepage, https://jumpstarter.dev
5
+ Project-URL: source_archive, https://github.com/jumpstarter-dev/repo/archive/c2927a2abac82d224c7bd28f9ed83c57b5222e65.zip
6
+ Author-email: Kirk Brauer <kbrauer@hatci.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: jumpstarter==0.6.0
10
+ Requires-Dist: kubernetes-asyncio>=31.1.0
11
+ Requires-Dist: kubernetes>=31.0.0
12
+ Requires-Dist: pydantic>=2.8.2
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Jumpstarter Kubernetes Library
@@ -0,0 +1,17 @@
1
+ jumpstarter_kubernetes/__init__.py,sha256=kYHnYJYZ4ly6q7dU2cIOgnbWnO64xU_HmkTMkY0ARpE,983
2
+ jumpstarter_kubernetes/clients.py,sha256=uo43zmWhymfsatM5P87PnwJxzg4pWu9s7JTeamWny0Q,5711
3
+ jumpstarter_kubernetes/clients_test.py,sha256=dGWdFlq08CULXQw_yPT-KXjvQS-BVU5dTB2kU2-2tFo,1457
4
+ jumpstarter_kubernetes/exporters.py,sha256=4PS-kXuriJuKMlYIGcDGaB0lhZLkhg_zCoM-9LwuAD4,5903
5
+ jumpstarter_kubernetes/exporters_test.py,sha256=aKZuw6AGN_FGCnKqTAOp3MRAXrThxKtBVboYYhK-e08,2079
6
+ jumpstarter_kubernetes/install.py,sha256=HjfTwNAK_VKMoFXTqdwNuvagXDSaxS23qQ6ap5qGFGQ,1788
7
+ jumpstarter_kubernetes/json.py,sha256=69BNuBjHASbWpPXIdjiZbcLLh7-DXhYREEUFWfREQec,452
8
+ jumpstarter_kubernetes/leases.py,sha256=o-vmnLRypnDR7aWPxP1LZN28-c5gsD6S4ouoQMzHgyY,4235
9
+ jumpstarter_kubernetes/list.py,sha256=sEJEQDAgxwYC8mmGzT_BG5OsSb4KRAMKLOOlyeH93xg,398
10
+ jumpstarter_kubernetes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ jumpstarter_kubernetes/serialize.py,sha256=ZFKd-PkDaB5zKTn9lqUN7oeZ1ripst0wogIWiEZdndo,593
12
+ jumpstarter_kubernetes/test_leases.py,sha256=uZE5-NF3vLow9GkQGcruq-7KPupfp4uMWWcj96T7NQ0,3042
13
+ jumpstarter_kubernetes/util/__init__.py,sha256=oNVNglqOgDzvcibGGVoBBpcFNJv_w3x-iE0OEDKfZhM,110
14
+ jumpstarter_kubernetes/util/async_custom_object_api.py,sha256=geIgT3yvNYJerwMy2V9bOOAmYXW4n1nA_5BFdsZw-Co,1486
15
+ jumpstarter_kubernetes-0.6.0.dist-info/METADATA,sha256=5yY_P3sJA11-0gyNgqn3lKqREMQJyZXSC0lb94o5su8,550
16
+ jumpstarter_kubernetes-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ jumpstarter_kubernetes-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any