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.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {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
|
+
]
|