kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,577 @@
1
+ """Temporal Cloud provisioning activities using tcld CLI"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+ from datetime import datetime, timezone
6
+ from temporalio import activity
7
+ import structlog
8
+ import subprocess
9
+ import json
10
+ import os
11
+ import time
12
+ import secrets
13
+
14
+ from control_plane_api.app.lib.supabase import get_supabase
15
+
16
+ logger = structlog.get_logger()
17
+
18
+
19
+ @dataclass
20
+ class CheckNamespaceInput:
21
+ """Input for check_namespace_exists activity"""
22
+ organization_id: str
23
+ namespace_name: str
24
+
25
+
26
+ @dataclass
27
+ class CreateNamespaceInput:
28
+ """Input for create_namespace activity"""
29
+ organization_id: str
30
+ namespace_name: str
31
+ account_id: str
32
+ region: str = "aws-us-east-1"
33
+ retention_days: int = 30
34
+
35
+
36
+ @dataclass
37
+ class PollNamespaceStatusInput:
38
+ """Input for poll_namespace_status activity"""
39
+ namespace_name: str
40
+ max_attempts: int = 60 # 60 attempts * 5 seconds = 5 minutes max
41
+ poll_interval_seconds: int = 5
42
+
43
+
44
+ @dataclass
45
+ class GenerateApiKeyInput:
46
+ """Input for generate_namespace_api_key activity"""
47
+ namespace_name: str
48
+ key_description: str = "Control Plane API Key"
49
+
50
+
51
+ @dataclass
52
+ class StoreNamespaceCredentialsInput:
53
+ """Input for store_namespace_credentials activity"""
54
+ organization_id: str
55
+ namespace_name: str
56
+ api_key: str
57
+ status: str = "ready"
58
+
59
+
60
+ def run_tcld_command(cmd: list[str], capture_output: bool = True) -> dict:
61
+ """
62
+ Execute tcld CLI command and return result.
63
+
64
+ Args:
65
+ cmd: Command list (e.g., ["tcld", "namespace", "get", "--namespace", "my-ns"])
66
+ capture_output: Whether to capture stdout/stderr
67
+
68
+ Returns:
69
+ Dict with success, stdout, stderr, returncode
70
+ """
71
+ try:
72
+ # Get admin token from environment
73
+ admin_token = os.getenv("TEMPORAL_CLOUD_ADMIN_TOKEN")
74
+ if not admin_token:
75
+ raise ValueError("TEMPORAL_CLOUD_ADMIN_TOKEN environment variable is not set")
76
+
77
+ # Add API key to command if not already present
78
+ # tcld expects --api-key flag for authentication
79
+ enhanced_cmd = cmd.copy()
80
+ if "--api-key" not in enhanced_cmd:
81
+ # Insert API key right after tcld command
82
+ enhanced_cmd.insert(1, "--api-key")
83
+ enhanced_cmd.insert(2, admin_token)
84
+
85
+ activity.logger.info(f"Running tcld command: {' '.join(enhanced_cmd[:3])}... [credentials hidden]")
86
+
87
+ # Prepare environment (tcld might also check env vars)
88
+ env = os.environ.copy()
89
+ env["TEMPORAL_CLOUD_API_KEY"] = admin_token # Backup: env var
90
+
91
+ result = subprocess.run(
92
+ enhanced_cmd,
93
+ capture_output=capture_output,
94
+ text=True,
95
+ timeout=60, # 60 second timeout (namespace operations can take longer)
96
+ env=env,
97
+ )
98
+
99
+ return {
100
+ "success": result.returncode == 0,
101
+ "stdout": result.stdout,
102
+ "stderr": result.stderr,
103
+ "returncode": result.returncode,
104
+ }
105
+ except subprocess.TimeoutExpired:
106
+ return {
107
+ "success": False,
108
+ "error": "Command timed out after 60 seconds",
109
+ "returncode": -1,
110
+ }
111
+ except Exception as e:
112
+ return {
113
+ "success": False,
114
+ "error": str(e),
115
+ "returncode": -1,
116
+ }
117
+
118
+
119
+ @activity.defn
120
+ async def check_namespace_exists(input: CheckNamespaceInput) -> dict:
121
+ """
122
+ Check if a Temporal Cloud namespace already exists.
123
+
124
+ Uses: tcld namespace get --namespace <namespace_name>
125
+
126
+ Returns:
127
+ Dict with exists (bool) and details if found
128
+ """
129
+ activity.logger.info(
130
+ f"Checking if namespace exists",
131
+ extra={
132
+ "organization_id": input.organization_id,
133
+ "namespace_name": input.namespace_name,
134
+ }
135
+ )
136
+
137
+ try:
138
+ # First check our database
139
+ client = get_supabase()
140
+ db_result = (
141
+ client.table("temporal_namespaces")
142
+ .select("*")
143
+ .eq("organization_id", input.organization_id)
144
+ .eq("namespace_name", input.namespace_name)
145
+ .execute()
146
+ )
147
+
148
+ if db_result.data:
149
+ db_namespace = db_result.data[0]
150
+ activity.logger.info(
151
+ f"Namespace found in database",
152
+ extra={
153
+ "namespace_name": input.namespace_name,
154
+ "status": db_namespace.get("status"),
155
+ }
156
+ )
157
+ return {
158
+ "exists": True,
159
+ "in_database": True,
160
+ "status": db_namespace.get("status"),
161
+ "details": db_namespace,
162
+ }
163
+
164
+ # Check Temporal Cloud using tcld
165
+ result = run_tcld_command([
166
+ "tcld", "namespace", "get",
167
+ "--namespace", input.namespace_name,
168
+ "--output", "json"
169
+ ])
170
+
171
+ if result["success"]:
172
+ # Parse JSON output
173
+ try:
174
+ namespace_data = json.loads(result["stdout"])
175
+ activity.logger.info(
176
+ f"Namespace exists in Temporal Cloud",
177
+ extra={"namespace_name": input.namespace_name}
178
+ )
179
+ return {
180
+ "exists": True,
181
+ "in_temporal_cloud": True,
182
+ "details": namespace_data,
183
+ }
184
+ except json.JSONDecodeError:
185
+ return {"exists": True, "in_temporal_cloud": True}
186
+ else:
187
+ # Namespace doesn't exist
188
+ activity.logger.info(
189
+ f"Namespace does not exist",
190
+ extra={"namespace_name": input.namespace_name}
191
+ )
192
+ return {"exists": False}
193
+
194
+ except Exception as e:
195
+ activity.logger.error(
196
+ f"Failed to check namespace existence",
197
+ extra={"error": str(e), "namespace_name": input.namespace_name}
198
+ )
199
+ raise
200
+
201
+
202
+ @activity.defn
203
+ async def create_namespace(input: CreateNamespaceInput) -> dict:
204
+ """
205
+ Create a new Temporal Cloud namespace using tcld CLI.
206
+
207
+ Uses: tcld namespace create --namespace <name> --region <region> --retention-days <days>
208
+
209
+ Returns:
210
+ Dict with success flag and namespace details
211
+ """
212
+ activity.logger.info(
213
+ f"Creating Temporal Cloud namespace",
214
+ extra={
215
+ "organization_id": input.organization_id,
216
+ "namespace_name": input.namespace_name,
217
+ "region": input.region,
218
+ }
219
+ )
220
+
221
+ try:
222
+ # Create namespace record in database first (status: provisioning)
223
+ client = get_supabase()
224
+ namespace_id = None
225
+
226
+ # Check if already exists in DB
227
+ existing = (
228
+ client.table("temporal_namespaces")
229
+ .select("id")
230
+ .eq("organization_id", input.organization_id)
231
+ .execute()
232
+ )
233
+
234
+ if existing.data:
235
+ namespace_id = existing.data[0]["id"]
236
+ # Update to provisioning
237
+ client.table("temporal_namespaces").update({
238
+ "status": "provisioning",
239
+ "updated_at": datetime.now(timezone.utc).isoformat(),
240
+ }).eq("id", namespace_id).execute()
241
+ else:
242
+ # Create new record
243
+ namespace_record = {
244
+ "organization_id": input.organization_id,
245
+ "namespace_name": input.namespace_name,
246
+ "account_id": input.account_id,
247
+ "region": input.region,
248
+ "status": "provisioning",
249
+ "created_at": datetime.now(timezone.utc).isoformat(),
250
+ "updated_at": datetime.now(timezone.utc).isoformat(),
251
+ }
252
+ result = client.table("temporal_namespaces").insert(namespace_record).execute()
253
+ namespace_id = result.data[0]["id"] if result.data else None
254
+
255
+ # Execute tcld namespace create
256
+ cmd = [
257
+ "tcld", "namespace", "create",
258
+ "--namespace", input.namespace_name,
259
+ "--region", input.region,
260
+ "--retention-days", str(input.retention_days),
261
+ "--output", "json"
262
+ ]
263
+
264
+ result = run_tcld_command(cmd)
265
+
266
+ if result["success"]:
267
+ activity.logger.info(
268
+ f"Namespace creation initiated",
269
+ extra={"namespace_name": input.namespace_name}
270
+ )
271
+ return {
272
+ "success": True,
273
+ "namespace_id": namespace_id,
274
+ "namespace_name": input.namespace_name,
275
+ "message": "Namespace creation initiated",
276
+ }
277
+ else:
278
+ error_msg = result.get("stderr", result.get("error", "Unknown error"))
279
+ activity.logger.error(
280
+ f"Failed to create namespace",
281
+ extra={
282
+ "namespace_name": input.namespace_name,
283
+ "error": error_msg,
284
+ }
285
+ )
286
+
287
+ # Update database with error
288
+ if namespace_id:
289
+ client.table("temporal_namespaces").update({
290
+ "status": "error",
291
+ "error_message": error_msg,
292
+ "updated_at": datetime.now(timezone.utc).isoformat(),
293
+ }).eq("id", namespace_id).execute()
294
+
295
+ return {
296
+ "success": False,
297
+ "error": error_msg,
298
+ }
299
+
300
+ except Exception as e:
301
+ activity.logger.error(
302
+ f"Failed to create namespace",
303
+ extra={"error": str(e), "namespace_name": input.namespace_name}
304
+ )
305
+ raise
306
+
307
+
308
+ @activity.defn
309
+ async def poll_namespace_status(input: PollNamespaceStatusInput) -> dict:
310
+ """
311
+ Poll Temporal Cloud namespace status until it's ready.
312
+
313
+ Uses: tcld namespace get --namespace <name>
314
+
315
+ Returns:
316
+ Dict with ready (bool), status, and details
317
+ """
318
+ activity.logger.info(
319
+ f"Polling namespace status",
320
+ extra={
321
+ "namespace_name": input.namespace_name,
322
+ "max_attempts": input.max_attempts,
323
+ }
324
+ )
325
+
326
+ attempt = 0
327
+ while attempt < input.max_attempts:
328
+ attempt += 1
329
+
330
+ try:
331
+ result = run_tcld_command([
332
+ "tcld", "namespace", "get",
333
+ "--namespace", input.namespace_name,
334
+ "--output", "json"
335
+ ])
336
+
337
+ if result["success"]:
338
+ try:
339
+ namespace_data = json.loads(result["stdout"])
340
+ status = namespace_data.get("state", "unknown")
341
+
342
+ activity.logger.info(
343
+ f"Namespace status check",
344
+ extra={
345
+ "namespace_name": input.namespace_name,
346
+ "attempt": attempt,
347
+ "status": status,
348
+ }
349
+ )
350
+
351
+ # Check if namespace is ready (status might be "active" or "running")
352
+ if status.lower() in ["active", "running", "ready"]:
353
+ return {
354
+ "ready": True,
355
+ "status": status,
356
+ "attempts": attempt,
357
+ "details": namespace_data,
358
+ }
359
+ except json.JSONDecodeError:
360
+ activity.logger.warning(
361
+ f"Failed to parse namespace status JSON",
362
+ extra={"attempt": attempt}
363
+ )
364
+
365
+ # Wait before next attempt
366
+ if attempt < input.max_attempts:
367
+ time.sleep(input.poll_interval_seconds)
368
+
369
+ except Exception as e:
370
+ activity.logger.warning(
371
+ f"Error polling namespace status",
372
+ extra={"attempt": attempt, "error": str(e)}
373
+ )
374
+ if attempt < input.max_attempts:
375
+ time.sleep(input.poll_interval_seconds)
376
+
377
+ # Timed out
378
+ activity.logger.error(
379
+ f"Namespace provisioning timed out",
380
+ extra={
381
+ "namespace_name": input.namespace_name,
382
+ "attempts": attempt,
383
+ }
384
+ )
385
+ return {
386
+ "ready": False,
387
+ "status": "timeout",
388
+ "attempts": attempt,
389
+ "error": f"Namespace not ready after {attempt} attempts"
390
+ }
391
+
392
+
393
+ @activity.defn
394
+ async def generate_namespace_api_key(input: GenerateApiKeyInput) -> dict:
395
+ """
396
+ Generate an API key for the Temporal Cloud namespace.
397
+
398
+ Uses: tcld apikey create --namespace <name> --description <desc>
399
+
400
+ Returns:
401
+ Dict with success flag and api_key
402
+ """
403
+ activity.logger.info(
404
+ f"Generating API key for namespace",
405
+ extra={"namespace_name": input.namespace_name}
406
+ )
407
+
408
+ try:
409
+ result = run_tcld_command([
410
+ "tcld", "apikey", "create",
411
+ "--namespace", input.namespace_name,
412
+ "--description", input.key_description,
413
+ "--output", "json"
414
+ ])
415
+
416
+ if result["success"]:
417
+ try:
418
+ key_data = json.loads(result["stdout"])
419
+ api_key = key_data.get("key") or key_data.get("apiKey")
420
+
421
+ if api_key:
422
+ activity.logger.info(
423
+ f"API key generated successfully",
424
+ extra={"namespace_name": input.namespace_name}
425
+ )
426
+ return {
427
+ "success": True,
428
+ "api_key": api_key,
429
+ "key_id": key_data.get("id"),
430
+ }
431
+ else:
432
+ return {
433
+ "success": False,
434
+ "error": "API key not found in response",
435
+ }
436
+ except json.JSONDecodeError:
437
+ return {
438
+ "success": False,
439
+ "error": "Failed to parse API key response",
440
+ }
441
+ else:
442
+ error_msg = result.get("stderr", result.get("error", "Unknown error"))
443
+ activity.logger.error(
444
+ f"Failed to generate API key",
445
+ extra={"namespace_name": input.namespace_name, "error": error_msg}
446
+ )
447
+ return {
448
+ "success": False,
449
+ "error": error_msg,
450
+ }
451
+
452
+ except Exception as e:
453
+ activity.logger.error(
454
+ f"Failed to generate API key",
455
+ extra={"error": str(e), "namespace_name": input.namespace_name}
456
+ )
457
+ raise
458
+
459
+
460
+ @activity.defn
461
+ async def store_namespace_credentials(input: StoreNamespaceCredentialsInput) -> dict:
462
+ """
463
+ Store namespace credentials in database.
464
+
465
+ TODO: Encrypt API key before storing (use something like Fernet or AWS KMS)
466
+
467
+ Returns:
468
+ Dict with success flag
469
+ """
470
+ activity.logger.info(
471
+ f"Storing namespace credentials",
472
+ extra={
473
+ "organization_id": input.organization_id,
474
+ "namespace_name": input.namespace_name,
475
+ }
476
+ )
477
+
478
+ try:
479
+ client = get_supabase()
480
+
481
+ # TODO: Encrypt API key properly
482
+ # For now, we'll store it as-is (NOT RECOMMENDED FOR PRODUCTION)
483
+ # In production, use:
484
+ # - AWS KMS
485
+ # - Vault
486
+ # - Supabase Vault (vault.encrypt)
487
+ # - cryptography.Fernet
488
+ api_key_encrypted = input.api_key # Should be encrypted!
489
+
490
+ # Update namespace record
491
+ update_data = {
492
+ "api_key_encrypted": api_key_encrypted,
493
+ "status": input.status,
494
+ "provisioned_at": datetime.now(timezone.utc).isoformat(),
495
+ "updated_at": datetime.now(timezone.utc).isoformat(),
496
+ }
497
+
498
+ result = (
499
+ client.table("temporal_namespaces")
500
+ .update(update_data)
501
+ .eq("organization_id", input.organization_id)
502
+ .eq("namespace_name", input.namespace_name)
503
+ .execute()
504
+ )
505
+
506
+ if result.data:
507
+ activity.logger.info(
508
+ f"Namespace credentials stored",
509
+ extra={"namespace_name": input.namespace_name}
510
+ )
511
+ return {"success": True, "namespace_id": result.data[0]["id"]}
512
+ else:
513
+ raise Exception("Failed to update namespace credentials")
514
+
515
+ except Exception as e:
516
+ activity.logger.error(
517
+ f"Failed to store namespace credentials",
518
+ extra={"error": str(e), "namespace_name": input.namespace_name}
519
+ )
520
+ raise
521
+
522
+
523
+ @activity.defn
524
+ async def update_task_queue_status(
525
+ task_queue_id: str,
526
+ status: str,
527
+ error_message: Optional[str] = None,
528
+ temporal_namespace_id: Optional[str] = None,
529
+ ) -> dict:
530
+ """
531
+ Update task queue status after provisioning.
532
+
533
+ Returns:
534
+ Dict with success flag
535
+ """
536
+ activity.logger.info(
537
+ f"Updating task queue status",
538
+ extra={"task_queue_id": task_queue_id, "status": status}
539
+ )
540
+
541
+ try:
542
+ client = get_supabase()
543
+
544
+ update_data = {
545
+ "status": status,
546
+ "updated_at": datetime.now(timezone.utc).isoformat(),
547
+ }
548
+
549
+ if error_message:
550
+ update_data["error_message"] = error_message
551
+
552
+ if temporal_namespace_id:
553
+ update_data["temporal_namespace_id"] = temporal_namespace_id
554
+ update_data["provisioned_at"] = datetime.now(timezone.utc).isoformat()
555
+
556
+ result = (
557
+ client.table("environments")
558
+ .update(update_data)
559
+ .eq("id", task_queue_id)
560
+ .execute()
561
+ )
562
+
563
+ if result.data:
564
+ activity.logger.info(
565
+ f"Task queue status updated",
566
+ extra={"task_queue_id": task_queue_id, "status": status}
567
+ )
568
+ return {"success": True}
569
+ else:
570
+ raise Exception("Failed to update task queue status")
571
+
572
+ except Exception as e:
573
+ activity.logger.error(
574
+ f"Failed to update task queue status",
575
+ extra={"error": str(e), "task_queue_id": task_queue_id}
576
+ )
577
+ raise
@@ -0,0 +1,35 @@
1
+ """
2
+ Configuration module for Control Plane API.
3
+
4
+ This module provides separate configuration classes for API and shared settings.
5
+ """
6
+
7
+ from .api_config import APIConfig
8
+
9
+ # Create singleton instance
10
+ _api_config = None
11
+
12
+
13
+ def get_api_config() -> APIConfig:
14
+ """
15
+ Get or create the API configuration singleton.
16
+
17
+ Returns:
18
+ APIConfig instance
19
+ """
20
+ global _api_config
21
+
22
+ if _api_config is None:
23
+ _api_config = APIConfig()
24
+
25
+ return _api_config
26
+
27
+
28
+ # For backward compatibility with existing code
29
+ settings = get_api_config()
30
+
31
+ __all__ = [
32
+ "APIConfig",
33
+ "get_api_config",
34
+ "settings",
35
+ ]