kaos-cli 0.7.4__tar.gz → 0.7.7__tar.gz
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.
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/PKG-INFO +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/__init__.py +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/deploy.py +5 -10
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/deploy.py +5 -10
- kaos_cli-0.7.7/kaos_cli/utils/wait.py +56 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/PKG-INFO +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/SOURCES.txt +2 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/pyproject.toml +1 -1
- kaos_cli-0.7.7/tests/test_deploy_wait.py +103 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/README.md +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/a2a.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/build.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/init.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/memory.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/run.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/status.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/tools.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/consent.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/grant.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/login.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/cluster_http.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/config.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/config_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/auth.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/infra.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/observability.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/main.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/build.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/crud.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/deploy.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/init.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/run.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memory/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memorystore/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memorystore/create.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/proxy.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/create_rbac.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/install.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/runtimes.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/status.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/ui.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/crud.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/port_forward.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/dependency_links.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/entry_points.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/requires.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/top_level.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/setup.cfg +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_agent_invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_agent_tools.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_auth_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_cli_integration.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_config_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_install_surface.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_memory_cli.py +0 -0
|
@@ -7,6 +7,8 @@ import tempfile
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
+
from kaos_cli.utils.wait import wait_for_deployment_available
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
AGENT_TEMPLATE = """apiVersion: kaos.tools/v1alpha1
|
|
12
14
|
kind: Agent
|
|
@@ -184,16 +186,9 @@ def deploy_agent(
|
|
|
184
186
|
# Wait for deployment if requested
|
|
185
187
|
if wait:
|
|
186
188
|
typer.echo("⏳ Waiting for deployment to be available...")
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
f"deployment/agent-{name}",
|
|
191
|
-
"--for=condition=available",
|
|
192
|
-
f"--timeout={wait_timeout}s",
|
|
193
|
-
]
|
|
194
|
-
if namespace:
|
|
195
|
-
wait_args.extend(["-n", namespace])
|
|
196
|
-
wait_result = subprocess.run(wait_args, capture_output=True, text=True)
|
|
189
|
+
wait_result = wait_for_deployment_available(
|
|
190
|
+
f"agent-{name}", namespace, wait_timeout
|
|
191
|
+
)
|
|
197
192
|
if wait_result.returncode != 0:
|
|
198
193
|
typer.echo(wait_result.stderr or wait_result.stdout, err=True)
|
|
199
194
|
sys.exit(wait_result.returncode)
|
|
@@ -8,6 +8,8 @@ import tempfile
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
+
from kaos_cli.utils.wait import wait_for_deployment_available
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
DEFAULT_WAIT_TIMEOUT = 120
|
|
13
15
|
|
|
@@ -174,16 +176,9 @@ spec:
|
|
|
174
176
|
# Wait for deployment if requested
|
|
175
177
|
if wait:
|
|
176
178
|
typer.echo("⏳ Waiting for deployment to be available...")
|
|
177
|
-
|
|
178
|
-
"
|
|
179
|
-
|
|
180
|
-
f"deployment/modelapi-{name}",
|
|
181
|
-
"--for=condition=available",
|
|
182
|
-
f"--timeout={wait_timeout}s",
|
|
183
|
-
]
|
|
184
|
-
if namespace:
|
|
185
|
-
wait_args.extend(["-n", namespace])
|
|
186
|
-
wait_result = subprocess.run(wait_args, capture_output=True, text=True)
|
|
179
|
+
wait_result = wait_for_deployment_available(
|
|
180
|
+
f"modelapi-{name}", namespace, wait_timeout
|
|
181
|
+
)
|
|
187
182
|
if wait_result.returncode != 0:
|
|
188
183
|
typer.echo(wait_result.stderr or wait_result.stdout, err=True)
|
|
189
184
|
sys.exit(wait_result.returncode)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Wait helpers for resources the operator creates asynchronously."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
NOT_FOUND_POLL_INTERVAL = 2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_not_found(result: subprocess.CompletedProcess) -> bool:
|
|
11
|
+
"""Return True when kubectl failed only because the object does not exist yet."""
|
|
12
|
+
message = f"{result.stderr}{result.stdout}".lower()
|
|
13
|
+
return "notfound" in message or "not found" in message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def wait_for_deployment_available(
|
|
17
|
+
deployment: str,
|
|
18
|
+
namespace: str | None,
|
|
19
|
+
timeout: int,
|
|
20
|
+
) -> subprocess.CompletedProcess:
|
|
21
|
+
"""Wait for a Deployment to exist and report condition=available.
|
|
22
|
+
|
|
23
|
+
`kubectl wait` resolves the object once, up front: if it is not there yet the
|
|
24
|
+
command fails immediately with NotFound, and --timeout only bounds waiting for
|
|
25
|
+
the condition on an object that already exists. The Deployments waited on here
|
|
26
|
+
are created by the operator in response to a custom resource, so calling
|
|
27
|
+
`kubectl wait` straight after `kubectl apply` races the operator's first
|
|
28
|
+
reconcile and loses whenever the reconcile lands a moment later.
|
|
29
|
+
|
|
30
|
+
Retry while the Deployment is still absent, and let `kubectl wait` handle the
|
|
31
|
+
condition itself once it appears. `timeout` bounds the whole operation, so a
|
|
32
|
+
Deployment that never appears fails after the same total wait as one that
|
|
33
|
+
appears but never becomes available.
|
|
34
|
+
"""
|
|
35
|
+
deadline = time.monotonic() + timeout
|
|
36
|
+
while True:
|
|
37
|
+
remaining = deadline - time.monotonic()
|
|
38
|
+
args = [
|
|
39
|
+
"kubectl",
|
|
40
|
+
"wait",
|
|
41
|
+
f"deployment/{deployment}",
|
|
42
|
+
"--for=condition=available",
|
|
43
|
+
f"--timeout={max(1, int(remaining))}s",
|
|
44
|
+
]
|
|
45
|
+
if namespace:
|
|
46
|
+
args.extend(["-n", namespace])
|
|
47
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
48
|
+
|
|
49
|
+
if result.returncode == 0 or not _is_not_found(result):
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# The Deployment has not been created yet. Retry until the deadline.
|
|
53
|
+
remaining = deadline - time.monotonic()
|
|
54
|
+
if remaining <= 0:
|
|
55
|
+
return result
|
|
56
|
+
time.sleep(min(NOT_FOUND_POLL_INTERVAL, remaining))
|
|
@@ -61,10 +61,12 @@ kaos_cli/system/status.py
|
|
|
61
61
|
kaos_cli/utils/__init__.py
|
|
62
62
|
kaos_cli/utils/crud.py
|
|
63
63
|
kaos_cli/utils/port_forward.py
|
|
64
|
+
kaos_cli/utils/wait.py
|
|
64
65
|
tests/test_agent_invoke.py
|
|
65
66
|
tests/test_agent_tools.py
|
|
66
67
|
tests/test_auth_cli.py
|
|
67
68
|
tests/test_cli_integration.py
|
|
68
69
|
tests/test_config_cli.py
|
|
70
|
+
tests/test_deploy_wait.py
|
|
69
71
|
tests/test_install_surface.py
|
|
70
72
|
tests/test_memory_cli.py
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Tests for the deploy wait helper.
|
|
2
|
+
|
|
3
|
+
Regression cover for the create-then-wait race that failed the v0.7.5 and v0.7.6
|
|
4
|
+
release pipelines: `kubectl wait` returns NotFound immediately when the operator
|
|
5
|
+
has not created the Deployment yet, which aborted `kaos modelapi deploy --wait`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from kaos_cli.utils.wait import wait_for_deployment_available
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _result(returncode: int, stderr: str = "", stdout: str = ""):
|
|
15
|
+
return subprocess.CompletedProcess(
|
|
16
|
+
args=["kubectl"], returncode=returncode, stdout=stdout, stderr=stderr
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
NOT_FOUND = _result(
|
|
21
|
+
1, stderr='Error from server (NotFound): deployments.apps "modelapi-x" not found\n'
|
|
22
|
+
)
|
|
23
|
+
AVAILABLE = _result(0, stdout="deployment.apps/modelapi-x condition met\n")
|
|
24
|
+
TIMED_OUT = _result(1, stderr="error: timed out waiting for the condition\n")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_retries_while_deployment_does_not_exist_yet():
|
|
28
|
+
"""The operator creating the Deployment late must not fail the command."""
|
|
29
|
+
responses = [NOT_FOUND, NOT_FOUND, AVAILABLE]
|
|
30
|
+
with patch("subprocess.run", side_effect=responses) as run:
|
|
31
|
+
with patch("time.sleep"):
|
|
32
|
+
result = wait_for_deployment_available("modelapi-x", "ns", 120)
|
|
33
|
+
|
|
34
|
+
assert result.returncode == 0
|
|
35
|
+
assert run.call_count == 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_returns_immediately_once_available():
|
|
39
|
+
with patch("subprocess.run", return_value=AVAILABLE) as run:
|
|
40
|
+
result = wait_for_deployment_available("modelapi-x", "ns", 120)
|
|
41
|
+
|
|
42
|
+
assert result.returncode == 0
|
|
43
|
+
assert run.call_count == 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_does_not_retry_on_a_real_condition_timeout():
|
|
47
|
+
"""A Deployment that exists but never goes available is a genuine failure."""
|
|
48
|
+
with patch("subprocess.run", return_value=TIMED_OUT) as run:
|
|
49
|
+
result = wait_for_deployment_available("modelapi-x", "ns", 120)
|
|
50
|
+
|
|
51
|
+
assert result.returncode == 1
|
|
52
|
+
assert run.call_count == 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_gives_up_when_deployment_never_appears():
|
|
56
|
+
"""A Deployment that never shows up still fails, bounded by the timeout."""
|
|
57
|
+
with patch("subprocess.run", return_value=NOT_FOUND) as run:
|
|
58
|
+
with patch("time.sleep"):
|
|
59
|
+
result = wait_for_deployment_available("modelapi-x", "ns", 0)
|
|
60
|
+
|
|
61
|
+
assert result.returncode == 1
|
|
62
|
+
assert "not found" in result.stderr
|
|
63
|
+
assert run.call_count == 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_passes_namespace_when_given():
|
|
67
|
+
with patch("subprocess.run", return_value=AVAILABLE) as run:
|
|
68
|
+
wait_for_deployment_available("modelapi-x", "my-ns", 120)
|
|
69
|
+
|
|
70
|
+
args = run.call_args[0][0]
|
|
71
|
+
assert args[:3] == ["kubectl", "wait", "deployment/modelapi-x"]
|
|
72
|
+
assert "--for=condition=available" in args
|
|
73
|
+
assert args[-2:] == ["-n", "my-ns"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_omits_namespace_when_not_given():
|
|
77
|
+
with patch("subprocess.run", return_value=AVAILABLE) as run:
|
|
78
|
+
wait_for_deployment_available("modelapi-x", None, 120)
|
|
79
|
+
|
|
80
|
+
assert "-n" not in run.call_args[0][0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_timeout_shrinks_across_retries():
|
|
84
|
+
"""Each retry must pass kubectl the time that is actually left, never the full
|
|
85
|
+
budget again, or the total wait could run to timeout x attempts."""
|
|
86
|
+
clock = {"t": 0.0}
|
|
87
|
+
|
|
88
|
+
def fake_monotonic():
|
|
89
|
+
return clock["t"]
|
|
90
|
+
|
|
91
|
+
def fake_sleep(seconds):
|
|
92
|
+
clock["t"] += seconds
|
|
93
|
+
|
|
94
|
+
with patch("subprocess.run", side_effect=[NOT_FOUND, NOT_FOUND, AVAILABLE]) as run:
|
|
95
|
+
with patch("time.monotonic", side_effect=fake_monotonic):
|
|
96
|
+
with patch("time.sleep", side_effect=fake_sleep):
|
|
97
|
+
wait_for_deployment_available("modelapi-x", "ns", 100)
|
|
98
|
+
|
|
99
|
+
timeouts = [
|
|
100
|
+
next(a for a in call[0][0] if a.startswith("--timeout="))
|
|
101
|
+
for call in run.call_args_list
|
|
102
|
+
]
|
|
103
|
+
assert timeouts == ["--timeout=100s", "--timeout=98s", "--timeout=96s"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|