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.
Files changed (74) hide show
  1. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/PKG-INFO +1 -1
  2. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/__init__.py +1 -1
  3. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/deploy.py +5 -10
  4. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/deploy.py +5 -10
  5. kaos_cli-0.7.7/kaos_cli/utils/wait.py +56 -0
  6. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/PKG-INFO +1 -1
  7. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/SOURCES.txt +2 -0
  8. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/pyproject.toml +1 -1
  9. kaos_cli-0.7.7/tests/test_deploy_wait.py +103 -0
  10. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/README.md +0 -0
  11. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/__init__.py +0 -0
  12. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/a2a.py +0 -0
  13. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/build.py +0 -0
  14. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/init.py +0 -0
  15. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/invoke.py +0 -0
  16. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/memory.py +0 -0
  17. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/run.py +0 -0
  18. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/status.py +0 -0
  19. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/agent/tools.py +0 -0
  20. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/__init__.py +0 -0
  21. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/consent.py +0 -0
  22. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/grant.py +0 -0
  23. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/auth/login.py +0 -0
  24. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/cluster_http.py +0 -0
  25. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/config.py +0 -0
  26. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/config_cli.py +0 -0
  27. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/__init__.py +0 -0
  28. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/auth.py +0 -0
  29. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/infra.py +0 -0
  30. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/install/observability.py +0 -0
  31. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/main.py +0 -0
  32. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/__init__.py +0 -0
  33. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/build.py +0 -0
  34. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/crud.py +0 -0
  35. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/deploy.py +0 -0
  36. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/init.py +0 -0
  37. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/invoke.py +0 -0
  38. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/mcp/run.py +0 -0
  39. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memory/__init__.py +0 -0
  40. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memorystore/__init__.py +0 -0
  41. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/memorystore/create.py +0 -0
  42. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/__init__.py +0 -0
  43. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/modelapi/invoke.py +0 -0
  44. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/proxy.py +0 -0
  45. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/__init__.py +0 -0
  46. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
  47. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
  48. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
  49. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
  50. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
  51. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
  52. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
  53. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
  54. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/__init__.py +0 -0
  55. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/create_rbac.py +0 -0
  56. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/install.py +0 -0
  57. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/runtimes.py +0 -0
  58. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/system/status.py +0 -0
  59. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/ui.py +0 -0
  60. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/__init__.py +0 -0
  61. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/crud.py +0 -0
  62. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli/utils/port_forward.py +0 -0
  63. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/dependency_links.txt +0 -0
  64. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/entry_points.txt +0 -0
  65. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/requires.txt +0 -0
  66. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/kaos_cli.egg-info/top_level.txt +0 -0
  67. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/setup.cfg +0 -0
  68. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_agent_invoke.py +0 -0
  69. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_agent_tools.py +0 -0
  70. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_auth_cli.py +0 -0
  71. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_cli_integration.py +0 -0
  72. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_config_cli.py +0 -0
  73. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_install_surface.py +0 -0
  74. {kaos_cli-0.7.4 → kaos_cli-0.7.7}/tests/test_memory_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaos-cli
3
- Version: 0.7.4
3
+ Version: 0.7.7
4
4
  Summary: CLI for KAOS (K8s Agent Orchestration System)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -5,4 +5,4 @@ try:
5
5
 
6
6
  __version__ = version("kaos-cli")
7
7
  except Exception:
8
- __version__ = "0.7.4.dev0"
8
+ __version__ = "0.7.5.dev0"
@@ -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
- wait_args = [
188
- "kubectl",
189
- "wait",
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
- wait_args = [
178
- "kubectl",
179
- "wait",
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaos-cli
3
- Version: 0.7.4
3
+ Version: 0.7.7
4
4
  Summary: CLI for KAOS (K8s Agent Orchestration System)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kaos-cli"
7
- version = "0.7.4"
7
+ version = "0.7.7"
8
8
  description = "CLI for KAOS (K8s Agent Orchestration System)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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