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.
Files changed (29) hide show
  1. ccai/interop/kubernetes/__init__.py +41 -0
  2. ccai/interop/kubernetes/aspectskubernetes.py +89 -0
  3. ccai/interop/kubernetes/exceptionmapping.py +106 -0
  4. ccai/interop/kubernetes/exceptions/__init__.py +35 -0
  5. ccai/interop/kubernetes/exceptions/kuberneteserrors.py +125 -0
  6. ccai/interop/kubernetes/kubeconfigloader.py +163 -0
  7. ccai/interop/kubernetes/kubernetesclient.py +363 -0
  8. ccai/interop/kubernetes/kuberneteshealth.py +33 -0
  9. ccai/interop/kubernetes/model/__init__.py +13 -0
  10. ccai/interop/kubernetes/model/nodeinfo.py +86 -0
  11. ccai/interop/kubernetes/model/workloadinfo.py +171 -0
  12. ccai/interop/kubernetes/py.typed +0 -0
  13. ccai/interop/kubernetes/specs/__init__.py +11 -0
  14. ccai/interop/kubernetes/specs/workloadspec.py +70 -0
  15. ccai/interop/kubernetes/specs/workloadspecs.py +319 -0
  16. ccai/interop/kubernetes/workload/__init__.py +23 -0
  17. ccai/interop/kubernetes/workload/deploymentprimitive.py +274 -0
  18. ccai/interop/kubernetes/workload/ingressprimitive.py +247 -0
  19. ccai/interop/kubernetes/workload/jobprimitive.py +344 -0
  20. ccai/interop/kubernetes/workload/labelselector.py +87 -0
  21. ccai/interop/kubernetes/workload/namespaceprimitive.py +189 -0
  22. ccai/interop/kubernetes/workload/pvcprimitive.py +259 -0
  23. ccai/interop/kubernetes/workload/serviceprimitive.py +230 -0
  24. ccai/interop/kubernetes/workload/statefulsetprimitive.py +274 -0
  25. ccai/interop/kubernetes/workload/workloadprimitive.py +271 -0
  26. codecraftai_interop_kubernetes-1.2.0.dist-info/METADATA +84 -0
  27. codecraftai_interop_kubernetes-1.2.0.dist-info/RECORD +29 -0
  28. codecraftai_interop_kubernetes-1.2.0.dist-info/WHEEL +4 -0
  29. 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"]