agentex-sdk 0.1.0a6__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.
Files changed (289) hide show
  1. agentex/__init__.py +103 -0
  2. agentex/_base_client.py +1992 -0
  3. agentex/_client.py +506 -0
  4. agentex/_compat.py +219 -0
  5. agentex/_constants.py +14 -0
  6. agentex/_exceptions.py +108 -0
  7. agentex/_files.py +123 -0
  8. agentex/_models.py +829 -0
  9. agentex/_qs.py +150 -0
  10. agentex/_resource.py +43 -0
  11. agentex/_response.py +830 -0
  12. agentex/_streaming.py +333 -0
  13. agentex/_types.py +219 -0
  14. agentex/_utils/__init__.py +57 -0
  15. agentex/_utils/_logs.py +25 -0
  16. agentex/_utils/_proxy.py +65 -0
  17. agentex/_utils/_reflection.py +42 -0
  18. agentex/_utils/_resources_proxy.py +24 -0
  19. agentex/_utils/_streams.py +12 -0
  20. agentex/_utils/_sync.py +86 -0
  21. agentex/_utils/_transform.py +447 -0
  22. agentex/_utils/_typing.py +151 -0
  23. agentex/_utils/_utils.py +422 -0
  24. agentex/_version.py +4 -0
  25. agentex/lib/.keep +4 -0
  26. agentex/lib/__init__.py +0 -0
  27. agentex/lib/adk/__init__.py +41 -0
  28. agentex/lib/adk/_modules/__init__.py +0 -0
  29. agentex/lib/adk/_modules/acp.py +247 -0
  30. agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
  31. agentex/lib/adk/_modules/agents.py +77 -0
  32. agentex/lib/adk/_modules/events.py +141 -0
  33. agentex/lib/adk/_modules/messages.py +285 -0
  34. agentex/lib/adk/_modules/state.py +291 -0
  35. agentex/lib/adk/_modules/streaming.py +75 -0
  36. agentex/lib/adk/_modules/tasks.py +124 -0
  37. agentex/lib/adk/_modules/tracing.py +194 -0
  38. agentex/lib/adk/providers/__init__.py +9 -0
  39. agentex/lib/adk/providers/_modules/__init__.py +0 -0
  40. agentex/lib/adk/providers/_modules/litellm.py +232 -0
  41. agentex/lib/adk/providers/_modules/openai.py +416 -0
  42. agentex/lib/adk/providers/_modules/sgp.py +85 -0
  43. agentex/lib/adk/utils/__init__.py +5 -0
  44. agentex/lib/adk/utils/_modules/__init__.py +0 -0
  45. agentex/lib/adk/utils/_modules/templating.py +94 -0
  46. agentex/lib/cli/__init__.py +0 -0
  47. agentex/lib/cli/commands/__init__.py +0 -0
  48. agentex/lib/cli/commands/agents.py +328 -0
  49. agentex/lib/cli/commands/init.py +227 -0
  50. agentex/lib/cli/commands/main.py +33 -0
  51. agentex/lib/cli/commands/secrets.py +169 -0
  52. agentex/lib/cli/commands/tasks.py +118 -0
  53. agentex/lib/cli/commands/uv.py +133 -0
  54. agentex/lib/cli/handlers/__init__.py +0 -0
  55. agentex/lib/cli/handlers/agent_handlers.py +160 -0
  56. agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
  57. agentex/lib/cli/handlers/deploy_handlers.py +351 -0
  58. agentex/lib/cli/handlers/run_handlers.py +452 -0
  59. agentex/lib/cli/handlers/secret_handlers.py +670 -0
  60. agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
  61. agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
  62. agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
  63. agentex/lib/cli/templates/default/README.md.j2 +193 -0
  64. agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
  65. agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
  66. agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
  67. agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
  68. agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
  69. agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
  70. agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
  71. agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
  72. agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
  73. agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
  74. agentex/lib/cli/templates/sync/README.md.j2 +293 -0
  75. agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
  76. agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
  77. agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
  78. agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
  79. agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
  80. agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
  81. agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
  82. agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
  83. agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
  84. agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
  85. agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
  86. agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
  87. agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
  88. agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
  89. agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
  90. agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
  91. agentex/lib/cli/utils/cli_utils.py +14 -0
  92. agentex/lib/cli/utils/credential_utils.py +103 -0
  93. agentex/lib/cli/utils/exceptions.py +6 -0
  94. agentex/lib/cli/utils/kubectl_utils.py +135 -0
  95. agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
  96. agentex/lib/core/__init__.py +0 -0
  97. agentex/lib/core/adapters/__init__.py +0 -0
  98. agentex/lib/core/adapters/llm/__init__.py +1 -0
  99. agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
  100. agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
  101. agentex/lib/core/adapters/llm/port.py +24 -0
  102. agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
  103. agentex/lib/core/adapters/streams/port.py +50 -0
  104. agentex/lib/core/clients/__init__.py +1 -0
  105. agentex/lib/core/clients/temporal/__init__.py +0 -0
  106. agentex/lib/core/clients/temporal/temporal_client.py +181 -0
  107. agentex/lib/core/clients/temporal/types.py +47 -0
  108. agentex/lib/core/clients/temporal/utils.py +56 -0
  109. agentex/lib/core/services/__init__.py +0 -0
  110. agentex/lib/core/services/adk/__init__.py +0 -0
  111. agentex/lib/core/services/adk/acp/__init__.py +0 -0
  112. agentex/lib/core/services/adk/acp/acp.py +210 -0
  113. agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
  114. agentex/lib/core/services/adk/agents.py +43 -0
  115. agentex/lib/core/services/adk/events.py +61 -0
  116. agentex/lib/core/services/adk/messages.py +164 -0
  117. agentex/lib/core/services/adk/providers/__init__.py +0 -0
  118. agentex/lib/core/services/adk/providers/litellm.py +256 -0
  119. agentex/lib/core/services/adk/providers/openai.py +723 -0
  120. agentex/lib/core/services/adk/providers/sgp.py +99 -0
  121. agentex/lib/core/services/adk/state.py +120 -0
  122. agentex/lib/core/services/adk/streaming.py +262 -0
  123. agentex/lib/core/services/adk/tasks.py +69 -0
  124. agentex/lib/core/services/adk/tracing.py +36 -0
  125. agentex/lib/core/services/adk/utils/__init__.py +0 -0
  126. agentex/lib/core/services/adk/utils/templating.py +58 -0
  127. agentex/lib/core/temporal/__init__.py +0 -0
  128. agentex/lib/core/temporal/activities/__init__.py +207 -0
  129. agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
  130. agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
  131. agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
  132. agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
  133. agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
  134. agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
  135. agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
  136. agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
  137. agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
  138. agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
  139. agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
  140. agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
  141. agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
  142. agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
  143. agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
  144. agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
  145. agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
  146. agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
  147. agentex/lib/core/temporal/services/__init__.py +0 -0
  148. agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
  149. agentex/lib/core/temporal/types/__init__.py +0 -0
  150. agentex/lib/core/temporal/types/workflow.py +5 -0
  151. agentex/lib/core/temporal/workers/__init__.py +0 -0
  152. agentex/lib/core/temporal/workers/worker.py +162 -0
  153. agentex/lib/core/temporal/workflows/workflow.py +26 -0
  154. agentex/lib/core/tracing/__init__.py +5 -0
  155. agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
  156. agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
  157. agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
  158. agentex/lib/core/tracing/trace.py +311 -0
  159. agentex/lib/core/tracing/tracer.py +70 -0
  160. agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
  161. agentex/lib/environment_variables.py +87 -0
  162. agentex/lib/py.typed +0 -0
  163. agentex/lib/sdk/__init__.py +0 -0
  164. agentex/lib/sdk/config/__init__.py +0 -0
  165. agentex/lib/sdk/config/agent_config.py +61 -0
  166. agentex/lib/sdk/config/agent_manifest.py +219 -0
  167. agentex/lib/sdk/config/build_config.py +35 -0
  168. agentex/lib/sdk/config/deployment_config.py +117 -0
  169. agentex/lib/sdk/config/local_development_config.py +56 -0
  170. agentex/lib/sdk/config/project_config.py +103 -0
  171. agentex/lib/sdk/fastacp/__init__.py +3 -0
  172. agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
  173. agentex/lib/sdk/fastacp/fastacp.py +74 -0
  174. agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
  175. agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
  176. agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
  177. agentex/lib/sdk/fastacp/tests/README.md +297 -0
  178. agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
  179. agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
  180. agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
  181. agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
  182. agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
  183. agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
  184. agentex/lib/sdk/state_machine/__init__.py +6 -0
  185. agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
  186. agentex/lib/sdk/state_machine/state.py +10 -0
  187. agentex/lib/sdk/state_machine/state_machine.py +189 -0
  188. agentex/lib/sdk/state_machine/state_workflow.py +16 -0
  189. agentex/lib/sdk/utils/__init__.py +0 -0
  190. agentex/lib/sdk/utils/messages.py +223 -0
  191. agentex/lib/types/__init__.py +0 -0
  192. agentex/lib/types/acp.py +94 -0
  193. agentex/lib/types/agent_configs.py +79 -0
  194. agentex/lib/types/agent_results.py +29 -0
  195. agentex/lib/types/credentials.py +34 -0
  196. agentex/lib/types/fastacp.py +61 -0
  197. agentex/lib/types/files.py +13 -0
  198. agentex/lib/types/json_rpc.py +49 -0
  199. agentex/lib/types/llm_messages.py +354 -0
  200. agentex/lib/types/task_message_updates.py +171 -0
  201. agentex/lib/types/tracing.py +34 -0
  202. agentex/lib/utils/__init__.py +0 -0
  203. agentex/lib/utils/completions.py +131 -0
  204. agentex/lib/utils/console.py +14 -0
  205. agentex/lib/utils/io.py +29 -0
  206. agentex/lib/utils/iterables.py +14 -0
  207. agentex/lib/utils/json_schema.py +23 -0
  208. agentex/lib/utils/logging.py +31 -0
  209. agentex/lib/utils/mcp.py +17 -0
  210. agentex/lib/utils/model_utils.py +46 -0
  211. agentex/lib/utils/parsing.py +15 -0
  212. agentex/lib/utils/regex.py +6 -0
  213. agentex/lib/utils/temporal.py +13 -0
  214. agentex/py.typed +0 -0
  215. agentex/resources/__init__.py +103 -0
  216. agentex/resources/agents.py +707 -0
  217. agentex/resources/events.py +294 -0
  218. agentex/resources/messages/__init__.py +33 -0
  219. agentex/resources/messages/batch.py +271 -0
  220. agentex/resources/messages/messages.py +492 -0
  221. agentex/resources/spans.py +557 -0
  222. agentex/resources/states.py +544 -0
  223. agentex/resources/tasks.py +615 -0
  224. agentex/resources/tracker.py +384 -0
  225. agentex/types/__init__.py +56 -0
  226. agentex/types/acp_type.py +7 -0
  227. agentex/types/agent.py +29 -0
  228. agentex/types/agent_list_params.py +13 -0
  229. agentex/types/agent_list_response.py +10 -0
  230. agentex/types/agent_rpc_by_name_params.py +21 -0
  231. agentex/types/agent_rpc_params.py +51 -0
  232. agentex/types/agent_rpc_params1.py +21 -0
  233. agentex/types/agent_rpc_response.py +20 -0
  234. agentex/types/agent_rpc_result.py +90 -0
  235. agentex/types/agent_task_tracker.py +34 -0
  236. agentex/types/data_content.py +30 -0
  237. agentex/types/data_content_param.py +31 -0
  238. agentex/types/data_delta.py +14 -0
  239. agentex/types/event.py +29 -0
  240. agentex/types/event_list_params.py +22 -0
  241. agentex/types/event_list_response.py +10 -0
  242. agentex/types/message_author.py +7 -0
  243. agentex/types/message_create_params.py +18 -0
  244. agentex/types/message_list_params.py +14 -0
  245. agentex/types/message_list_response.py +10 -0
  246. agentex/types/message_style.py +7 -0
  247. agentex/types/message_update_params.py +18 -0
  248. agentex/types/messages/__init__.py +8 -0
  249. agentex/types/messages/batch_create_params.py +16 -0
  250. agentex/types/messages/batch_create_response.py +10 -0
  251. agentex/types/messages/batch_update_params.py +16 -0
  252. agentex/types/messages/batch_update_response.py +10 -0
  253. agentex/types/shared/__init__.py +3 -0
  254. agentex/types/shared/task_message_update.py +83 -0
  255. agentex/types/span.py +36 -0
  256. agentex/types/span_create_params.py +40 -0
  257. agentex/types/span_list_params.py +12 -0
  258. agentex/types/span_list_response.py +10 -0
  259. agentex/types/span_update_params.py +37 -0
  260. agentex/types/state.py +25 -0
  261. agentex/types/state_create_params.py +16 -0
  262. agentex/types/state_list_params.py +16 -0
  263. agentex/types/state_list_response.py +10 -0
  264. agentex/types/state_update_params.py +16 -0
  265. agentex/types/task.py +23 -0
  266. agentex/types/task_delete_by_name_response.py +8 -0
  267. agentex/types/task_delete_response.py +8 -0
  268. agentex/types/task_list_response.py +10 -0
  269. agentex/types/task_message.py +33 -0
  270. agentex/types/task_message_content.py +16 -0
  271. agentex/types/task_message_content_param.py +17 -0
  272. agentex/types/task_message_delta.py +16 -0
  273. agentex/types/text_content.py +53 -0
  274. agentex/types/text_content_param.py +54 -0
  275. agentex/types/text_delta.py +14 -0
  276. agentex/types/tool_request_content.py +36 -0
  277. agentex/types/tool_request_content_param.py +37 -0
  278. agentex/types/tool_request_delta.py +18 -0
  279. agentex/types/tool_response_content.py +36 -0
  280. agentex/types/tool_response_content_param.py +36 -0
  281. agentex/types/tool_response_delta.py +18 -0
  282. agentex/types/tracker_list_params.py +16 -0
  283. agentex/types/tracker_list_response.py +10 -0
  284. agentex/types/tracker_update_params.py +19 -0
  285. agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
  286. agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
  287. agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
  288. agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
  289. agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,670 @@
