kubectl-mcp-server 1.16.0__py3-none-any.whl → 1.17.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.
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/RECORD +28 -14
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/mcp_server.py +219 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- tests/test_config.py +386 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
tests/test_safety.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Tests for safety mode implementation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from kubectl_mcp_tool.safety import (
|
|
5
|
+
SafetyMode,
|
|
6
|
+
get_safety_mode,
|
|
7
|
+
set_safety_mode,
|
|
8
|
+
is_operation_allowed,
|
|
9
|
+
check_safety_mode,
|
|
10
|
+
get_mode_info,
|
|
11
|
+
WRITE_OPERATIONS,
|
|
12
|
+
DESTRUCTIVE_OPERATIONS,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestSafetyMode:
|
|
17
|
+
"""Test safety mode enum and state management."""
|
|
18
|
+
|
|
19
|
+
def setup_method(self):
|
|
20
|
+
"""Reset safety mode to NORMAL before each test."""
|
|
21
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
22
|
+
|
|
23
|
+
def test_default_mode_is_normal(self):
|
|
24
|
+
"""Test that default safety mode is NORMAL."""
|
|
25
|
+
assert get_safety_mode() == SafetyMode.NORMAL
|
|
26
|
+
|
|
27
|
+
def test_set_read_only_mode(self):
|
|
28
|
+
"""Test setting read-only mode."""
|
|
29
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
30
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
31
|
+
|
|
32
|
+
def test_set_disable_destructive_mode(self):
|
|
33
|
+
"""Test setting disable-destructive mode."""
|
|
34
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
35
|
+
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestOperationAllowed:
|
|
39
|
+
"""Test is_operation_allowed function."""
|
|
40
|
+
|
|
41
|
+
def setup_method(self):
|
|
42
|
+
"""Reset safety mode to NORMAL before each test."""
|
|
43
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
44
|
+
|
|
45
|
+
def test_all_operations_allowed_in_normal_mode(self):
|
|
46
|
+
"""Test that all operations are allowed in NORMAL mode."""
|
|
47
|
+
for op in WRITE_OPERATIONS | DESTRUCTIVE_OPERATIONS:
|
|
48
|
+
allowed, reason = is_operation_allowed(op)
|
|
49
|
+
assert allowed is True
|
|
50
|
+
assert reason == ""
|
|
51
|
+
|
|
52
|
+
def test_read_operations_allowed_in_all_modes(self):
|
|
53
|
+
"""Test that read operations are allowed in all modes."""
|
|
54
|
+
read_ops = ["get_pods", "list_namespaces", "describe_deployment", "get_logs"]
|
|
55
|
+
|
|
56
|
+
for mode in SafetyMode:
|
|
57
|
+
set_safety_mode(mode)
|
|
58
|
+
for op in read_ops:
|
|
59
|
+
allowed, reason = is_operation_allowed(op)
|
|
60
|
+
assert allowed is True
|
|
61
|
+
|
|
62
|
+
def test_write_operations_blocked_in_read_only_mode(self):
|
|
63
|
+
"""Test that write operations are blocked in READ_ONLY mode."""
|
|
64
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
65
|
+
|
|
66
|
+
for op in WRITE_OPERATIONS:
|
|
67
|
+
allowed, reason = is_operation_allowed(op)
|
|
68
|
+
assert allowed is False
|
|
69
|
+
assert "read-only mode" in reason
|
|
70
|
+
|
|
71
|
+
def test_destructive_operations_blocked_in_read_only_mode(self):
|
|
72
|
+
"""Test that destructive operations are blocked in READ_ONLY mode."""
|
|
73
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
74
|
+
|
|
75
|
+
for op in DESTRUCTIVE_OPERATIONS:
|
|
76
|
+
allowed, reason = is_operation_allowed(op)
|
|
77
|
+
assert allowed is False
|
|
78
|
+
assert "read-only mode" in reason
|
|
79
|
+
|
|
80
|
+
def test_write_operations_allowed_in_disable_destructive_mode(self):
|
|
81
|
+
"""Test that non-destructive write operations are allowed in DISABLE_DESTRUCTIVE mode."""
|
|
82
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
83
|
+
|
|
84
|
+
# Operations that are write but not destructive
|
|
85
|
+
non_destructive_writes = WRITE_OPERATIONS - DESTRUCTIVE_OPERATIONS
|
|
86
|
+
for op in non_destructive_writes:
|
|
87
|
+
allowed, reason = is_operation_allowed(op)
|
|
88
|
+
assert allowed is True
|
|
89
|
+
|
|
90
|
+
def test_destructive_operations_blocked_in_disable_destructive_mode(self):
|
|
91
|
+
"""Test that destructive operations are blocked in DISABLE_DESTRUCTIVE mode."""
|
|
92
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
93
|
+
|
|
94
|
+
for op in DESTRUCTIVE_OPERATIONS:
|
|
95
|
+
allowed, reason = is_operation_allowed(op)
|
|
96
|
+
assert allowed is False
|
|
97
|
+
assert "destructive operations are disabled" in reason
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestCheckSafetyModeDecorator:
|
|
101
|
+
"""Test the check_safety_mode decorator."""
|
|
102
|
+
|
|
103
|
+
def setup_method(self):
|
|
104
|
+
"""Reset safety mode to NORMAL before each test."""
|
|
105
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
106
|
+
|
|
107
|
+
def test_decorator_allows_in_normal_mode(self):
|
|
108
|
+
"""Test that decorated function executes in NORMAL mode."""
|
|
109
|
+
@check_safety_mode
|
|
110
|
+
def delete_pod():
|
|
111
|
+
return {"success": True, "message": "Pod deleted"}
|
|
112
|
+
|
|
113
|
+
result = delete_pod()
|
|
114
|
+
assert result["success"] is True
|
|
115
|
+
assert result["message"] == "Pod deleted"
|
|
116
|
+
|
|
117
|
+
def test_decorator_blocks_write_in_read_only_mode(self):
|
|
118
|
+
"""Test that decorated function is blocked in READ_ONLY mode."""
|
|
119
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
120
|
+
|
|
121
|
+
@check_safety_mode
|
|
122
|
+
def run_pod():
|
|
123
|
+
return {"success": True, "message": "Pod created"}
|
|
124
|
+
|
|
125
|
+
result = run_pod()
|
|
126
|
+
assert result["success"] is False
|
|
127
|
+
assert "blocked" in result["error"]
|
|
128
|
+
assert result["blocked_by"] == "read_only"
|
|
129
|
+
assert result["operation"] == "run_pod"
|
|
130
|
+
|
|
131
|
+
def test_decorator_blocks_destructive_in_disable_destructive_mode(self):
|
|
132
|
+
"""Test that destructive operations are blocked."""
|
|
133
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
134
|
+
|
|
135
|
+
@check_safety_mode
|
|
136
|
+
def delete_deployment():
|
|
137
|
+
return {"success": True, "message": "Deployment deleted"}
|
|
138
|
+
|
|
139
|
+
result = delete_deployment()
|
|
140
|
+
assert result["success"] is False
|
|
141
|
+
assert "blocked" in result["error"]
|
|
142
|
+
assert result["blocked_by"] == "disable_destructive"
|
|
143
|
+
|
|
144
|
+
def test_decorator_allows_non_destructive_write_in_disable_destructive_mode(self):
|
|
145
|
+
"""Test that non-destructive writes are allowed in DISABLE_DESTRUCTIVE mode."""
|
|
146
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
147
|
+
|
|
148
|
+
@check_safety_mode
|
|
149
|
+
def scale_deployment():
|
|
150
|
+
return {"success": True, "message": "Deployment scaled"}
|
|
151
|
+
|
|
152
|
+
result = scale_deployment()
|
|
153
|
+
assert result["success"] is True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestGetModeInfo:
|
|
157
|
+
"""Test get_mode_info function."""
|
|
158
|
+
|
|
159
|
+
def setup_method(self):
|
|
160
|
+
"""Reset safety mode to NORMAL before each test."""
|
|
161
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
162
|
+
|
|
163
|
+
def test_normal_mode_info(self):
|
|
164
|
+
"""Test mode info in NORMAL mode."""
|
|
165
|
+
info = get_mode_info()
|
|
166
|
+
assert info["mode"] == "normal"
|
|
167
|
+
assert "All operations allowed" in info["description"]
|
|
168
|
+
assert info["blocked_operations"] == []
|
|
169
|
+
|
|
170
|
+
def test_read_only_mode_info(self):
|
|
171
|
+
"""Test mode info in READ_ONLY mode."""
|
|
172
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
173
|
+
info = get_mode_info()
|
|
174
|
+
assert info["mode"] == "read_only"
|
|
175
|
+
assert "read" in info["description"].lower()
|
|
176
|
+
assert len(info["blocked_operations"]) > 0
|
|
177
|
+
assert "delete_pod" in info["blocked_operations"]
|
|
178
|
+
assert "run_pod" in info["blocked_operations"]
|
|
179
|
+
|
|
180
|
+
def test_disable_destructive_mode_info(self):
|
|
181
|
+
"""Test mode info in DISABLE_DESTRUCTIVE mode."""
|
|
182
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
183
|
+
info = get_mode_info()
|
|
184
|
+
assert info["mode"] == "disable_destructive"
|
|
185
|
+
assert "delete" in info["description"].lower()
|
|
186
|
+
assert len(info["blocked_operations"]) > 0
|
|
187
|
+
assert "delete_pod" in info["blocked_operations"]
|
|
188
|
+
# Non-destructive writes should not be blocked
|
|
189
|
+
assert "scale_deployment" not in info["blocked_operations"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestOperationCategories:
|
|
193
|
+
"""Test that operations are categorized correctly."""
|
|
194
|
+
|
|
195
|
+
def test_all_destructive_ops_are_write_ops(self):
|
|
196
|
+
"""Test that all destructive operations are also write operations."""
|
|
197
|
+
for op in DESTRUCTIVE_OPERATIONS:
|
|
198
|
+
assert op in WRITE_OPERATIONS, f"{op} is destructive but not in WRITE_OPERATIONS"
|
|
199
|
+
|
|
200
|
+
def test_expected_write_operations_exist(self):
|
|
201
|
+
"""Test that expected write operations are defined."""
|
|
202
|
+
expected = [
|
|
203
|
+
"run_pod", "delete_pod",
|
|
204
|
+
"scale_deployment", "restart_deployment",
|
|
205
|
+
"install_helm_chart", "uninstall_helm_chart",
|
|
206
|
+
"apply_manifest", "delete_resource",
|
|
207
|
+
]
|
|
208
|
+
for op in expected:
|
|
209
|
+
assert op in WRITE_OPERATIONS, f"Expected {op} in WRITE_OPERATIONS"
|
|
210
|
+
|
|
211
|
+
def test_expected_destructive_operations_exist(self):
|
|
212
|
+
"""Test that expected destructive operations are defined."""
|
|
213
|
+
expected = [
|
|
214
|
+
"delete_pod", "delete_deployment", "delete_namespace",
|
|
215
|
+
"delete_resource", "uninstall_helm_chart",
|
|
216
|
+
]
|
|
217
|
+
for op in expected:
|
|
218
|
+
assert op in DESTRUCTIVE_OPERATIONS, f"Expected {op} in DESTRUCTIVE_OPERATIONS"
|
|
File without changes
|
{kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|