kaos-cli 0.7.4__tar.gz → 0.8.0__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.8.0}/PKG-INFO +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/__init__.py +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/deploy.py +5 -10
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/__init__.py +40 -5
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/auth.py +306 -33
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/infra.py +26 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/deploy.py +5 -10
- kaos_cli-0.8.0/kaos_cli/utils/wait.py +56 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/PKG-INFO +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/SOURCES.txt +2 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/pyproject.toml +1 -1
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_cli_integration.py +164 -22
- kaos_cli-0.8.0/tests/test_deploy_wait.py +103 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/README.md +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/a2a.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/build.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/init.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/memory.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/run.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/status.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/agent/tools.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/consent.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/grant.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/auth/login.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/cluster_http.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/config.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/config_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/install/observability.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/main.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/build.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/crud.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/deploy.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/init.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/mcp/run.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memory/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memorystore/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/memorystore/create.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/modelapi/invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/proxy.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/create_rbac.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/install.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/runtimes.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/system/status.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/ui.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/__init__.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/crud.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli/utils/port_forward.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/dependency_links.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/entry_points.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/requires.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/kaos_cli.egg-info/top_level.txt +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/setup.cfg +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_agent_invoke.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_agent_tools.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_auth_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_config_cli.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/tests/test_install_surface.py +0 -0
- {kaos_cli-0.7.4 → kaos_cli-0.8.0}/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)
|
|
@@ -38,6 +38,9 @@ DEFAULT_CREDENTIAL_SECRET_PREFIX = "kaos-aib"
|
|
|
38
38
|
AUTH_ENDUSER_PORT = 8000
|
|
39
39
|
AUTH_ADMIN_PORT = 14000
|
|
40
40
|
AUTH_EXTPROC_PORT = 50051
|
|
41
|
+
DEFAULT_AIB_EXTPROC_IMAGE = (
|
|
42
|
+
"ghcr.io/zalando-incubator/agentic-identity-broker-extproc:v0.1.8"
|
|
43
|
+
)
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
# Independent agent and user identity modes selected by the install flags.
|
|
@@ -121,8 +124,8 @@ from .auth import (
|
|
|
121
124
|
_build_aib_broker_public_url_args, _build_auth_operator_args,
|
|
122
125
|
_build_token_exchange_aib_args, _default_auth_admin_url,
|
|
123
126
|
_default_auth_issuer, _default_user_auth_issuer, _install_aib,
|
|
124
|
-
_install_keycloak, _keycloak_dev_manifests,
|
|
125
|
-
_keycloak_realm_json,
|
|
127
|
+
_install_aib_extproc, _install_keycloak, _keycloak_dev_manifests,
|
|
128
|
+
_keycloak_realm_configmap_name, _keycloak_realm_json,
|
|
126
129
|
)
|
|
127
130
|
|
|
128
131
|
|
|
@@ -248,8 +251,23 @@ def install_command(
|
|
|
248
251
|
keycloak_namespace, keycloak_release
|
|
249
252
|
)
|
|
250
253
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
254
|
+
# In exchange mode the public broker and extProc both contact Keycloak
|
|
255
|
+
# during startup, so apply Keycloak first.
|
|
256
|
+
if token_exchange_enabled:
|
|
257
|
+
if not _install_keycloak(
|
|
258
|
+
keycloak_namespace,
|
|
259
|
+
keycloak_release,
|
|
260
|
+
DEFAULT_USER_AUTH_REALM,
|
|
261
|
+
user_auth_audience,
|
|
262
|
+
keycloak_chart_path,
|
|
263
|
+
wait,
|
|
264
|
+
token_exchange_enabled,
|
|
265
|
+
):
|
|
266
|
+
typer.echo(
|
|
267
|
+
"Warning: Keycloak installation failed, continuing...", err=True
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Install the broker from the public chart checkout when provided.
|
|
253
271
|
if (identity_provider == "aib" or token_exchange_enabled) and aib_chart_path:
|
|
254
272
|
if token_exchange_enabled:
|
|
255
273
|
keycloak_issuer = user_auth_issuer or _default_user_auth_issuer(
|
|
@@ -269,6 +287,7 @@ def install_command(
|
|
|
269
287
|
aib_values_path,
|
|
270
288
|
wait,
|
|
271
289
|
extra_set=aib_extra_set,
|
|
290
|
+
seed_identity_bootstrap=identity_provider == "aib",
|
|
272
291
|
):
|
|
273
292
|
typer.echo(
|
|
274
293
|
"Warning: identity broker installation failed, continuing...",
|
|
@@ -285,7 +304,7 @@ def install_command(
|
|
|
285
304
|
|
|
286
305
|
# Keycloak backs the user plane and the OIDC DCR agent mode. Install it
|
|
287
306
|
# when either plane selects it.
|
|
288
|
-
if user_auth or identity_provider == "oidc":
|
|
307
|
+
if (user_auth or identity_provider == "oidc") and not token_exchange_enabled:
|
|
289
308
|
if not _install_keycloak(
|
|
290
309
|
keycloak_namespace,
|
|
291
310
|
keycloak_release,
|
|
@@ -299,6 +318,17 @@ def install_command(
|
|
|
299
318
|
"Warning: Keycloak installation failed, continuing...", err=True
|
|
300
319
|
)
|
|
301
320
|
|
|
321
|
+
if token_exchange_enabled and not _install_aib_extproc(
|
|
322
|
+
auth_namespace,
|
|
323
|
+
auth_release,
|
|
324
|
+
user_auth_issuer
|
|
325
|
+
or _default_user_auth_issuer(keycloak_namespace, keycloak_release),
|
|
326
|
+
wait,
|
|
327
|
+
):
|
|
328
|
+
typer.echo(
|
|
329
|
+
"Warning: AIB extProc installation failed, continuing...", err=True
|
|
330
|
+
)
|
|
331
|
+
|
|
302
332
|
# Phase 2: Wait for infra that the operator depends on
|
|
303
333
|
if gateway_enabled:
|
|
304
334
|
_wait_for_gateway_class()
|
|
@@ -432,6 +462,11 @@ def install_command(
|
|
|
432
462
|
policy_rego_override=policy_rego_override,
|
|
433
463
|
policy_configmap_name=policy_configmap_name or "",
|
|
434
464
|
policy_configmap_namespace=policy_configmap_namespace or "",
|
|
465
|
+
default_permission_set=(
|
|
466
|
+
"kaos-identity"
|
|
467
|
+
if identity_provider == "aib" and aib_chart_path
|
|
468
|
+
else ""
|
|
469
|
+
),
|
|
435
470
|
)
|
|
436
471
|
)
|
|
437
472
|
if token_exchange_enabled:
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Authentication provider installation and Helm argument helpers."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from urllib.parse import urlsplit
|
|
4
5
|
|
|
6
|
+
import httpx
|
|
5
7
|
import typer
|
|
6
8
|
|
|
9
|
+
from kaos_cli.cluster_http import local_service_url
|
|
10
|
+
|
|
7
11
|
from . import (
|
|
8
|
-
AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT,
|
|
12
|
+
AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT, AUTH_EXTPROC_PORT,
|
|
13
|
+
DEFAULT_AGENT_AUTH_AUDIENCE, DEFAULT_AIB_EXTPROC_IMAGE,
|
|
9
14
|
DEFAULT_KEYCLOAK_ADMIN_PASSWORD, DEFAULT_KEYCLOAK_ADMIN_USER,
|
|
10
15
|
DEFAULT_KEYCLOAK_IMAGE, DEFAULT_OIDC_REGISTRATION_SECRET_KEY,
|
|
11
16
|
DEFAULT_OIDC_REGISTRATION_SECRET_NAME, DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
|
|
@@ -64,6 +69,7 @@ def _build_auth_operator_args(
|
|
|
64
69
|
policy_rego_override: bool = False,
|
|
65
70
|
policy_configmap_name: str = "",
|
|
66
71
|
policy_configmap_namespace: str = "",
|
|
72
|
+
default_permission_set: str = "",
|
|
67
73
|
) -> list[str]:
|
|
68
74
|
"""Build the operator Helm --set arguments that enable agent-auth wiring.
|
|
69
75
|
|
|
@@ -101,6 +107,14 @@ def _build_auth_operator_args(
|
|
|
101
107
|
)
|
|
102
108
|
if identity_provider == "aib" and admin_url:
|
|
103
109
|
args.extend(["--set", f"security.agentAuth.adminUrl={admin_url}"])
|
|
110
|
+
if default_permission_set:
|
|
111
|
+
args.extend(
|
|
112
|
+
[
|
|
113
|
+
"--set",
|
|
114
|
+
"security.agentAuth.defaultPermissionSet="
|
|
115
|
+
f"{default_permission_set}",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
104
118
|
if identity_provider == "oidc" and oidc_registration_secret_name:
|
|
105
119
|
args.extend(
|
|
106
120
|
[
|
|
@@ -179,8 +193,9 @@ def _install_aib(
|
|
|
179
193
|
values_path: str | None,
|
|
180
194
|
wait: bool,
|
|
181
195
|
extra_set: list[str] | None = None,
|
|
196
|
+
seed_identity_bootstrap: bool = True,
|
|
182
197
|
) -> bool:
|
|
183
|
-
"""Install the identity
|
|
198
|
+
"""Install the public identity-broker chart from a local v0.1.8 checkout."""
|
|
184
199
|
typer.echo("Installing identity broker...")
|
|
185
200
|
helm_args = [
|
|
186
201
|
"upgrade",
|
|
@@ -203,9 +218,111 @@ def _install_aib(
|
|
|
203
218
|
typer.echo(f"Error installing identity broker: {result.stderr}", err=True)
|
|
204
219
|
return False
|
|
205
220
|
|
|
221
|
+
if seed_identity_bootstrap and not _seed_aib_identity_bootstrap(
|
|
222
|
+
namespace, release
|
|
223
|
+
):
|
|
224
|
+
return False
|
|
225
|
+
|
|
206
226
|
typer.echo(f"✅ Identity broker installed in '{namespace}' namespace")
|
|
207
227
|
return True
|
|
208
228
|
|
|
229
|
+
|
|
230
|
+
AIB_BOOTSTRAP_SERVICE = "kaos-identity-placeholder"
|
|
231
|
+
AIB_BOOTSTRAP_PERMISSION_SET = "kaos-identity"
|
|
232
|
+
AIB_BOOTSTRAP_DESCRIPTION = (
|
|
233
|
+
"KAOS identity-only bootstrap; remove once AIB allows agents without permission sets"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _items(response: httpx.Response) -> list[dict]:
|
|
238
|
+
response.raise_for_status()
|
|
239
|
+
payload = response.json()
|
|
240
|
+
return payload if isinstance(payload, list) else payload.get("items", [])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _seed_aib_identity_bootstrap(namespace: str, release: str) -> bool:
|
|
244
|
+
"""Idempotently seed the public AIB permission-set registration bridge."""
|
|
245
|
+
rollout = _root()._run_kubectl(
|
|
246
|
+
[
|
|
247
|
+
"rollout",
|
|
248
|
+
"status",
|
|
249
|
+
f"deployment/{_auth_broker_fullname(release)}",
|
|
250
|
+
"-n",
|
|
251
|
+
namespace,
|
|
252
|
+
"--timeout=120s",
|
|
253
|
+
],
|
|
254
|
+
check=False,
|
|
255
|
+
)
|
|
256
|
+
if rollout.returncode != 0:
|
|
257
|
+
typer.echo(f"Error waiting for identity broker: {rollout.stderr}", err=True)
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
admin_url = _default_auth_admin_url(namespace, release)
|
|
261
|
+
headers = {
|
|
262
|
+
"Host": urlsplit(admin_url).netloc,
|
|
263
|
+
"X-Remote-User": "kaos-operator",
|
|
264
|
+
}
|
|
265
|
+
try:
|
|
266
|
+
with local_service_url(admin_url) as local_url:
|
|
267
|
+
base = local_url.rstrip("/")
|
|
268
|
+
with httpx.Client(headers=headers, timeout=30.0) as client:
|
|
269
|
+
services = _items(client.get(f"{base}/services"))
|
|
270
|
+
service = next(
|
|
271
|
+
(
|
|
272
|
+
item
|
|
273
|
+
for item in services
|
|
274
|
+
if item.get("canonical_id") == AIB_BOOTSTRAP_SERVICE
|
|
275
|
+
or item.get("display_name") == AIB_BOOTSTRAP_SERVICE
|
|
276
|
+
),
|
|
277
|
+
None,
|
|
278
|
+
)
|
|
279
|
+
if service is None:
|
|
280
|
+
response = client.post(
|
|
281
|
+
f"{base}/services",
|
|
282
|
+
json={
|
|
283
|
+
"canonical_id": AIB_BOOTSTRAP_SERVICE,
|
|
284
|
+
# AIB services have no description field, so the
|
|
285
|
+
# descriptive marker lives in display_name.
|
|
286
|
+
"display_name": AIB_BOOTSTRAP_DESCRIPTION,
|
|
287
|
+
"client_id": AIB_BOOTSTRAP_SERVICE,
|
|
288
|
+
"token_endpoint_auth_method": "none",
|
|
289
|
+
"issuer_uri": "https://kaos-identity-placeholder.invalid",
|
|
290
|
+
"discovery": {"enable_discovery": False},
|
|
291
|
+
"endpoints": {
|
|
292
|
+
"authorize_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/authorize",
|
|
293
|
+
"token_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/token",
|
|
294
|
+
},
|
|
295
|
+
"scopes": [],
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
response.raise_for_status()
|
|
299
|
+
service = response.json()
|
|
300
|
+
|
|
301
|
+
permission_sets = _items(client.get(f"{base}/permission-sets"))
|
|
302
|
+
if not any(
|
|
303
|
+
item.get("name") == AIB_BOOTSTRAP_PERMISSION_SET
|
|
304
|
+
for item in permission_sets
|
|
305
|
+
):
|
|
306
|
+
response = client.post(
|
|
307
|
+
f"{base}/permission-sets",
|
|
308
|
+
json={
|
|
309
|
+
"canonical_id": AIB_BOOTSTRAP_PERMISSION_SET,
|
|
310
|
+
"name": AIB_BOOTSTRAP_PERMISSION_SET,
|
|
311
|
+
"description": AIB_BOOTSTRAP_DESCRIPTION,
|
|
312
|
+
"service_scopes": [
|
|
313
|
+
{"service_id": service["id"], "scopes": []}
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
except (httpx.HTTPError, KeyError, RuntimeError, ValueError) as exc:
|
|
319
|
+
typer.echo(f"Error seeding identity broker: {exc}", err=True)
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
typer.echo("✅ Seeded AIB permission set 'kaos-identity'")
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
|
|
209
326
|
def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
|
|
210
327
|
"""Set the broker enduser ``public_url`` to its in-cluster service URL.
|
|
211
328
|
|
|
@@ -219,25 +336,17 @@ def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
|
|
|
219
336
|
"""
|
|
220
337
|
return ["--set", f"broker.server.enduser.publicUrl={public_url}"]
|
|
221
338
|
|
|
339
|
+
|
|
222
340
|
def _build_token_exchange_aib_args(
|
|
223
341
|
auth_namespace: str,
|
|
224
342
|
auth_release: str,
|
|
225
343
|
keycloak_issuer: str,
|
|
226
344
|
) -> list[str]:
|
|
227
|
-
"""Configure
|
|
345
|
+
"""Configure public AIB v0.1.8 for Keycloak-backed token exchange."""
|
|
228
346
|
aib_issuer = _default_auth_issuer(auth_namespace, auth_release)
|
|
229
347
|
keycloak_token_endpoint = f"{keycloak_issuer}/protocol/openid-connect/token"
|
|
230
348
|
keycloak_authorize_endpoint = f"{keycloak_issuer}/protocol/openid-connect/auth"
|
|
231
|
-
|
|
232
|
-
{"name": "EXTPROC_OAUTH2_ISSUER", "value": keycloak_issuer},
|
|
233
|
-
{"name": "EXTPROC_OAUTH2_CLIENT_ID", "value": DEFAULT_USER_AUTH_CLIENT_ID},
|
|
234
|
-
{
|
|
235
|
-
"name": "EXTPROC_OAUTH2_CLIENT_SECRET",
|
|
236
|
-
"value": DEFAULT_USER_AUTH_CLIENT_SECRET,
|
|
237
|
-
},
|
|
238
|
-
{"name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE", "value": "access_token"},
|
|
239
|
-
]
|
|
240
|
-
return [
|
|
349
|
+
args = [
|
|
241
350
|
"--set",
|
|
242
351
|
f"broker.server.enduser.publicUrl={aib_issuer}",
|
|
243
352
|
"--set",
|
|
@@ -248,7 +357,8 @@ def _build_token_exchange_aib_args(
|
|
|
248
357
|
"broker.oauth2AuthorizationServer.proxy.upstreamAuthorizeEndpoint="
|
|
249
358
|
f"{keycloak_authorize_endpoint}",
|
|
250
359
|
"--set",
|
|
251
|
-
|
|
360
|
+
"broker.oauth2AuthorizationServer.proxy.upstreamTokenEndpoint="
|
|
361
|
+
f"{keycloak_token_endpoint}",
|
|
252
362
|
"--set",
|
|
253
363
|
f"broker.tokenExchange.expectedAudience={DEFAULT_TOKEN_EXCHANGE_AUDIENCE}",
|
|
254
364
|
"--set",
|
|
@@ -261,13 +371,153 @@ def _build_token_exchange_aib_args(
|
|
|
261
371
|
"--set",
|
|
262
372
|
"broker.tokenExchange.authorization.cel.expression="
|
|
263
373
|
f'client_assertion.azp == "{DEFAULT_USER_AUTH_CLIENT_ID}"',
|
|
264
|
-
"--set",
|
|
265
|
-
"extProc.enabled=true",
|
|
266
|
-
"--set",
|
|
267
|
-
f"extProc.oauth2.clientCredentialsEndpoint={keycloak_token_endpoint}",
|
|
268
|
-
"--set-json",
|
|
269
|
-
f"extProc.extraEnv={json.dumps(extra_env, separators=(',', ':'))}",
|
|
270
374
|
]
|
|
375
|
+
if keycloak_issuer.startswith("http://"):
|
|
376
|
+
args.extend(
|
|
377
|
+
["--set", "broker.security.skipThirdpartyHttpsValidation=true"]
|
|
378
|
+
)
|
|
379
|
+
return args
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _auth_extproc_name(auth_release: str) -> str:
|
|
383
|
+
return f"{auth_release}-agentic-identity-broker-extproc"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _aib_extproc_manifests(
|
|
387
|
+
namespace: str, release: str, keycloak_issuer: str
|
|
388
|
+
) -> list[dict]:
|
|
389
|
+
"""Build the public extProc workload using its v0.1.8 env contract."""
|
|
390
|
+
name = _auth_extproc_name(release)
|
|
391
|
+
labels = {"app.kubernetes.io/name": name}
|
|
392
|
+
secret_name = f"{name}-credentials"
|
|
393
|
+
token_endpoint = f"{_default_auth_issuer(namespace, release)}/oauth2/token"
|
|
394
|
+
client_credentials_endpoint = (
|
|
395
|
+
f"{keycloak_issuer}/protocol/openid-connect/token"
|
|
396
|
+
)
|
|
397
|
+
secret = {
|
|
398
|
+
"apiVersion": "v1",
|
|
399
|
+
"kind": "Secret",
|
|
400
|
+
"metadata": {"name": secret_name, "namespace": namespace},
|
|
401
|
+
"type": "Opaque",
|
|
402
|
+
"stringData": {"client-secret": DEFAULT_USER_AUTH_CLIENT_SECRET},
|
|
403
|
+
}
|
|
404
|
+
deployment = {
|
|
405
|
+
"apiVersion": "apps/v1",
|
|
406
|
+
"kind": "Deployment",
|
|
407
|
+
"metadata": {"name": name, "namespace": namespace, "labels": labels},
|
|
408
|
+
"spec": {
|
|
409
|
+
"replicas": 1,
|
|
410
|
+
"selector": {"matchLabels": labels},
|
|
411
|
+
"template": {
|
|
412
|
+
"metadata": {"labels": labels},
|
|
413
|
+
"spec": {
|
|
414
|
+
"containers": [
|
|
415
|
+
{
|
|
416
|
+
"name": "extproc",
|
|
417
|
+
"image": DEFAULT_AIB_EXTPROC_IMAGE,
|
|
418
|
+
"imagePullPolicy": "IfNotPresent",
|
|
419
|
+
"ports": [
|
|
420
|
+
{"name": "grpc", "containerPort": AUTH_EXTPROC_PORT}
|
|
421
|
+
],
|
|
422
|
+
"env": [
|
|
423
|
+
{
|
|
424
|
+
"name": "EXTPROC_OAUTH2_TOKEN_ENDPOINT",
|
|
425
|
+
"value": token_endpoint,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"name": "EXTPROC_OAUTH2_ISSUER",
|
|
429
|
+
"value": keycloak_issuer,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "EXTPROC_OAUTH2_CLIENT_ID",
|
|
433
|
+
"value": DEFAULT_USER_AUTH_CLIENT_ID,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "EXTPROC_OAUTH2_CLIENT_SECRET",
|
|
437
|
+
"valueFrom": {
|
|
438
|
+
"secretKeyRef": {
|
|
439
|
+
"name": secret_name,
|
|
440
|
+
"key": "client-secret",
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"name": (
|
|
446
|
+
"EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT"
|
|
447
|
+
),
|
|
448
|
+
"value": client_credentials_endpoint,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE",
|
|
452
|
+
"value": "access_token",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"name": "EXTPROC_OAUTH2_TLS_ALLOW_HTTP",
|
|
456
|
+
"value": "true",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"name": "EXTPROC_GRPC_PORT",
|
|
460
|
+
"value": str(AUTH_EXTPROC_PORT),
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
"readinessProbe": {
|
|
464
|
+
"tcpSocket": {"port": "grpc"},
|
|
465
|
+
"periodSeconds": 2,
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
service = {
|
|
474
|
+
"apiVersion": "v1",
|
|
475
|
+
"kind": "Service",
|
|
476
|
+
"metadata": {"name": name, "namespace": namespace, "labels": labels},
|
|
477
|
+
"spec": {
|
|
478
|
+
"selector": labels,
|
|
479
|
+
"ports": [
|
|
480
|
+
{
|
|
481
|
+
"name": "grpc",
|
|
482
|
+
"port": AUTH_EXTPROC_PORT,
|
|
483
|
+
"targetPort": "grpc",
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
return [secret, deployment, service]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _install_aib_extproc(
|
|
492
|
+
namespace: str, release: str, keycloak_issuer: str, wait: bool
|
|
493
|
+
) -> bool:
|
|
494
|
+
"""Apply KAOS-owned extProc resources for the public AIB release."""
|
|
495
|
+
typer.echo("Installing AIB token-exchange extProc...")
|
|
496
|
+
for manifest in _aib_extproc_manifests(namespace, release, keycloak_issuer):
|
|
497
|
+
result = _root()._run_kubectl(
|
|
498
|
+
["apply", "-f", "-"], check=False, input=json.dumps(manifest)
|
|
499
|
+
)
|
|
500
|
+
if result.returncode != 0:
|
|
501
|
+
typer.echo(f"Error installing AIB extProc: {result.stderr}", err=True)
|
|
502
|
+
return False
|
|
503
|
+
if wait:
|
|
504
|
+
result = _root()._run_kubectl(
|
|
505
|
+
[
|
|
506
|
+
"rollout",
|
|
507
|
+
"status",
|
|
508
|
+
f"deployment/{_auth_extproc_name(release)}",
|
|
509
|
+
"-n",
|
|
510
|
+
namespace,
|
|
511
|
+
"--timeout=120s",
|
|
512
|
+
],
|
|
513
|
+
check=False,
|
|
514
|
+
)
|
|
515
|
+
if result.returncode != 0:
|
|
516
|
+
typer.echo(f"Error waiting for AIB extProc: {result.stderr}", err=True)
|
|
517
|
+
return False
|
|
518
|
+
typer.echo(f"✅ AIB extProc installed in '{namespace}' namespace")
|
|
519
|
+
return True
|
|
520
|
+
|
|
271
521
|
|
|
272
522
|
def _keycloak_realm_json(
|
|
273
523
|
realm: str,
|
|
@@ -276,6 +526,7 @@ def _keycloak_realm_json(
|
|
|
276
526
|
audience: str,
|
|
277
527
|
username: str,
|
|
278
528
|
password: str,
|
|
529
|
+
token_exchange_enabled: bool = False,
|
|
279
530
|
) -> dict:
|
|
280
531
|
"""Build a minimal Keycloak realm definition for non-interactive validation.
|
|
281
532
|
|
|
@@ -285,6 +536,32 @@ def _keycloak_realm_json(
|
|
|
285
536
|
and group-based AccessGrants require.
|
|
286
537
|
"""
|
|
287
538
|
|
|
539
|
+
agent_audience_mappers = [
|
|
540
|
+
{
|
|
541
|
+
"name": "kaos-agent-audience",
|
|
542
|
+
"protocol": "openid-connect",
|
|
543
|
+
"protocolMapper": "oidc-audience-mapper",
|
|
544
|
+
"config": {
|
|
545
|
+
"included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
|
|
546
|
+
"id.token.claim": "false",
|
|
547
|
+
"access.token.claim": "true",
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
if token_exchange_enabled:
|
|
552
|
+
agent_audience_mappers.append(
|
|
553
|
+
{
|
|
554
|
+
"name": "token-exchange-audience",
|
|
555
|
+
"protocol": "openid-connect",
|
|
556
|
+
"protocolMapper": "oidc-audience-mapper",
|
|
557
|
+
"config": {
|
|
558
|
+
"included.custom.audience": DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
|
|
559
|
+
"id.token.claim": "false",
|
|
560
|
+
"access.token.claim": "true",
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
|
|
288
565
|
return {
|
|
289
566
|
"realm": realm,
|
|
290
567
|
"enabled": True,
|
|
@@ -293,18 +570,7 @@ def _keycloak_realm_json(
|
|
|
293
570
|
"name": "kaos-agent-audience",
|
|
294
571
|
"protocol": "openid-connect",
|
|
295
572
|
"attributes": {"include.in.token.scope": "false"},
|
|
296
|
-
"protocolMappers":
|
|
297
|
-
{
|
|
298
|
-
"name": "kaos-agent-audience",
|
|
299
|
-
"protocol": "openid-connect",
|
|
300
|
-
"protocolMapper": "oidc-audience-mapper",
|
|
301
|
-
"config": {
|
|
302
|
-
"included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
|
|
303
|
-
"id.token.claim": "false",
|
|
304
|
-
"access.token.claim": "true",
|
|
305
|
-
},
|
|
306
|
-
}
|
|
307
|
-
],
|
|
573
|
+
"protocolMappers": agent_audience_mappers,
|
|
308
574
|
}
|
|
309
575
|
],
|
|
310
576
|
"defaultDefaultClientScopes": ["kaos-agent-audience"],
|
|
@@ -385,15 +651,18 @@ def _keycloak_realm_json(
|
|
|
385
651
|
],
|
|
386
652
|
}
|
|
387
653
|
|
|
654
|
+
|
|
388
655
|
def _keycloak_realm_configmap_name(release: str) -> str:
|
|
389
656
|
"""Name of the ConfigMap holding the imported realm definition."""
|
|
390
657
|
return f"{release}-realm-import"
|
|
391
658
|
|
|
659
|
+
|
|
392
660
|
def _bootstrap_keycloak_realm(
|
|
393
661
|
namespace: str,
|
|
394
662
|
release: str,
|
|
395
663
|
realm: str,
|
|
396
664
|
audience: str,
|
|
665
|
+
token_exchange_enabled: bool = False,
|
|
397
666
|
) -> bool:
|
|
398
667
|
"""Create the realm-import ConfigMap consumed by Keycloak's --import-realm.
|
|
399
668
|
|
|
@@ -409,6 +678,7 @@ def _bootstrap_keycloak_realm(
|
|
|
409
678
|
audience,
|
|
410
679
|
DEFAULT_USER_AUTH_TEST_USER,
|
|
411
680
|
DEFAULT_USER_AUTH_TEST_PASSWORD,
|
|
681
|
+
token_exchange_enabled,
|
|
412
682
|
),
|
|
413
683
|
indent=2,
|
|
414
684
|
)
|
|
@@ -521,6 +791,7 @@ def _keycloak_dev_manifests(
|
|
|
521
791
|
}
|
|
522
792
|
return [deployment, service]
|
|
523
793
|
|
|
794
|
+
|
|
524
795
|
def _install_keycloak(
|
|
525
796
|
namespace: str,
|
|
526
797
|
release: str,
|
|
@@ -537,7 +808,9 @@ def _install_keycloak(
|
|
|
537
808
|
dev path); otherwise a self-contained dev deployment is applied via kubectl.
|
|
538
809
|
"""
|
|
539
810
|
typer.echo("Installing user identity provider (Keycloak)...")
|
|
540
|
-
if not _bootstrap_keycloak_realm(
|
|
811
|
+
if not _bootstrap_keycloak_realm(
|
|
812
|
+
namespace, release, realm, audience, token_exchange_enabled
|
|
813
|
+
):
|
|
541
814
|
return False
|
|
542
815
|
|
|
543
816
|
if chart_path:
|
|
@@ -90,6 +90,32 @@ def _install_gateway_api(enable_backend: bool = False) -> bool:
|
|
|
90
90
|
typer.echo(f"Error installing Envoy Gateway: {result.stderr}", err=True)
|
|
91
91
|
return False
|
|
92
92
|
|
|
93
|
+
if enable_backend:
|
|
94
|
+
for args in (
|
|
95
|
+
[
|
|
96
|
+
"rollout",
|
|
97
|
+
"restart",
|
|
98
|
+
"deployment/envoy-gateway",
|
|
99
|
+
"-n",
|
|
100
|
+
"envoy-gateway-system",
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"rollout",
|
|
104
|
+
"status",
|
|
105
|
+
"deployment/envoy-gateway",
|
|
106
|
+
"-n",
|
|
107
|
+
"envoy-gateway-system",
|
|
108
|
+
"--timeout=180s",
|
|
109
|
+
],
|
|
110
|
+
):
|
|
111
|
+
rollout = _root()._run_kubectl(args, check=False)
|
|
112
|
+
if rollout.returncode != 0:
|
|
113
|
+
typer.echo(
|
|
114
|
+
f"Error reloading Envoy Gateway Backend support: {rollout.stderr}",
|
|
115
|
+
err=True,
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
118
|
+
|
|
93
119
|
typer.echo("✅ Envoy Gateway installed")
|
|
94
120
|
return True
|
|
95
121
|
|
|
@@ -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)
|