1
+ import base64
2
+ import json
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import questionary
8
+ import typer
9
+ import yaml
10
+ from kubernetes.client.rest import ApiException
11
+ from rich.console import Console
12
+
13
+ from agentex.lib.cli.utils.cli_utils import handle_questionary_cancellation
14
+ from agentex.lib.cli.utils.kubectl_utils import get_k8s_client
15
+ from agentex.lib.cli.utils.kubernetes_secrets_utils import (
16
+ KUBERNETES_SECRET_TO_MANIFEST_KEY,
17
+ KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON,
18
+ KUBERNETES_SECRET_TYPE_OPAQUE,
19
+ VALID_SECRET_TYPES,
20
+ create_image_pull_secret_with_data,
21
+ create_secret_with_data,
22
+ get_secret_data,
23
+ update_image_pull_secret_with_data,
24
+ update_secret_with_data,
25
+ )
26
+ from agentex.lib.sdk.config.agent_config import AgentConfig
27
+ from agentex.lib.sdk.config.agent_manifest import AgentManifest
28
+ from agentex.lib.sdk.config.deployment_config import (
29
+ DeploymentConfig,
30
+ ImagePullSecretConfig,
31
+ InjectedSecretsValues,
32
+ )
33
+ from agentex.lib.types.credentials import CredentialMapping
34
+ from agentex.lib.utils.logging import make_logger
35
+
36
+ logger = make_logger(__name__)
37
+ console = Console()
38
+
39
+
40
+ # TODO: parse this into a Pydantic model.
41
+ def load_values_file(values_path: str) -> dict[str, dict[str, str]]:
42
+ """Load and parse the values file (YAML/JSON)"""
43
+ try:
44
+ path = Path(values_path)
45
+ content = path.read_text()
46
+
47
+ if path.suffix.lower() in [".yaml", ".yml"]:
48
+ data = yaml.safe_load(content)
49
+ elif path.suffix.lower() == ".json":
50
+ data = json.loads(content)
51
+ else:
52
+ # Try YAML first, then JSON
53
+ try:
54
+ data = yaml.safe_load(content)
55
+ except yaml.YAMLError:
56
+ data = json.loads(content)
57
+ return InjectedSecretsValues.model_validate(data).model_dump()
58
+
59
+ except Exception as e:
60
+ raise RuntimeError(
61
+ f"Failed to load values file '{values_path}': {str(e)}"
62
+ ) from e
63
+
64
+
65
+ def interactive_secret_input(secret_name: str, secret_key: str) -> str:
66
+ """Prompt user for secret value with appropriate input method"""
67
+ console.print(
68
+ f"\n[bold]Enter value for secret '[cyan]{secret_name}[/cyan]' key '[cyan]{secret_key}[/cyan]':[/bold]"
69
+ )
70
+
71
+ input_type = questionary.select(
72
+ "What type of value is this?",
73
+ choices=[
74
+ "Simple text",
75
+ "Sensitive/password (hidden input)",
76
+ "Multi-line text",
77
+ "JSON/YAML content",
78
+ "Read from file",
79
+ ],
80
+ ).ask()
81
+
82
+ input_type = handle_questionary_cancellation(input_type, "secret input")
83
+
84
+ if input_type == "Sensitive/password (hidden input)":
85
+ result = questionary.password("Enter value (input will be hidden):").ask()
86
+ return handle_questionary_cancellation(result, "password input")
87
+
88
+ elif input_type == "Multi-line text":
89
+ console.print(
90
+ "[yellow]Enter multi-line text (press Ctrl+D when finished):[/yellow]"
91
+ )
92
+ lines = []
93
+ try:
94
+ while True:
95
+ line = input()
96
+ lines.append(line)
97
+ except EOFError:
98
+ pass
99
+ except KeyboardInterrupt:
100
+ console.print("[yellow]Multi-line input cancelled by user[/yellow]")
101
+ raise typer.Exit(0) # noqa
102
+ return "\n".join(lines)
103
+
104
+ elif input_type == "JSON/YAML content":
105
+ value = questionary.text("Enter JSON/YAML content:").ask()
106
+ value = handle_questionary_cancellation(value, "JSON/YAML input")
107
+ # Validate JSON/YAML format
108
+ try:
109
+ json.loads(value)
110
+ except json.JSONDecodeError:
111
+ try:
112
+ yaml.safe_load(value)
113
+ except yaml.YAMLError:
114
+ console.print(
115
+ "[yellow]Warning: Content doesn't appear to be valid JSON or YAML[/yellow]"
116
+ )
117
+ return value
118
+
119
+ elif input_type == "Read from file":
120
+ file_path = questionary.path("Enter file path:").ask()
121
+ file_path = handle_questionary_cancellation(file_path, "file path input")
122
+ try:
123
+ return Path(file_path).read_text().strip()
124
+ except Exception as e:
125
+ console.print(f"[red]Error reading file: {e}[/red]")
126
+ manual_value = questionary.text("Enter value manually:").ask()
127
+ return handle_questionary_cancellation(manual_value, "manual value input")
128
+
129
+ else: # Simple text
130
+ result = questionary.text("Enter value:").ask()
131
+ return handle_questionary_cancellation(result, "text input")
132
+
133
+
134
+ def get_secret(name: str, namespace: str, context: str | None = None) -> dict:
135
+ """Get details about a secret"""
136
+ v1 = get_k8s_client(context)
137
+
138
+ try:
139
+ secret = v1.read_namespaced_secret(name=name, namespace=namespace)
140
+ return {
141
+ "name": secret.metadata.name,
142
+ "namespace": namespace,
143
+ "created": secret.metadata.creation_timestamp.isoformat(),
144
+ "exists": True,
145
+ }
146
+ except ApiException as e:
147
+ if e.status == 404:
148
+ console.print(
149
+ f"[red]Error: Secret '{name}' not found in namespace '{namespace}'[/red]"
150
+ )
151
+ return {"name": name, "namespace": namespace, "exists": False}
152
+ raise RuntimeError(f"Failed to get secret: {str(e)}") from e
153
+
154
+
155
+ def delete_secret(name: str, namespace: str, context: str | None = None) -> None:
156
+ """Delete a secret"""
157
+ v1 = get_k8s_client(context)
158
+
159
+ try:
160
+ v1.delete_namespaced_secret(name=name, namespace=namespace)
161
+ console.print(
162
+ f"[green]Deleted secret '{name}' from namespace '{namespace}'[/green]"
163
+ )
164
+ except ApiException as e:
165
+ if e.status == 404:
166
+ console.print(
167
+ f"[red]Error: Secret '{name}' not found in namespace '{namespace}'[/red]"
168
+ )
169
+ else:
170
+ console.print(f"[red]Error deleting secret: {e.reason}[/red]")
171
+ raise RuntimeError(f"Failed to delete secret: {str(e)}") from e
172
+
173
+
174
+ def get_kubernetes_secrets_by_type(
175
+ namespace: str, context: str | None = None
176
+ ) -> dict[str, list[dict]]:
177
+ """List metadata about secrets in the namespace"""
178
+ v1 = get_k8s_client(context)
179
+
180
+ try:
181
+ secrets = v1.list_namespaced_secret(namespace=namespace)
182
+ secret_type_to_secret = defaultdict(list)
183
+ for secret in secrets.items:
184
+ if secret.type in VALID_SECRET_TYPES:
185
+ secret_type_to_secret[secret.type].append(
186
+ {
187
+ "name": secret.metadata.name,
188
+ "namespace": namespace,
189
+ "created": secret.metadata.creation_timestamp.isoformat(),
190
+ }
191
+ )
192
+
193
+ return secret_type_to_secret
194
+ except ApiException as e:
195
+ console.print(
196
+ f"[red]Error listing secrets in namespace '{namespace}': {e.reason}[/red]"
197
+ )
198
+ raise RuntimeError(f"Failed to list secrets: {str(e)}") from e
199
+
200
+ # NOTE: This corresponds with KUBERNETES_SECRET_TYPE_OPAQUE
201
+
202
+
203
+ def sync_user_defined_secrets(
204
+ manifest_obj: AgentManifest,
205
+ found_secrets: list[dict],
206
+ values_data: dict[str, Any],
207
+ cluster: str,
208
+ namespace: str,
209
+ interactive: bool,
210
+ changes: dict[str, list[str]],
211
+ ) -> None:
212
+ """Sync user defined secrets between manifest, cluster, and values file"""
213
+ console.print(
214
+ f"[bold]Syncing user defined secrets to cluster: {cluster} namespace: {namespace}[/bold]"
215
+ )
216
+
217
+ # Get the secrets from the cluster using the specified namespace and cluster context
218
+ cluster_secret_names = {secret["name"] for secret in found_secrets}
219
+ # Get the secrets from the manifest
220
+ agent_config: AgentConfig = manifest_obj.agent
221
+ manifest_credentials: list[CredentialMapping] = agent_config.credentials or []
222
+
223
+ if not manifest_credentials:
224
+ console.print("[yellow]No credentials found in manifest[/yellow]")
225
+ return
226
+
227
+ # Build required secrets map from manifest
228
+ required_secrets = {} # {secret_name: {secret_key: env_var_name}}
229
+ for cred in manifest_credentials:
230
+ if cred.secret_name not in required_secrets:
231
+ required_secrets[cred.secret_name] = {}
232
+ required_secrets[cred.secret_name][cred.secret_key] = cred.env_var_name
233
+
234
+ # Process each required secret
235
+ for secret_name, required_keys in required_secrets.items():
236
+ current_secret_data = get_secret_data(secret_name, namespace, cluster)
237
+ new_secret_data = {}
238
+ secret_needs_update = False
239
+
240
+ # Process each required key in this secret
241
+ for secret_key, _ in required_keys.items():
242
+ current_value = current_secret_data.get(secret_key)
243
+
244
+ # Get the new value
245
+ if (
246
+ values_data
247
+ and secret_name in values_data
248
+ and secret_key in values_data[secret_name]
249
+ ):
250
+ new_value = values_data[secret_name][secret_key]
251
+ elif interactive:
252
+ if current_value:
253
+ console.print(
254
+ f"[blue]Secret '{secret_name}' key '{secret_key}' already exists[/blue]"
255
+ )
256
+ update_choice = questionary.select(
257
+ "What would you like to do?",
258
+ choices=[
259
+ "Keep current value",
260
+ "Update with new value",
261
+ "Show current value",
262
+ ],
263
+ ).ask()
264
+ update_choice = handle_questionary_cancellation(
265
+ update_choice, "secret update choice"
266
+ )
267
+
268
+ if update_choice == "Show current value":
269
+ console.print(f"Current value: [dim]{current_value}[/dim]")
270
+ update_choice = questionary.select(
271
+ "What would you like to do?",
272
+ choices=["Keep current value", "Update with new value"],
273
+ ).ask()
274
+ update_choice = handle_questionary_cancellation(
275
+ update_choice, "secret update choice"
276
+ )
277
+
278
+ if update_choice == "Update with new value":
279
+ new_value = interactive_secret_input(secret_name, secret_key)
280
+ else:
281
+ new_value = current_value
282
+ else:
283
+ console.print(
284
+ f"[yellow]Secret '{secret_name}' key '{secret_key}' does not exist[/yellow]"
285
+ )
286
+ new_value = interactive_secret_input(secret_name, secret_key)
287
+ else:
288
+ raise RuntimeError(
289
+ f"No value provided for secret '{secret_name}' key '{secret_key}'. Provide values file or use interactive mode."
290
+ )
291
+
292
+ # Must be a string because kubernetes always expects a
293
+ new_value = str(new_value)
294
+ new_secret_data[secret_key] = new_value
295
+
296
+ # Check if value changed
297
+ if current_value != new_value:
298
+ secret_needs_update = True
299
+ else:
300
+ changes["noop"].append(
301
+ f"Secret '{secret_name}' key '{secret_key}' is up to date"
302
+ )
303
+
304
+ # Determine action needed
305
+ if secret_name not in cluster_secret_names:
306
+ changes["create"].append(
307
+ f"Create secret '{secret_name}' with keys: {list(required_keys.keys())}"
308
+ )
309
+ create_secret_with_data(secret_name, new_secret_data, namespace, cluster)
310
+ elif secret_needs_update:
311
+ changes["update"].append(f"Update secret '{secret_name}' (values changed)")
312
+ update_secret_with_data(secret_name, new_secret_data, namespace, cluster)
313
+
314
+ # Handle orphaned secrets (in cluster but not in manifest)
315
+ orphaned_secrets = cluster_secret_names - set(required_secrets.keys())
316
+ if orphaned_secrets:
317
+ console.print(
318
+ f"\n[yellow]Warning: Found {len(orphaned_secrets)} secrets in cluster not defined in manifest:[/yellow]"
319
+ )
320
+ for secret in orphaned_secrets:
321
+ console.print(f" - {secret}")
322
+
323
+
324
+ def create_dockerconfigjson_string(
325
+ registry: str, username: str, password: str, email: str | None = None
326
+ ) -> str:
327
+ """Create raw dockerconfigjson string data for use with Kubernetes string_data field"""
328
+ # Create the auth field (base64 encoded username:password)
329
+ auth_string = f"{username}:{password}"
330
+ auth_b64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
331
+
332
+ # Build the auth entry
333
+ auth_entry = {"username": username, "password": password, "auth": auth_b64}
334
+
335
+ # Only include email if provided
336
+ if email:
337
+ auth_entry["email"] = email
338
+
339
+ # Create the full dockerconfig structure
340
+ docker_config = {"auths": {registry: auth_entry}}
341
+
342
+ # Return raw JSON string (Kubernetes will handle base64 encoding when using string_data)
343
+ return json.dumps(docker_config)
344
+
345
+
346
+ def parse_dockerconfigjson_data(input_data: str) -> dict[str, dict[str, str]]:
347
+ """Parse existing dockerconfigjson data to extract registry credentials"""
348
+ try:
349
+ # Decode base64
350
+ config = json.loads(input_data)
351
+
352
+ # Extract auths section
353
+ auths = config.get("auths", {})
354
+
355
+ # Convert to comparable format: {registry: {username, password, email}}
356
+ parsed_auths = {}
357
+ for registry, auth_data in auths.items():
358
+ # Try to decode the base64 auth field first
359
+ username = ""
360
+ password = ""
361
+ if "auth" in auth_data:
362
+ try:
363
+ auth_b64 = auth_data["auth"]
364
+ username_password = base64.b64decode(auth_b64).decode("utf-8")
365
+ if ":" in username_password:
366
+ username, password = username_password.split(":", 1)
367
+ except Exception:
368
+ pass
369
+
370
+ # Fall back to direct username/password fields if auth decode failed
371
+ if not username:
372
+ username = auth_data.get("username", "")
373
+ if not password:
374
+ password = auth_data.get("password", "")
375
+
376
+ parsed_auths[registry] = {
377
+ "username": username,
378
+ "password": password,
379
+ "email": auth_data.get("email", ""),
380
+ }
381
+
382
+ return parsed_auths
383
+ except Exception:
384
+ return {} # If parsing fails, assume empty/invalid
385
+
386
+
387
+ def credentials_changed(
388
+ current_auths: dict[str, dict[str, str]],
389
+ new_registry: str,
390
+ new_username: str,
391
+ new_password: str,
392
+ new_email: str = "",
393
+ ) -> bool:
394
+ """Check if credentials have actually changed"""
395
+
396
+ # If registry doesn't exist in current, it's a change
397
+ if new_registry not in current_auths:
398
+ return True
399
+
400
+ current_creds = current_auths[new_registry]
401
+ # Compare each field
402
+ if (
403
+ current_creds.get("username", "") != new_username
404
+ or current_creds.get("password", "") != new_password
405
+ or current_creds.get("email", "") != (new_email or "")
406
+ ):
407
+ return True
408
+ else:
409
+ return False # No changes detected
410
+
411
+
412
+ def interactive_image_pull_secret_input(secret_name: str) -> dict[str, str]:
413
+ """Prompt user for image pull secret values"""
414
+ console.print(
415
+ f"\n[bold]Configure image pull secret '[cyan]{secret_name}[/cyan]':[/bold]"
416
+ )
417
+
418
+ registry = questionary.text(
419
+ "Registry URL (e.g., docker.io, gcr.io, your-registry.com):",
420
+ default="docker.io",
421
+ ).ask()
422
+ registry = handle_questionary_cancellation(registry, "registry input")
423
+
424
+ username = questionary.text("Username:").ask()
425
+ username = handle_questionary_cancellation(username, "username input")
426
+
427
+ password = questionary.password("Password (input will be hidden):").ask()
428
+ password = handle_questionary_cancellation(password, "password input")
429
+
430
+ email_choice = questionary.confirm(
431
+ "Do you want to include an email address? (optional)"
432
+ ).ask()
433
+ email_choice = handle_questionary_cancellation(email_choice, "email choice")
434
+ email = ""
435
+ if email_choice:
436
+ email = questionary.text("Email address:").ask() or ""
437
+ if email is None: # Handle None from questionary
438
+ email = ""
439
+
440
+ return {
441
+ "registry": registry,
442
+ "username": username,
443
+ "password": password,
444
+ "email": email,
445
+ }
446
+
447
+
448
+ def sync_image_pull_secrets(
449
+ manifest_obj: AgentManifest,
450
+ found_dockerconfigjson_secrets: list[dict],
451
+ values_data: dict[str, Any],
452
+ cluster: str,
453
+ namespace: str,
454
+ interactive: bool,
455
+ changes: dict[str, list[str]],
456
+ ) -> None:
457
+ """Sync image pull secrets between manifest, cluster, and values file"""
458
+ console.print(
459
+ f"[bold]Syncing image pull secrets to cluster: {cluster} namespace: {namespace}[/bold]"
460
+ )
461
+
462
+ # Get the secrets of type KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON
463
+ cluster_dockerconfigjson_secret_names = {
464
+ secret["name"] for secret in found_dockerconfigjson_secrets
465
+ }
466
+
467
+ # Get the secrets from the manifest
468
+ deployment_config: DeploymentConfig = manifest_obj.deployment
469
+ manifest_image_pull_secrets: list[ImagePullSecretConfig] = (
470
+ deployment_config.imagePullSecrets or []
471
+ )
472
+
473
+ if not manifest_image_pull_secrets:
474
+ logger.info("No image pull secrets found in manifest")
475
+ return
476
+
477
+ # Get image pull secrets from values data
478
+ image_pull_values = values_data
479
+
480
+ # Process each required image pull secret
481
+ for pull_secret in manifest_image_pull_secrets:
482
+ secret_name = pull_secret.name
483
+ current_secret_data = get_secret_data(secret_name, namespace, cluster)
484
+
485
+ # Get new values
486
+ new_registry = ""
487
+ new_username = ""
488
+ new_password = ""
489
+ new_email = ""
490
+
491
+ if secret_name in image_pull_values:
492
+ # Get values from values file
493
+ secret_config = image_pull_values[secret_name]
494
+ new_registry = secret_config.get("registry", "")
495
+ new_username = secret_config.get("username", "")
496
+ new_password = secret_config.get("password", "")
497
+ new_email = secret_config.get("email", "")
498
+
499
+ if not new_registry or not new_username or not new_password:
500
+ raise RuntimeError(
501
+ f"Incomplete image pull secret configuration for '{secret_name}'. "
502
+ f"Required: registry, username, password. Optional: email"
503
+ )
504
+ elif interactive:
505
+ # Get values interactively
506
+ if secret_name in cluster_dockerconfigjson_secret_names:
507
+ console.print(
508
+ f"[blue]Image pull secret '{secret_name}' already exists[/blue]"
509
+ )
510
+ update_choice = questionary.select(
511
+ "What would you like to do?",
512
+ choices=["Keep current credentials", "Update with new credentials"],
513
+ ).ask()
514
+ update_choice = handle_questionary_cancellation(
515
+ update_choice, "image pull secret update choice"
516
+ )
517
+
518
+ if update_choice == "Keep current credentials":
519
+ continue # Skip this secret
520
+
521
+ console.print(
522
+ f"[yellow]Image pull secret '{secret_name}' needs configuration[/yellow]"
523
+ )
524
+ creds = interactive_image_pull_secret_input(secret_name)
525
+ new_registry = creds["registry"]
526
+ new_username = creds["username"]
527
+ new_password = creds["password"]
528
+ new_email = creds["email"]
529
+ else:
530
+ raise RuntimeError(
531
+ f"No configuration provided for image pull secret '{secret_name}'. "
532
+ f"Provide values file or use interactive mode."
533
+ )
534
+
535
+ # Check if update is needed
536
+ secret_needs_update = False
537
+ action = ""
538
+
539
+ if secret_name not in cluster_dockerconfigjson_secret_names:
540
+ # Secret doesn't exist, needs creation
541
+ secret_needs_update = True
542
+ action = "create"
543
+ else:
544
+ # Secret exists, check if values changed
545
+ current_dockerconfig = current_secret_data.get(".dockerconfigjson", {})
546
+ current_auths = parse_dockerconfigjson_data(current_dockerconfig)
547
+ if credentials_changed(
548
+ current_auths, new_registry, new_username, new_password, new_email
549
+ ):
550
+ secret_needs_update = True
551
+ action = "update"
552
+ else:
553
+ changes["noop"].append(
554
+ f"Secret '{secret_name}' key '{secret_name}' is up to date"
555
+ )
556
+
557
+ # Only perform action if update is needed
558
+ if secret_needs_update:
559
+ dockerconfig_string = create_dockerconfigjson_string(
560
+ new_registry, new_username, new_password, new_email
561
+ )
562
+ secret_data = {".dockerconfigjson": dockerconfig_string}
563
+
564
+ if action == "create":
565
+ changes[action].append(
566
+ f"Create image pull secret '{secret_name}' for registry '{new_registry}'"
567
+ )
568
+ create_image_pull_secret_with_data(
569
+ secret_name, secret_data, namespace, cluster
570
+ )
571
+ elif action == "update":
572
+ changes[action].append(
573
+ f"Update image pull secret '{secret_name}' (credentials changed)"
574
+ )
575
+ update_image_pull_secret_with_data(
576
+ secret_name, secret_data, namespace, cluster
577
+ )
578
+
579
+
580
+ def print_changes_summary(change_type: str, changes: dict[str, list[str]]) -> None:
581
+ # Show summary
582
+ console.print(f"\n[bold]Sync Summary for {change_type}:[/bold]")
583
+ if changes["create"]:
584
+ console.print("[green]Created:[/green]")
585
+ for change in changes["create"]:
586
+ console.print(f" ✓ {change}")
587
+
588
+ if changes["update"]:
589
+ console.print("[yellow]Updated:[/yellow]")
590
+ for change in changes["update"]:
591
+ console.print(f" ⚠ {change}")
592
+
593
+ if changes["noop"]:
594
+ console.print("[yellow]No changes:[/yellow]")
595
+ for change in changes["noop"]:
596
+ console.print(f" ✓ {change}")
597
+ del changes["noop"]
598
+
599
+ if not any(changes.values()):
600
+ console.print(
601
+ f"[green]✓ All secrets are already in sync for {change_type}[/green]"
602
+ )
603
+
604
+ console.print("")
605
+
606
+
607
+ def sync_secrets(
608
+ manifest_obj: AgentManifest,
609
+ cluster: str,
610
+ namespace: str,
611
+ interactive: bool,
612
+ values_path: str | None,
613
+ ) -> None:
614
+ """Sync secrets between manifest, cluster, and values file"""
615
+ logger.info(f"Syncing secrets to cluster: {cluster} namespace: {namespace}")
616
+
617
+ # Load values from file if provided
618
+ values_data = {}
619
+ if values_path:
620
+ try:
621
+ # TODO: Convert this to a pydantic model to validate the values file
622
+ values_data = load_values_file(values_path)
623
+ console.print(f"[green]Loaded values from {values_path}[/green]")
624
+ except Exception as e:
625
+ console.print(f"[red]Error loading values file: {e}[/red]")
626
+ raise
627
+
628
+ # Get the secrets from the cluster using the specified namespace and cluster context
629
+ cluster_secrets_by_type = get_kubernetes_secrets_by_type(
630
+ namespace=namespace, context=cluster
631
+ )
632
+
633
+ # Track changes for summary
634
+ changes = {"create": [], "update": [], "noop": []}
635
+
636
+ sync_user_defined_secrets(
637
+ manifest_obj,
638
+ cluster_secrets_by_type[KUBERNETES_SECRET_TYPE_OPAQUE],
639
+ values_data.get(
640
+ KUBERNETES_SECRET_TO_MANIFEST_KEY[KUBERNETES_SECRET_TYPE_OPAQUE], {}
641
+ ),
642
+ cluster,
643
+ namespace,
644
+ interactive,
645
+ changes,
646
+ )
647
+
648
+ print_changes_summary("User Defined Secrets", changes)
649
+
650
+ # Track changes for summary
651
+ changes = {"create": [], "update": [], "noop": []}
652
+
653
+ sync_image_pull_secrets(
654
+ manifest_obj,
655
+ cluster_secrets_by_type[KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON],
656
+ values_data.get(
657
+ KUBERNETES_SECRET_TO_MANIFEST_KEY[KUBERNETES_SECRET_TYPE_DOCKERCONFIGJSON],
658
+ {},
659
+ ),
660
+ cluster,
661
+ namespace,
662
+ interactive,
663
+ changes,
664
+ )
665
+
666
+ print_changes_summary("Image Pull Secrets", changes)
667
+
668
+ console.print(
669
+ f"\n[green]Secret sync completed for cluster '{cluster}' namespace '{namespace}'[/green]"
670
+ )