kubectl-mcp-server 1.19.0__py3-none-any.whl → 1.19.1__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.
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/METADATA +88 -18
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/RECORD +24 -22
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/WHEEL +1 -1
- kubectl_mcp_tool/k8s_config.py +296 -277
- kubectl_mcp_tool/mcp_server.py +30 -0
- kubectl_mcp_tool/providers.py +347 -0
- kubectl_mcp_tool/tools/__init__.py +2 -1
- kubectl_mcp_tool/tools/backup.py +10 -47
- kubectl_mcp_tool/tools/capi.py +12 -56
- kubectl_mcp_tool/tools/certs.py +11 -29
- kubectl_mcp_tool/tools/cilium.py +10 -47
- kubectl_mcp_tool/tools/cluster.py +489 -9
- kubectl_mcp_tool/tools/gitops.py +12 -51
- kubectl_mcp_tool/tools/keda.py +9 -47
- kubectl_mcp_tool/tools/kiali.py +10 -50
- kubectl_mcp_tool/tools/kubevirt.py +11 -49
- kubectl_mcp_tool/tools/pods.py +93 -0
- kubectl_mcp_tool/tools/policy.py +11 -49
- kubectl_mcp_tool/tools/rollouts.py +11 -65
- kubectl_mcp_tool/tools/utils.py +41 -0
- tests/test_tools.py +44 -11
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.19.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/top_level.txt +0 -0
kubectl_mcp_tool/mcp_server.py
CHANGED
|
@@ -71,6 +71,7 @@ from kubectl_mcp_tool.tools import (
|
|
|
71
71
|
register_pod_tools,
|
|
72
72
|
register_core_tools,
|
|
73
73
|
register_cluster_tools,
|
|
74
|
+
register_multicluster_tools,
|
|
74
75
|
register_deployment_tools,
|
|
75
76
|
register_security_tools,
|
|
76
77
|
register_networking_tools,
|
|
@@ -314,6 +315,7 @@ class MCPServer:
|
|
|
314
315
|
register_pod_tools(self.server, self.non_destructive)
|
|
315
316
|
register_core_tools(self.server, self.non_destructive)
|
|
316
317
|
register_cluster_tools(self.server, self.non_destructive)
|
|
318
|
+
register_multicluster_tools(self.server, self.non_destructive)
|
|
317
319
|
register_deployment_tools(self.server, self.non_destructive)
|
|
318
320
|
register_security_tools(self.server, self.non_destructive)
|
|
319
321
|
register_networking_tools(self.server, self.non_destructive)
|
|
@@ -741,8 +743,36 @@ if __name__ == "__main__":
|
|
|
741
743
|
action="store_true",
|
|
742
744
|
help="Disable destructive operations (allow create/update, block delete).",
|
|
743
745
|
)
|
|
746
|
+
parser.add_argument(
|
|
747
|
+
"--stateless",
|
|
748
|
+
action="store_true",
|
|
749
|
+
help="Enable stateless mode (don't cache API clients, reload config each request). Useful for serverless environments.",
|
|
750
|
+
)
|
|
751
|
+
parser.add_argument(
|
|
752
|
+
"--watch-kubeconfig",
|
|
753
|
+
action="store_true",
|
|
754
|
+
help="Watch kubeconfig files for changes and auto-reload. Useful when credentials are refreshed externally.",
|
|
755
|
+
)
|
|
756
|
+
parser.add_argument(
|
|
757
|
+
"--watch-interval",
|
|
758
|
+
type=float,
|
|
759
|
+
default=5.0,
|
|
760
|
+
help="Interval in seconds for kubeconfig watch checks. Default: 5.0.",
|
|
761
|
+
)
|
|
744
762
|
args = parser.parse_args()
|
|
745
763
|
|
|
764
|
+
# Configure stateless mode if requested
|
|
765
|
+
if args.stateless or os.environ.get("MCP_STATELESS_MODE", "").lower() in ("true", "1", "yes"):
|
|
766
|
+
from kubectl_mcp_tool.k8s_config import set_stateless_mode
|
|
767
|
+
set_stateless_mode(True)
|
|
768
|
+
logger.info("Stateless mode enabled via CLI flag")
|
|
769
|
+
|
|
770
|
+
# Configure kubeconfig watching if requested
|
|
771
|
+
if args.watch_kubeconfig or os.environ.get("MCP_KUBECONFIG_WATCH", "").lower() in ("true", "1", "yes"):
|
|
772
|
+
from kubectl_mcp_tool.k8s_config import enable_kubeconfig_watch
|
|
773
|
+
enable_kubeconfig_watch(check_interval=args.watch_interval)
|
|
774
|
+
logger.info(f"Kubeconfig watching enabled (interval: {args.watch_interval}s)")
|
|
775
|
+
|
|
746
776
|
server_name = "kubectl_mcp_server"
|
|
747
777
|
mcp_server = MCPServer(
|
|
748
778
|
name=server_name,
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("mcp-server")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProviderType(Enum):
|
|
12
|
+
"""Kubernetes provider types."""
|
|
13
|
+
KUBECONFIG = "kubeconfig"
|
|
14
|
+
IN_CLUSTER = "in-cluster"
|
|
15
|
+
SINGLE = "single"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnknownContextError(Exception):
|
|
19
|
+
"""Raised when a requested context is not found."""
|
|
20
|
+
def __init__(self, context: str, available: List[str] = None):
|
|
21
|
+
self.context = context
|
|
22
|
+
self.available = available or []
|
|
23
|
+
msg = f"Context '{context}' not found"
|
|
24
|
+
if self.available:
|
|
25
|
+
msg += f". Available contexts: {', '.join(self.available)}"
|
|
26
|
+
super().__init__(msg)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProviderError(Exception):
|
|
30
|
+
"""Raised when provider configuration is invalid."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ProviderConfig:
|
|
36
|
+
"""Configuration for Kubernetes provider."""
|
|
37
|
+
provider_type: ProviderType = ProviderType.KUBECONFIG
|
|
38
|
+
kubeconfig_path: str = ""
|
|
39
|
+
context: str = ""
|
|
40
|
+
qps: float = 100.0
|
|
41
|
+
burst: int = 200
|
|
42
|
+
timeout: int = 30
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_env(cls) -> "ProviderConfig":
|
|
46
|
+
"""Create config from environment variables."""
|
|
47
|
+
provider_str = os.environ.get("MCP_K8S_PROVIDER", "kubeconfig").lower()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
provider_type = ProviderType(provider_str)
|
|
51
|
+
except ValueError:
|
|
52
|
+
logger.warning(f"Unknown provider type '{provider_str}', using kubeconfig")
|
|
53
|
+
provider_type = ProviderType.KUBECONFIG
|
|
54
|
+
|
|
55
|
+
kubeconfig_path = os.environ.get(
|
|
56
|
+
"MCP_K8S_KUBECONFIG",
|
|
57
|
+
os.environ.get("KUBECONFIG", "~/.kube/config")
|
|
58
|
+
)
|
|
59
|
+
kubeconfig_path = os.path.expanduser(kubeconfig_path)
|
|
60
|
+
|
|
61
|
+
return cls(
|
|
62
|
+
provider_type=provider_type,
|
|
63
|
+
kubeconfig_path=kubeconfig_path,
|
|
64
|
+
context=os.environ.get("MCP_K8S_CONTEXT", ""),
|
|
65
|
+
qps=float(os.environ.get("MCP_K8S_QPS", "100")),
|
|
66
|
+
burst=int(os.environ.get("MCP_K8S_BURST", "200")),
|
|
67
|
+
timeout=int(os.environ.get("MCP_K8S_TIMEOUT", "30")),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class ContextInfo:
|
|
73
|
+
"""Information about a kubeconfig context."""
|
|
74
|
+
name: str
|
|
75
|
+
cluster: str
|
|
76
|
+
user: str
|
|
77
|
+
namespace: str = "default"
|
|
78
|
+
is_active: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class KubernetesProvider:
|
|
82
|
+
"""
|
|
83
|
+
Multi-cluster Kubernetes provider.
|
|
84
|
+
|
|
85
|
+
Manages connections to multiple Kubernetes clusters based on
|
|
86
|
+
kubeconfig contexts or in-cluster configuration.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
_instance: Optional["KubernetesProvider"] = None
|
|
90
|
+
|
|
91
|
+
def __init__(self, config: Optional[ProviderConfig] = None):
|
|
92
|
+
"""Initialize provider with configuration."""
|
|
93
|
+
self.config = config or ProviderConfig.from_env()
|
|
94
|
+
self._api_clients: Dict[str, Any] = {}
|
|
95
|
+
self._in_cluster = False
|
|
96
|
+
self._contexts_cache: Optional[List[ContextInfo]] = None
|
|
97
|
+
self._active_context: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
self._initialize()
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def get_instance(cls) -> "KubernetesProvider":
|
|
103
|
+
"""Get singleton provider instance."""
|
|
104
|
+
if cls._instance is None:
|
|
105
|
+
cls._instance = cls()
|
|
106
|
+
return cls._instance
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def reset_instance(cls):
|
|
110
|
+
"""Reset singleton instance (for testing)."""
|
|
111
|
+
cls._instance = None
|
|
112
|
+
|
|
113
|
+
def _initialize(self):
|
|
114
|
+
"""Initialize the provider based on type."""
|
|
115
|
+
if self.config.provider_type == ProviderType.IN_CLUSTER:
|
|
116
|
+
self._initialize_in_cluster()
|
|
117
|
+
elif self.config.provider_type == ProviderType.SINGLE:
|
|
118
|
+
self._initialize_single()
|
|
119
|
+
else:
|
|
120
|
+
self._initialize_kubeconfig()
|
|
121
|
+
|
|
122
|
+
def _initialize_in_cluster(self):
|
|
123
|
+
"""Initialize for in-cluster provider."""
|
|
124
|
+
from kubernetes import config
|
|
125
|
+
from kubernetes.config.config_exception import ConfigException
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
config.load_incluster_config()
|
|
129
|
+
self._in_cluster = True
|
|
130
|
+
self._active_context = "in-cluster"
|
|
131
|
+
logger.info("Initialized in-cluster Kubernetes provider")
|
|
132
|
+
except ConfigException as e:
|
|
133
|
+
raise ProviderError(f"Failed to load in-cluster config: {e}")
|
|
134
|
+
|
|
135
|
+
def _initialize_single(self):
|
|
136
|
+
"""Initialize for single-context provider."""
|
|
137
|
+
if not self.config.context:
|
|
138
|
+
raise ProviderError("MCP_K8S_CONTEXT must be set for 'single' provider")
|
|
139
|
+
|
|
140
|
+
from kubernetes import config
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
config.load_kube_config(
|
|
144
|
+
config_file=self.config.kubeconfig_path,
|
|
145
|
+
context=self.config.context
|
|
146
|
+
)
|
|
147
|
+
self._active_context = self.config.context
|
|
148
|
+
logger.info(f"Initialized single-context provider: {self.config.context}")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise ProviderError(f"Failed to load context '{self.config.context}': {e}")
|
|
151
|
+
|
|
152
|
+
def _initialize_kubeconfig(self):
|
|
153
|
+
"""Initialize for multi-cluster kubeconfig provider."""
|
|
154
|
+
from kubernetes import config
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
contexts, active = config.list_kube_config_contexts(
|
|
158
|
+
config_file=self.config.kubeconfig_path
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if active:
|
|
162
|
+
self._active_context = active.get("name")
|
|
163
|
+
|
|
164
|
+
self._contexts_cache = [
|
|
165
|
+
ContextInfo(
|
|
166
|
+
name=ctx.get("name", ""),
|
|
167
|
+
cluster=ctx.get("context", {}).get("cluster", ""),
|
|
168
|
+
user=ctx.get("context", {}).get("user", ""),
|
|
169
|
+
namespace=ctx.get("context", {}).get("namespace", "default"),
|
|
170
|
+
is_active=ctx.get("name") == self._active_context
|
|
171
|
+
)
|
|
172
|
+
for ctx in contexts
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
f"Initialized kubeconfig provider with {len(self._contexts_cache)} contexts, "
|
|
177
|
+
f"active: {self._active_context}"
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Failed to list contexts: {e}")
|
|
181
|
+
self._contexts_cache = []
|
|
182
|
+
|
|
183
|
+
def list_contexts(self) -> List[ContextInfo]:
|
|
184
|
+
"""
|
|
185
|
+
List all available contexts.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of ContextInfo objects
|
|
189
|
+
"""
|
|
190
|
+
if self._in_cluster:
|
|
191
|
+
return [ContextInfo(
|
|
192
|
+
name="in-cluster",
|
|
193
|
+
cluster="in-cluster",
|
|
194
|
+
user="service-account",
|
|
195
|
+
namespace="default",
|
|
196
|
+
is_active=True
|
|
197
|
+
)]
|
|
198
|
+
|
|
199
|
+
if self.config.provider_type == ProviderType.SINGLE:
|
|
200
|
+
from kubernetes import config
|
|
201
|
+
try:
|
|
202
|
+
contexts, _ = config.list_kube_config_contexts(
|
|
203
|
+
config_file=self.config.kubeconfig_path
|
|
204
|
+
)
|
|
205
|
+
for ctx in contexts:
|
|
206
|
+
if ctx.get("name") == self.config.context:
|
|
207
|
+
return [ContextInfo(
|
|
208
|
+
name=ctx.get("name", ""),
|
|
209
|
+
cluster=ctx.get("context", {}).get("cluster", ""),
|
|
210
|
+
user=ctx.get("context", {}).get("user", ""),
|
|
211
|
+
namespace=ctx.get("context", {}).get("namespace", "default"),
|
|
212
|
+
is_active=True
|
|
213
|
+
)]
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return []
|
|
217
|
+
|
|
218
|
+
self._refresh_contexts_cache()
|
|
219
|
+
return self._contexts_cache or []
|
|
220
|
+
|
|
221
|
+
def _refresh_contexts_cache(self):
|
|
222
|
+
"""Refresh the contexts cache from kubeconfig."""
|
|
223
|
+
if self._in_cluster or self.config.provider_type == ProviderType.SINGLE:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
from kubernetes import config
|
|
227
|
+
try:
|
|
228
|
+
contexts, active = config.list_kube_config_contexts(
|
|
229
|
+
config_file=self.config.kubeconfig_path
|
|
230
|
+
)
|
|
231
|
+
self._active_context = active.get("name") if active else None
|
|
232
|
+
self._contexts_cache = [
|
|
233
|
+
ContextInfo(
|
|
234
|
+
name=ctx.get("name", ""),
|
|
235
|
+
cluster=ctx.get("context", {}).get("cluster", ""),
|
|
236
|
+
user=ctx.get("context", {}).get("user", ""),
|
|
237
|
+
namespace=ctx.get("context", {}).get("namespace", "default"),
|
|
238
|
+
is_active=ctx.get("name") == self._active_context
|
|
239
|
+
)
|
|
240
|
+
for ctx in contexts
|
|
241
|
+
]
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.warning(f"Failed to refresh contexts: {e}")
|
|
244
|
+
|
|
245
|
+
def get_current_context(self) -> Optional[str]:
|
|
246
|
+
"""Get the current active context name."""
|
|
247
|
+
if self._in_cluster:
|
|
248
|
+
return "in-cluster"
|
|
249
|
+
return self._active_context
|
|
250
|
+
|
|
251
|
+
def _get_context_names(self) -> List[str]:
|
|
252
|
+
"""Get list of available context names."""
|
|
253
|
+
contexts = self.list_contexts()
|
|
254
|
+
return [ctx.name for ctx in contexts]
|
|
255
|
+
|
|
256
|
+
def validate_context(self, context: str) -> str:
|
|
257
|
+
"""
|
|
258
|
+
Validate and resolve a context name.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
context: Context name (empty string uses default)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Resolved context name
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
UnknownContextError: If context is not found
|
|
268
|
+
"""
|
|
269
|
+
if self._in_cluster:
|
|
270
|
+
return "in-cluster"
|
|
271
|
+
|
|
272
|
+
if self.config.provider_type == ProviderType.SINGLE:
|
|
273
|
+
if context and context != self.config.context:
|
|
274
|
+
raise UnknownContextError(
|
|
275
|
+
context,
|
|
276
|
+
[self.config.context]
|
|
277
|
+
)
|
|
278
|
+
return self.config.context
|
|
279
|
+
|
|
280
|
+
if not context:
|
|
281
|
+
return self._active_context or ""
|
|
282
|
+
|
|
283
|
+
available = self._get_context_names()
|
|
284
|
+
if context not in available:
|
|
285
|
+
raise UnknownContextError(context, available)
|
|
286
|
+
|
|
287
|
+
return context
|
|
288
|
+
|
|
289
|
+
def get_api_client(self, context: str = "") -> Any:
|
|
290
|
+
"""
|
|
291
|
+
Get an API client configuration for a specific context.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
context: Context name (empty uses default)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
kubernetes.client.ApiClient configured for the context
|
|
298
|
+
"""
|
|
299
|
+
from kubernetes import client, config
|
|
300
|
+
|
|
301
|
+
resolved_context = self.validate_context(context)
|
|
302
|
+
|
|
303
|
+
if resolved_context in self._api_clients:
|
|
304
|
+
return self._api_clients[resolved_context]
|
|
305
|
+
|
|
306
|
+
if self._in_cluster:
|
|
307
|
+
api_client = client.ApiClient()
|
|
308
|
+
else:
|
|
309
|
+
api_config = client.Configuration()
|
|
310
|
+
config.load_kube_config(
|
|
311
|
+
config_file=self.config.kubeconfig_path,
|
|
312
|
+
context=resolved_context,
|
|
313
|
+
client_configuration=api_config
|
|
314
|
+
)
|
|
315
|
+
api_client = client.ApiClient(configuration=api_config)
|
|
316
|
+
|
|
317
|
+
self._api_clients[resolved_context] = api_client
|
|
318
|
+
|
|
319
|
+
return api_client
|
|
320
|
+
|
|
321
|
+
def clear_client_cache(self, context: str = ""):
|
|
322
|
+
"""Clear cached API client(s)."""
|
|
323
|
+
if context:
|
|
324
|
+
self._api_clients.pop(context, None)
|
|
325
|
+
else:
|
|
326
|
+
self._api_clients.clear()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_provider() -> KubernetesProvider:
|
|
330
|
+
"""Get the global Kubernetes provider instance."""
|
|
331
|
+
return KubernetesProvider.get_instance()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_context_names() -> List[str]:
|
|
335
|
+
"""Get list of available context names."""
|
|
336
|
+
provider = get_provider()
|
|
337
|
+
return [ctx.name for ctx in provider.list_contexts()]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get_current_context() -> Optional[str]:
|
|
341
|
+
"""Get the current active context name."""
|
|
342
|
+
return get_provider().get_current_context()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def validate_context(context: str) -> str:
|
|
346
|
+
"""Validate and resolve a context name."""
|
|
347
|
+
return get_provider().validate_context(context)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .helm import register_helm_tools
|
|
2
2
|
from .pods import register_pod_tools
|
|
3
3
|
from .core import register_core_tools
|
|
4
|
-
from .cluster import register_cluster_tools
|
|
4
|
+
from .cluster import register_cluster_tools, register_multicluster_tools
|
|
5
5
|
from .deployments import register_deployment_tools
|
|
6
6
|
from .security import register_security_tools
|
|
7
7
|
from .networking import register_networking_tools
|
|
@@ -27,6 +27,7 @@ __all__ = [
|
|
|
27
27
|
"register_pod_tools",
|
|
28
28
|
"register_core_tools",
|
|
29
29
|
"register_cluster_tools",
|
|
30
|
+
"register_multicluster_tools",
|
|
30
31
|
"register_deployment_tools",
|
|
31
32
|
"register_security_tools",
|
|
32
33
|
"register_networking_tools",
|
kubectl_mcp_tool/tools/backup.py
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
"""Backup toolset for kubectl-mcp-server.
|
|
2
|
-
|
|
3
|
-
Provides tools for managing Velero backups and restores.
|
|
4
|
-
"""
|
|
1
|
+
"""Backup toolset for kubectl-mcp-server (Velero backups and restores)."""
|
|
5
2
|
|
|
6
3
|
import subprocess
|
|
7
4
|
import json
|
|
8
|
-
from typing import Dict, Any, List
|
|
5
|
+
from typing import Dict, Any, List
|
|
9
6
|
from datetime import datetime
|
|
10
7
|
|
|
11
8
|
try:
|
|
@@ -15,8 +12,8 @@ except ImportError:
|
|
|
15
12
|
from mcp.server.fastmcp import FastMCP
|
|
16
13
|
from mcp.types import ToolAnnotations
|
|
17
14
|
|
|
18
|
-
from ..k8s_config import _get_kubectl_context_args
|
|
19
15
|
from ..crd_detector import crd_exists
|
|
16
|
+
from .utils import run_kubectl, get_resources
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
VELERO_BACKUP_CRD = "backups.velero.io"
|
|
@@ -26,40 +23,6 @@ VELERO_BSL_CRD = "backupstoragelocations.velero.io"
|
|
|
26
23
|
VELERO_VSL_CRD = "volumesnapshotlocations.velero.io"
|
|
27
24
|
|
|
28
25
|
|
|
29
|
-
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
30
|
-
"""Run kubectl command and return result."""
|
|
31
|
-
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
32
|
-
try:
|
|
33
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
34
|
-
if result.returncode == 0:
|
|
35
|
-
return {"success": True, "output": result.stdout}
|
|
36
|
-
return {"success": False, "error": result.stderr}
|
|
37
|
-
except subprocess.TimeoutExpired:
|
|
38
|
-
return {"success": False, "error": "Command timed out"}
|
|
39
|
-
except Exception as e:
|
|
40
|
-
return {"success": False, "error": str(e)}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
44
|
-
"""Get Kubernetes resources of a specific kind."""
|
|
45
|
-
args = ["get", kind, "-o", "json"]
|
|
46
|
-
if namespace:
|
|
47
|
-
args.extend(["-n", namespace])
|
|
48
|
-
else:
|
|
49
|
-
args.append("-A")
|
|
50
|
-
if label_selector:
|
|
51
|
-
args.extend(["-l", label_selector])
|
|
52
|
-
|
|
53
|
-
result = _run_kubectl(args, context)
|
|
54
|
-
if result["success"]:
|
|
55
|
-
try:
|
|
56
|
-
data = json.loads(result["output"])
|
|
57
|
-
return data.get("items", [])
|
|
58
|
-
except json.JSONDecodeError:
|
|
59
|
-
return []
|
|
60
|
-
return []
|
|
61
|
-
|
|
62
|
-
|
|
63
26
|
def _velero_cli_available() -> bool:
|
|
64
27
|
"""Check if velero CLI is available."""
|
|
65
28
|
try:
|
|
@@ -112,7 +75,7 @@ def backup_list(
|
|
|
112
75
|
}
|
|
113
76
|
|
|
114
77
|
backups = []
|
|
115
|
-
for item in
|
|
78
|
+
for item in get_resources("backups.velero.io", namespace, context, label_selector):
|
|
116
79
|
status = item.get("status", {})
|
|
117
80
|
spec = item.get("spec", {})
|
|
118
81
|
progress = status.get("progress", {})
|
|
@@ -165,7 +128,7 @@ def backup_get(
|
|
|
165
128
|
return {"success": False, "error": "Velero is not installed"}
|
|
166
129
|
|
|
167
130
|
args = ["get", "backups.velero.io", name, "-n", namespace, "-o", "json"]
|
|
168
|
-
result =
|
|
131
|
+
result = run_kubectl(args, context)
|
|
169
132
|
|
|
170
133
|
if result["success"]:
|
|
171
134
|
try:
|
|
@@ -341,7 +304,7 @@ def backup_delete(
|
|
|
341
304
|
return result
|
|
342
305
|
|
|
343
306
|
args = ["delete", "backups.velero.io", name, "-n", namespace]
|
|
344
|
-
result =
|
|
307
|
+
result = run_kubectl(args, context)
|
|
345
308
|
|
|
346
309
|
if result["success"]:
|
|
347
310
|
return {
|
|
@@ -375,7 +338,7 @@ def restore_list(
|
|
|
375
338
|
}
|
|
376
339
|
|
|
377
340
|
restores = []
|
|
378
|
-
for item in
|
|
341
|
+
for item in get_resources("restores.velero.io", namespace, context, label_selector):
|
|
379
342
|
status = item.get("status", {})
|
|
380
343
|
spec = item.get("spec", {})
|
|
381
344
|
progress = status.get("progress", {})
|
|
@@ -536,7 +499,7 @@ def restore_get(
|
|
|
536
499
|
return {"success": False, "error": "Velero is not installed"}
|
|
537
500
|
|
|
538
501
|
args = ["get", "restores.velero.io", name, "-n", namespace, "-o", "json"]
|
|
539
|
-
result =
|
|
502
|
+
result = run_kubectl(args, context)
|
|
540
503
|
|
|
541
504
|
if result["success"]:
|
|
542
505
|
try:
|
|
@@ -572,7 +535,7 @@ def backup_locations_list(
|
|
|
572
535
|
}
|
|
573
536
|
|
|
574
537
|
locations = []
|
|
575
|
-
for item in
|
|
538
|
+
for item in get_resources("backupstoragelocations.velero.io", namespace, context):
|
|
576
539
|
status = item.get("status", {})
|
|
577
540
|
spec = item.get("spec", {})
|
|
578
541
|
|
|
@@ -615,7 +578,7 @@ def backup_schedules_list(
|
|
|
615
578
|
}
|
|
616
579
|
|
|
617
580
|
schedules = []
|
|
618
|
-
for item in
|
|
581
|
+
for item in get_resources("schedules.velero.io", namespace, context):
|
|
619
582
|
status = item.get("status", {})
|
|
620
583
|
spec = item.get("spec", {})
|
|
621
584
|
template = spec.get("template", {})
|