codecraftai-interop-kubernetes 1.2.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.
- ccai/interop/kubernetes/__init__.py +41 -0
- ccai/interop/kubernetes/aspectskubernetes.py +89 -0
- ccai/interop/kubernetes/exceptionmapping.py +106 -0
- ccai/interop/kubernetes/exceptions/__init__.py +35 -0
- ccai/interop/kubernetes/exceptions/kuberneteserrors.py +125 -0
- ccai/interop/kubernetes/kubeconfigloader.py +163 -0
- ccai/interop/kubernetes/kubernetesclient.py +363 -0
- ccai/interop/kubernetes/kuberneteshealth.py +33 -0
- ccai/interop/kubernetes/model/__init__.py +13 -0
- ccai/interop/kubernetes/model/nodeinfo.py +86 -0
- ccai/interop/kubernetes/model/workloadinfo.py +171 -0
- ccai/interop/kubernetes/py.typed +0 -0
- ccai/interop/kubernetes/specs/__init__.py +11 -0
- ccai/interop/kubernetes/specs/workloadspec.py +70 -0
- ccai/interop/kubernetes/specs/workloadspecs.py +319 -0
- ccai/interop/kubernetes/workload/__init__.py +23 -0
- ccai/interop/kubernetes/workload/deploymentprimitive.py +274 -0
- ccai/interop/kubernetes/workload/ingressprimitive.py +247 -0
- ccai/interop/kubernetes/workload/jobprimitive.py +344 -0
- ccai/interop/kubernetes/workload/labelselector.py +87 -0
- ccai/interop/kubernetes/workload/namespaceprimitive.py +189 -0
- ccai/interop/kubernetes/workload/pvcprimitive.py +259 -0
- ccai/interop/kubernetes/workload/serviceprimitive.py +230 -0
- ccai/interop/kubernetes/workload/statefulsetprimitive.py +274 -0
- ccai/interop/kubernetes/workload/workloadprimitive.py +271 -0
- codecraftai_interop_kubernetes-1.2.0.dist-info/METADATA +84 -0
- codecraftai_interop_kubernetes-1.2.0.dist-info/RECORD +29 -0
- codecraftai_interop_kubernetes-1.2.0.dist-info/WHEEL +4 -0
- codecraftai_interop_kubernetes-1.2.0.dist-info/licenses/LICENSE.md +204 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: kubernetes
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: The ``codecraftai-interop-kubernetes`` package — the Kubernetes
|
|
5
|
+
interop client for the ccai suite.
|
|
6
|
+
|
|
7
|
+
Transport note
|
|
8
|
+
--------------
|
|
9
|
+
This package wraps the official ``kubernetes`` Python SDK. That SDK is
|
|
10
|
+
the correct, supported transport for the Kubernetes API server, so this
|
|
11
|
+
package is deliberately *not* a ``RestClientBase`` codegen package and
|
|
12
|
+
introduces no parallel REST client. All public APIs normalise the SDK's
|
|
13
|
+
behaviour to the ccai ``Aspects`` pattern — callers control
|
|
14
|
+
timeouts, polling and logging; no ``@retry``-style decorators are used.
|
|
15
|
+
|
|
16
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__author__ = "Myron Walker"
|
|
20
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
21
|
+
__credits__ = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from ccai.interop.kubernetes.aspectskubernetes import (
|
|
25
|
+
AspectsKubernetes,
|
|
26
|
+
DEFAULT_KUBERNETES_ASPECTS,
|
|
27
|
+
)
|
|
28
|
+
from ccai.interop.kubernetes.kubernetesclient import (
|
|
29
|
+
KubernetesClient,
|
|
30
|
+
DEFAULT_NAMESPACE,
|
|
31
|
+
)
|
|
32
|
+
from ccai.interop.kubernetes.kuberneteshealth import KubernetesHealth
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AspectsKubernetes",
|
|
37
|
+
"DEFAULT_KUBERNETES_ASPECTS",
|
|
38
|
+
"DEFAULT_NAMESPACE",
|
|
39
|
+
"KubernetesClient",
|
|
40
|
+
"KubernetesHealth",
|
|
41
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: aspectskubernetes
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Provides :class:`AspectsKubernetes` and the
|
|
5
|
+
``DEFAULT_KUBERNETES_ASPECTS`` instance applied to every public
|
|
6
|
+
method on :class:`KubernetesClient` and the workload primitives.
|
|
7
|
+
|
|
8
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__author__ = "Myron Walker"
|
|
12
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
13
|
+
__credits__ = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from ccai.aspects.aspects import Aspects
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AspectsKubernetes(Aspects):
|
|
24
|
+
"""
|
|
25
|
+
Kubernetes-specialised :class:`Aspects` carrier.
|
|
26
|
+
|
|
27
|
+
The class exists so that callers can write
|
|
28
|
+
``aspects: AspectsKubernetes`` for clarity at call sites and so
|
|
29
|
+
that Kubernetes-specific behaviour parameters (the request timeout
|
|
30
|
+
forwarded to the SDK, the waiter poll interval, and the structured
|
|
31
|
+
logging level) carry sensible defaults without churning the public
|
|
32
|
+
type signature on every primitive method.
|
|
33
|
+
|
|
34
|
+
:ivar timeout_seconds: Per-request timeout forwarded to the SDK.
|
|
35
|
+
:ivar poll_interval: Seconds between iterations of a waiter loop.
|
|
36
|
+
:ivar logging_level: Standard ``logging`` level for structured logs.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
completion_interval: float = 2.0,
|
|
43
|
+
completion_timeout: float = 600.0,
|
|
44
|
+
connection_timeout: float = 30.0,
|
|
45
|
+
max_attempts: int = 1,
|
|
46
|
+
logger_name: Optional[str] = None,
|
|
47
|
+
timeout_seconds: float = 30.0,
|
|
48
|
+
poll_interval: float = 2.0,
|
|
49
|
+
logging_level: int = logging.INFO,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Initialise a Kubernetes-flavoured aspects bundle.
|
|
53
|
+
|
|
54
|
+
:param completion_interval: Seconds between completion-poll
|
|
55
|
+
iterations for callers that drive a synchronous wait loop.
|
|
56
|
+
:param completion_timeout: Maximum seconds to wait for
|
|
57
|
+
completion before raising :class:`TimeoutError`.
|
|
58
|
+
:param connection_timeout: Maximum seconds to wait for a single
|
|
59
|
+
network attempt before reclassifying.
|
|
60
|
+
:param max_attempts: Maximum number of attempts a caller is
|
|
61
|
+
allowed to perform when reconciling against a cluster.
|
|
62
|
+
:param logger_name: Optional logger name a caller can route
|
|
63
|
+
logs into.
|
|
64
|
+
:param timeout_seconds: Per-request timeout forwarded to the
|
|
65
|
+
underlying ``kubernetes`` SDK call.
|
|
66
|
+
:param poll_interval: Seconds between iterations of a waiter
|
|
67
|
+
sub-loop.
|
|
68
|
+
:param logging_level: Standard ``logging`` level used for the
|
|
69
|
+
structured logging emitted by primitive methods.
|
|
70
|
+
|
|
71
|
+
:returns: ``None``
|
|
72
|
+
"""
|
|
73
|
+
super().__init__(
|
|
74
|
+
completion_interval=completion_interval,
|
|
75
|
+
completion_timeout=completion_timeout,
|
|
76
|
+
connection_timeout=connection_timeout,
|
|
77
|
+
max_attempts=max_attempts,
|
|
78
|
+
logger_name=logger_name,
|
|
79
|
+
)
|
|
80
|
+
self.timeout_seconds = timeout_seconds
|
|
81
|
+
self.poll_interval = poll_interval
|
|
82
|
+
self.logging_level = logging_level
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
DEFAULT_KUBERNETES_ASPECTS = AspectsKubernetes()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["AspectsKubernetes", "DEFAULT_KUBERNETES_ASPECTS"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: exceptionmapping
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Provides the :func:`reclassify_api_exception` helper which
|
|
5
|
+
translates a ``kubernetes`` SDK ``ApiException`` into the ccai
|
|
6
|
+
typed exception hierarchy.
|
|
7
|
+
|
|
8
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__author__ = "Myron Walker"
|
|
12
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
13
|
+
__credits__ = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from ccai.interop.kubernetes.exceptions.kuberneteserrors import (
|
|
19
|
+
KubernetesApiError,
|
|
20
|
+
KubernetesConflictError,
|
|
21
|
+
KubernetesForbiddenError,
|
|
22
|
+
KubernetesResourceNotFoundError,
|
|
23
|
+
KubernetesUnauthorizedError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
STATUS_TO_EXCEPTION = {
|
|
28
|
+
401: KubernetesUnauthorizedError,
|
|
29
|
+
403: KubernetesForbiddenError,
|
|
30
|
+
404: KubernetesResourceNotFoundError,
|
|
31
|
+
409: KubernetesConflictError,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_api_exception(error: BaseException) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Decide whether ``error`` is a ``kubernetes`` SDK ``ApiException``
|
|
38
|
+
(or a test double that mimics it) and is therefore safe to
|
|
39
|
+
reclassify into the ccai typed exception hierarchy.
|
|
40
|
+
|
|
41
|
+
Anything else — ``SemanticError``, ``TypeError``, ``ValueError`` and
|
|
42
|
+
other programming-defect errors — must NOT be reclassified; it has
|
|
43
|
+
to bubble untouched so callers and the audit can see the real fault.
|
|
44
|
+
|
|
45
|
+
The check is structural: the SDK ``ApiException`` is identified by a
|
|
46
|
+
class name ending in ``ApiException`` together with the ``status``
|
|
47
|
+
attribute it always carries. This avoids a hard import of the SDK
|
|
48
|
+
at module-import time.
|
|
49
|
+
|
|
50
|
+
:param error: The caught exception instance to classify.
|
|
51
|
+
|
|
52
|
+
:returns: ``True`` when ``error`` is an SDK API exception.
|
|
53
|
+
"""
|
|
54
|
+
type_name = type(error).__name__
|
|
55
|
+
is_named_api_exception = type_name.endswith("ApiException")
|
|
56
|
+
has_status_attribute = hasattr(error, "status")
|
|
57
|
+
|
|
58
|
+
rtnval = is_named_api_exception and has_status_attribute
|
|
59
|
+
return rtnval
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reclassify_api_exception(api_exception: Exception) -> KubernetesApiError:
|
|
63
|
+
"""
|
|
64
|
+
Translate a ``kubernetes`` SDK ``ApiException`` into the matching
|
|
65
|
+
ccai typed exception so callers never have to inspect HTTP
|
|
66
|
+
status codes.
|
|
67
|
+
|
|
68
|
+
:param api_exception: The SDK ``ApiException`` instance to translate.
|
|
69
|
+
|
|
70
|
+
:returns: A :class:`KubernetesApiError` (or one of its subclasses)
|
|
71
|
+
that should be raised ``from api_exception`` by the caller.
|
|
72
|
+
"""
|
|
73
|
+
status_code = getattr(api_exception, "status", None)
|
|
74
|
+
|
|
75
|
+
if status_code is None:
|
|
76
|
+
status_code = 0
|
|
77
|
+
|
|
78
|
+
detail = getattr(api_exception, "reason", None)
|
|
79
|
+
|
|
80
|
+
if detail is None:
|
|
81
|
+
detail = str(api_exception)
|
|
82
|
+
|
|
83
|
+
body = getattr(api_exception, "body", None)
|
|
84
|
+
|
|
85
|
+
err_msg_lines = ["Kubernetes API request failed."]
|
|
86
|
+
err_msg_lines.append("STATUS: {}".format(status_code))
|
|
87
|
+
err_msg_lines.append("REASON: {}".format(detail))
|
|
88
|
+
|
|
89
|
+
if body is not None:
|
|
90
|
+
err_msg_lines.append("BODY:")
|
|
91
|
+
err_msg_lines.append(" {}".format(body))
|
|
92
|
+
|
|
93
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
94
|
+
|
|
95
|
+
exception_type = STATUS_TO_EXCEPTION.get(status_code, KubernetesApiError)
|
|
96
|
+
|
|
97
|
+
reclassified = exception_type(
|
|
98
|
+
err_msg,
|
|
99
|
+
status_code=status_code,
|
|
100
|
+
detail=str(detail),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return reclassified
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["is_api_exception", "reclassify_api_exception", "STATUS_TO_EXCEPTION"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: exceptions
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Exception hierarchy for the ``codecraftai-interop-kubernetes``
|
|
5
|
+
package. Re-exports the named exception types so callers can import
|
|
6
|
+
them from a single location.
|
|
7
|
+
|
|
8
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__author__ = "Myron Walker"
|
|
12
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
13
|
+
__credits__ = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from ccai.interop.kubernetes.exceptions.kuberneteserrors import (
|
|
17
|
+
KubernetesApiError,
|
|
18
|
+
KubernetesConflictError,
|
|
19
|
+
KubernetesForbiddenError,
|
|
20
|
+
KubernetesJobFailedError,
|
|
21
|
+
KubernetesResourceNotFoundError,
|
|
22
|
+
KubernetesUnauthorizedError,
|
|
23
|
+
KubernetesWaitTimeoutError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"KubernetesApiError",
|
|
29
|
+
"KubernetesConflictError",
|
|
30
|
+
"KubernetesForbiddenError",
|
|
31
|
+
"KubernetesJobFailedError",
|
|
32
|
+
"KubernetesResourceNotFoundError",
|
|
33
|
+
"KubernetesUnauthorizedError",
|
|
34
|
+
"KubernetesWaitTimeoutError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: kuberneteserrors
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Defines the typed exception hierarchy raised by
|
|
5
|
+
:class:`KubernetesClient` and the workload primitives. The hierarchy
|
|
6
|
+
shields callers from the underlying SDK's ``ApiException`` status
|
|
7
|
+
codes.
|
|
8
|
+
|
|
9
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__author__ = "Myron Walker"
|
|
13
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
14
|
+
__credits__ = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KubernetesApiError(Exception):
|
|
21
|
+
"""
|
|
22
|
+
Base error for every failure reclassified from the underlying
|
|
23
|
+
``kubernetes`` SDK ``ApiException``.
|
|
24
|
+
|
|
25
|
+
:ivar status_code: The HTTP status code reported by the API server.
|
|
26
|
+
:ivar detail: The human readable detail message from the API server.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, *, status_code: int, detail: str) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialise a :class:`KubernetesApiError`.
|
|
32
|
+
|
|
33
|
+
:param message: The summary message describing the failure.
|
|
34
|
+
:param status_code: The HTTP status code reported by the API
|
|
35
|
+
server.
|
|
36
|
+
:param detail: The detail message reported by the API server.
|
|
37
|
+
|
|
38
|
+
:returns: ``None``
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.status_code = status_code
|
|
42
|
+
self.detail = detail
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class KubernetesUnauthorizedError(KubernetesApiError):
|
|
47
|
+
"""
|
|
48
|
+
Raised when the API server reports a 401 (authentication failure).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class KubernetesForbiddenError(KubernetesApiError):
|
|
53
|
+
"""
|
|
54
|
+
Raised when the API server reports a 403 (authorisation failure).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KubernetesResourceNotFoundError(KubernetesApiError):
|
|
59
|
+
"""
|
|
60
|
+
Raised when the API server reports a 404 (resource absent).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class KubernetesConflictError(KubernetesApiError):
|
|
65
|
+
"""
|
|
66
|
+
Raised when the API server reports a 409 (resource conflict). Also
|
|
67
|
+
raised by the workload primitives when an ``apply`` would adopt a
|
|
68
|
+
foreign, non-codecraftai-managed resource.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class KubernetesJobFailedError(Exception):
|
|
73
|
+
"""
|
|
74
|
+
Raised by :meth:`JobPrimitive.wait_for_completion` when the observed
|
|
75
|
+
Job transitions to a ``Failed`` state.
|
|
76
|
+
|
|
77
|
+
:ivar info: The :class:`JobInfo` DTO observed at the time of failure.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, message: str, *, info: object) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Initialise a :class:`KubernetesJobFailedError`.
|
|
83
|
+
|
|
84
|
+
:param message: The summary message describing the failure.
|
|
85
|
+
:param info: The :class:`JobInfo` DTO observed at failure time.
|
|
86
|
+
|
|
87
|
+
:returns: ``None``
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(message)
|
|
90
|
+
self.info = info
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class KubernetesWaitTimeoutError(TimeoutError):
|
|
95
|
+
"""
|
|
96
|
+
Raised by a workload primitive waiter when the resource does not
|
|
97
|
+
reach the awaited state within ``aspects.completion_timeout``.
|
|
98
|
+
|
|
99
|
+
:ivar observed: The last observed DTO or status object, retained for
|
|
100
|
+
debuggability.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, message: str, *, observed: Optional[object] = None) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Initialise a :class:`KubernetesWaitTimeoutError`.
|
|
106
|
+
|
|
107
|
+
:param message: The summary message describing the timeout.
|
|
108
|
+
:param observed: The last observed status object.
|
|
109
|
+
|
|
110
|
+
:returns: ``None``
|
|
111
|
+
"""
|
|
112
|
+
super().__init__(message)
|
|
113
|
+
self.observed = observed
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"KubernetesApiError",
|
|
119
|
+
"KubernetesConflictError",
|
|
120
|
+
"KubernetesForbiddenError",
|
|
121
|
+
"KubernetesJobFailedError",
|
|
122
|
+
"KubernetesResourceNotFoundError",
|
|
123
|
+
"KubernetesUnauthorizedError",
|
|
124
|
+
"KubernetesWaitTimeoutError",
|
|
125
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: kubeconfigloader
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Provides :class:`KubeConfigLoader`, the helper that resolves a
|
|
5
|
+
kubeconfig from a path, an in-memory dict, or an in-cluster
|
|
6
|
+
service-account context into a :class:`KubernetesRestClient`.
|
|
7
|
+
|
|
8
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__author__ = "Myron Walker"
|
|
12
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
13
|
+
__credits__ = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from ccai.errors.exceptions import SemanticError
|
|
21
|
+
|
|
22
|
+
from ccai.restclient.kubernetes.kubeconfigresolver import (
|
|
23
|
+
KubeConfigResolver
|
|
24
|
+
)
|
|
25
|
+
from ccai.restclient.kubernetes.kubernetesrestclient import (
|
|
26
|
+
KubernetesRestClient
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KubeConfigLoader:
|
|
31
|
+
"""
|
|
32
|
+
Resolves a kubeconfig source into a
|
|
33
|
+
:class:`ccai.restclient.kubernetes.kubernetesrestclient.KubernetesRestClient`.
|
|
34
|
+
|
|
35
|
+
Exactly one of ``kubeconfig_path``, ``kubeconfig_dict`` or
|
|
36
|
+
``in_cluster`` must be supplied. The loader keeps the resolved
|
|
37
|
+
in-memory dict (when one was supplied) so callers may later switch
|
|
38
|
+
contexts without re-reading the source.
|
|
39
|
+
|
|
40
|
+
:ivar kubeconfig_path: The path the kubeconfig was loaded from, or
|
|
41
|
+
``None``.
|
|
42
|
+
:ivar kubeconfig_dict: The in-memory kubeconfig dict, or ``None``.
|
|
43
|
+
:ivar in_cluster: ``True`` when the in-cluster context is in use.
|
|
44
|
+
:ivar context: The active context name, or ``None`` for in-cluster.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
kubeconfig_path: Optional[str] = None,
|
|
51
|
+
kubeconfig_dict: Optional[Dict[str, object]] = None,
|
|
52
|
+
in_cluster: bool = False,
|
|
53
|
+
context: Optional[str] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialise a :class:`KubeConfigLoader`.
|
|
57
|
+
|
|
58
|
+
:param kubeconfig_path: Filesystem path of a kubeconfig file.
|
|
59
|
+
:param kubeconfig_dict: An in-memory kubeconfig dict.
|
|
60
|
+
:param in_cluster: When ``True`` use the in-cluster
|
|
61
|
+
service-account context.
|
|
62
|
+
:param context: The named context to activate. Ignored when
|
|
63
|
+
``in_cluster`` is ``True``.
|
|
64
|
+
|
|
65
|
+
:returns: ``None``
|
|
66
|
+
:raises: SemanticError
|
|
67
|
+
"""
|
|
68
|
+
sources_supplied = []
|
|
69
|
+
|
|
70
|
+
if kubeconfig_path is not None:
|
|
71
|
+
sources_supplied.append("kubeconfig_path")
|
|
72
|
+
|
|
73
|
+
if kubeconfig_dict is not None:
|
|
74
|
+
sources_supplied.append("kubeconfig_dict")
|
|
75
|
+
|
|
76
|
+
if in_cluster is True:
|
|
77
|
+
sources_supplied.append("in_cluster")
|
|
78
|
+
|
|
79
|
+
if len(sources_supplied) == 0:
|
|
80
|
+
err_msg_lines = [
|
|
81
|
+
"A kubeconfig source must be supplied.",
|
|
82
|
+
("Provide exactly one of 'kubeconfig_path', "
|
|
83
|
+
"'kubeconfig_dict' or 'in_cluster=True'."),
|
|
84
|
+
]
|
|
85
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
86
|
+
raise SemanticError(err_msg) from None
|
|
87
|
+
|
|
88
|
+
if len(sources_supplied) > 1:
|
|
89
|
+
err_msg_lines = [
|
|
90
|
+
"Only one kubeconfig source may be supplied.",
|
|
91
|
+
"SUPPLIED: ({})".format(len(sources_supplied)),
|
|
92
|
+
]
|
|
93
|
+
for source_name in sources_supplied:
|
|
94
|
+
err_msg_lines.append(" {}".format(source_name))
|
|
95
|
+
|
|
96
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
97
|
+
raise SemanticError(err_msg) from None
|
|
98
|
+
|
|
99
|
+
self.kubeconfig_path = kubeconfig_path
|
|
100
|
+
self.kubeconfig_dict = kubeconfig_dict
|
|
101
|
+
self.in_cluster = in_cluster
|
|
102
|
+
self.context = context
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
def available_contexts(self) -> List[str]:
|
|
106
|
+
"""
|
|
107
|
+
Enumerate the context names available in the resolved kubeconfig.
|
|
108
|
+
|
|
109
|
+
:returns: The list of context names. An in-cluster loader yields
|
|
110
|
+
an empty list because in-cluster operation has no contexts.
|
|
111
|
+
"""
|
|
112
|
+
context_names: List[str] = []
|
|
113
|
+
|
|
114
|
+
if self.in_cluster is True:
|
|
115
|
+
return context_names
|
|
116
|
+
|
|
117
|
+
config_dict = self.kubeconfig_dict
|
|
118
|
+
|
|
119
|
+
if config_dict is None and self.kubeconfig_path is not None:
|
|
120
|
+
import yaml
|
|
121
|
+
|
|
122
|
+
with open(self.kubeconfig_path, "r") as config_handle:
|
|
123
|
+
config_dict = yaml.safe_load(config_handle)
|
|
124
|
+
|
|
125
|
+
if config_dict is None:
|
|
126
|
+
return context_names
|
|
127
|
+
|
|
128
|
+
contexts = config_dict.get("contexts", None)
|
|
129
|
+
|
|
130
|
+
if contexts is None:
|
|
131
|
+
return context_names
|
|
132
|
+
|
|
133
|
+
for context_entry in contexts:
|
|
134
|
+
entry_name = context_entry.get("name", None)
|
|
135
|
+
if entry_name is not None:
|
|
136
|
+
context_names.append(entry_name)
|
|
137
|
+
|
|
138
|
+
return context_names
|
|
139
|
+
|
|
140
|
+
def build_api_client(self) -> object:
|
|
141
|
+
"""
|
|
142
|
+
Resolve the kubeconfig source and build a configured
|
|
143
|
+
:class:`KubernetesRestClient`.
|
|
144
|
+
|
|
145
|
+
The name is retained from when this returned an SDK
|
|
146
|
+
``ApiClient``; the returned object now carries the api groups
|
|
147
|
+
directly (``core_v1``, ``apps_v1``, ``batch_v1``,
|
|
148
|
+
``networking_v1``, ``version``).
|
|
149
|
+
|
|
150
|
+
:returns: A configured :class:`KubernetesRestClient`.
|
|
151
|
+
"""
|
|
152
|
+
resolver = KubeConfigResolver(
|
|
153
|
+
kubeconfig_path=self.kubeconfig_path,
|
|
154
|
+
kubeconfig_dict=self.kubeconfig_dict,
|
|
155
|
+
in_cluster=self.in_cluster,
|
|
156
|
+
context=self.context,
|
|
157
|
+
)
|
|
158
|
+
resolution = resolver.resolve()
|
|
159
|
+
api_client = KubernetesRestClient(resolution=resolution)
|
|
160
|
+
return api_client
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ["KubeConfigLoader"]
|