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,654 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Queues router - Worker queue management for routing work to specific workers.
|
|
3
|
+
|
|
4
|
+
This router handles task queue CRUD operations and tracks worker availability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
import structlog
|
|
12
|
+
import uuid
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
16
|
+
from control_plane_api.app.lib.supabase import get_supabase
|
|
17
|
+
from control_plane_api.app.lib.temporal_client import get_temporal_client
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger()
|
|
20
|
+
|
|
21
|
+
router = APIRouter()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Pydantic schemas
|
|
25
|
+
class TaskQueueCreate(BaseModel):
|
|
26
|
+
name: str = Field(..., description="Queue name (e.g., default, high-priority)", min_length=2, max_length=100)
|
|
27
|
+
display_name: str | None = Field(None, description="User-friendly display name")
|
|
28
|
+
description: str | None = Field(None, description="Queue description")
|
|
29
|
+
tags: List[str] = Field(default_factory=list, description="Tags for categorization")
|
|
30
|
+
settings: dict = Field(default_factory=dict, description="Queue settings")
|
|
31
|
+
priority: int | None = Field(None, ge=1, le=10, description="Priority level (1-10)")
|
|
32
|
+
policy_ids: List[str] = Field(default_factory=list, description="OPA policy IDs")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaskQueueUpdate(BaseModel):
|
|
36
|
+
name: str | None = None
|
|
37
|
+
display_name: str | None = None
|
|
38
|
+
description: str | None = None
|
|
39
|
+
tags: List[str] | None = None
|
|
40
|
+
settings: dict | None = None
|
|
41
|
+
status: str | None = None
|
|
42
|
+
priority: int | None = Field(None, ge=1, le=10)
|
|
43
|
+
policy_ids: List[str] | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TaskQueueResponse(BaseModel):
|
|
47
|
+
id: str
|
|
48
|
+
organization_id: str
|
|
49
|
+
name: str
|
|
50
|
+
display_name: str | None
|
|
51
|
+
description: str | None
|
|
52
|
+
tags: List[str]
|
|
53
|
+
settings: dict
|
|
54
|
+
status: str
|
|
55
|
+
priority: int | None = None
|
|
56
|
+
policy_ids: List[str] = []
|
|
57
|
+
created_at: str
|
|
58
|
+
updated_at: str
|
|
59
|
+
created_by: str | None
|
|
60
|
+
|
|
61
|
+
# Temporal Cloud provisioning fields
|
|
62
|
+
worker_token: str | None = None # UUID token for worker registration
|
|
63
|
+
provisioning_workflow_id: str | None = None # Temporal workflow ID
|
|
64
|
+
provisioned_at: str | None = None
|
|
65
|
+
error_message: str | None = None
|
|
66
|
+
temporal_namespace_id: str | None = None
|
|
67
|
+
|
|
68
|
+
# Worker metrics
|
|
69
|
+
active_workers: int = 0
|
|
70
|
+
idle_workers: int = 0
|
|
71
|
+
busy_workers: int = 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WorkerHeartbeatResponse(BaseModel):
|
|
75
|
+
id: str
|
|
76
|
+
organization_id: str
|
|
77
|
+
task_queue_name: str
|
|
78
|
+
worker_id: str
|
|
79
|
+
hostname: str | None
|
|
80
|
+
worker_metadata: dict
|
|
81
|
+
last_heartbeat: str
|
|
82
|
+
status: str
|
|
83
|
+
tasks_processed: int
|
|
84
|
+
current_task_id: str | None
|
|
85
|
+
registered_at: str
|
|
86
|
+
updated_at: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ensure_default_queue(organization: dict) -> Optional[dict]:
|
|
90
|
+
"""
|
|
91
|
+
Ensure the organization has a default task queue.
|
|
92
|
+
Creates one if it doesn't exist.
|
|
93
|
+
|
|
94
|
+
Returns the default queue or None if creation failed.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
client = get_supabase()
|
|
98
|
+
|
|
99
|
+
# Check if default queue exists
|
|
100
|
+
existing = (
|
|
101
|
+
client.table("environments")
|
|
102
|
+
.select("*")
|
|
103
|
+
.eq("organization_id", organization["id"])
|
|
104
|
+
.eq("name", "default")
|
|
105
|
+
.execute()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if existing.data:
|
|
109
|
+
return existing.data[0]
|
|
110
|
+
|
|
111
|
+
# Create default queue
|
|
112
|
+
queue_id = str(uuid.uuid4())
|
|
113
|
+
now = datetime.utcnow().isoformat()
|
|
114
|
+
|
|
115
|
+
default_queue = {
|
|
116
|
+
"id": queue_id,
|
|
117
|
+
"organization_id": organization["id"],
|
|
118
|
+
"name": "default",
|
|
119
|
+
"display_name": "Default Queue",
|
|
120
|
+
"description": "Default task queue for all workers",
|
|
121
|
+
"tags": [],
|
|
122
|
+
"settings": {},
|
|
123
|
+
"status": "active",
|
|
124
|
+
"created_at": now,
|
|
125
|
+
"updated_at": now,
|
|
126
|
+
"created_by": organization.get("user_id"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
result = client.table("environments").insert(default_queue).execute()
|
|
130
|
+
|
|
131
|
+
if result.data:
|
|
132
|
+
logger.info(
|
|
133
|
+
"default_queue_created",
|
|
134
|
+
queue_id=queue_id,
|
|
135
|
+
org_id=organization["id"],
|
|
136
|
+
)
|
|
137
|
+
return result.data[0]
|
|
138
|
+
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error("ensure_default_queue_failed", error=str(e), org_id=organization.get("id"))
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.post("", response_model=TaskQueueResponse, status_code=status.HTTP_201_CREATED)
|
|
147
|
+
async def create_task_queue(
|
|
148
|
+
queue_data: TaskQueueCreate,
|
|
149
|
+
request: Request,
|
|
150
|
+
organization: dict = Depends(get_current_organization),
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Create a new task queue.
|
|
154
|
+
|
|
155
|
+
If this is the first task queue for the organization, it will trigger
|
|
156
|
+
Temporal Cloud namespace provisioning workflow.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
client = get_supabase()
|
|
160
|
+
|
|
161
|
+
# Check if queue name already exists for this organization
|
|
162
|
+
existing = (
|
|
163
|
+
client.table("environments")
|
|
164
|
+
.select("id")
|
|
165
|
+
.eq("organization_id", organization["id"])
|
|
166
|
+
.eq("name", queue_data.name)
|
|
167
|
+
.execute()
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if existing.data:
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
173
|
+
detail=f"Task queue with name '{queue_data.name}' already exists"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Check if this is the first task queue for this org
|
|
177
|
+
all_queues = (
|
|
178
|
+
client.table("environments")
|
|
179
|
+
.select("id")
|
|
180
|
+
.eq("organization_id", organization["id"])
|
|
181
|
+
.execute()
|
|
182
|
+
)
|
|
183
|
+
is_first_queue = len(all_queues.data or []) == 0
|
|
184
|
+
|
|
185
|
+
# Check if namespace already exists
|
|
186
|
+
namespace_result = (
|
|
187
|
+
client.table("temporal_namespaces")
|
|
188
|
+
.select("*")
|
|
189
|
+
.eq("organization_id", organization["id"])
|
|
190
|
+
.execute()
|
|
191
|
+
)
|
|
192
|
+
has_namespace = bool(namespace_result.data)
|
|
193
|
+
needs_provisioning = is_first_queue and not has_namespace
|
|
194
|
+
|
|
195
|
+
queue_id = str(uuid.uuid4())
|
|
196
|
+
now = datetime.utcnow().isoformat()
|
|
197
|
+
|
|
198
|
+
# Set initial status
|
|
199
|
+
initial_status = "provisioning" if needs_provisioning else "ready"
|
|
200
|
+
|
|
201
|
+
queue_record = {
|
|
202
|
+
"id": queue_id,
|
|
203
|
+
"organization_id": organization["id"],
|
|
204
|
+
"name": queue_data.name,
|
|
205
|
+
"display_name": queue_data.display_name or queue_data.name,
|
|
206
|
+
"description": queue_data.description,
|
|
207
|
+
"tags": queue_data.tags,
|
|
208
|
+
"settings": queue_data.settings,
|
|
209
|
+
"status": initial_status,
|
|
210
|
+
"created_at": now,
|
|
211
|
+
"updated_at": now,
|
|
212
|
+
"created_by": organization.get("user_id"),
|
|
213
|
+
"worker_token": str(uuid.uuid4()), # Generate worker token
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
result = client.table("environments").insert(queue_record).execute()
|
|
217
|
+
|
|
218
|
+
if not result.data:
|
|
219
|
+
raise HTTPException(
|
|
220
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
221
|
+
detail="Failed to create task queue"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
queue = result.data[0]
|
|
225
|
+
|
|
226
|
+
# Trigger namespace provisioning workflow if needed
|
|
227
|
+
if needs_provisioning:
|
|
228
|
+
try:
|
|
229
|
+
from control_plane_api.app.workflows.namespace_provisioning import (
|
|
230
|
+
ProvisionTemporalNamespaceWorkflow,
|
|
231
|
+
ProvisionNamespaceInput,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
temporal_client = await get_temporal_client()
|
|
235
|
+
account_id = os.environ.get("TEMPORAL_CLOUD_ACCOUNT_ID", "default-account")
|
|
236
|
+
|
|
237
|
+
workflow_input = ProvisionNamespaceInput(
|
|
238
|
+
organization_id=organization["id"],
|
|
239
|
+
organization_name=organization.get("name", organization["id"]),
|
|
240
|
+
task_queue_id=queue_id,
|
|
241
|
+
account_id=account_id,
|
|
242
|
+
region=os.environ.get("TEMPORAL_CLOUD_REGION", "aws-us-east-1"),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Start provisioning workflow on control plane's task queue
|
|
246
|
+
workflow_handle = await temporal_client.start_workflow(
|
|
247
|
+
ProvisionTemporalNamespaceWorkflow.run,
|
|
248
|
+
workflow_input,
|
|
249
|
+
id=f"provision-namespace-{organization['id']}",
|
|
250
|
+
task_queue="agent-control-plane", # Control plane's task queue
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Update queue with workflow ID
|
|
254
|
+
client.table("environments").update({
|
|
255
|
+
"provisioning_workflow_id": workflow_handle.id,
|
|
256
|
+
"updated_at": datetime.utcnow().isoformat(),
|
|
257
|
+
}).eq("id", queue_id).execute()
|
|
258
|
+
|
|
259
|
+
queue["provisioning_workflow_id"] = workflow_handle.id
|
|
260
|
+
|
|
261
|
+
logger.info(
|
|
262
|
+
"namespace_provisioning_workflow_started",
|
|
263
|
+
workflow_id=workflow_handle.id,
|
|
264
|
+
queue_id=queue_id,
|
|
265
|
+
org_id=organization["id"],
|
|
266
|
+
)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(
|
|
269
|
+
"failed_to_start_provisioning_workflow",
|
|
270
|
+
error=str(e),
|
|
271
|
+
queue_id=queue_id,
|
|
272
|
+
org_id=organization["id"],
|
|
273
|
+
)
|
|
274
|
+
# Update queue status to error
|
|
275
|
+
client.table("environments").update({
|
|
276
|
+
"status": "error",
|
|
277
|
+
"error_message": f"Failed to start provisioning: {str(e)}",
|
|
278
|
+
"updated_at": datetime.utcnow().isoformat(),
|
|
279
|
+
}).eq("id", queue_id).execute()
|
|
280
|
+
queue["status"] = "error"
|
|
281
|
+
queue["error_message"] = f"Failed to start provisioning: {str(e)}"
|
|
282
|
+
|
|
283
|
+
logger.info(
|
|
284
|
+
"task_queue_created",
|
|
285
|
+
queue_id=queue_id,
|
|
286
|
+
queue_name=queue["name"],
|
|
287
|
+
org_id=organization["id"],
|
|
288
|
+
needs_provisioning=needs_provisioning,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return TaskQueueResponse(
|
|
292
|
+
**queue,
|
|
293
|
+
active_workers=0,
|
|
294
|
+
idle_workers=0,
|
|
295
|
+
busy_workers=0,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
except HTTPException:
|
|
299
|
+
raise
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error("task_queue_creation_failed", error=str(e), org_id=organization["id"])
|
|
302
|
+
raise HTTPException(
|
|
303
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
304
|
+
detail=f"Failed to create task queue: {str(e)}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@router.get("", response_model=List[TaskQueueResponse])
|
|
309
|
+
async def list_task_queues(
|
|
310
|
+
request: Request,
|
|
311
|
+
status_filter: str | None = None,
|
|
312
|
+
organization: dict = Depends(get_current_organization),
|
|
313
|
+
):
|
|
314
|
+
"""List all task queues in the organization"""
|
|
315
|
+
try:
|
|
316
|
+
client = get_supabase()
|
|
317
|
+
|
|
318
|
+
# Ensure default queue exists
|
|
319
|
+
ensure_default_queue(organization)
|
|
320
|
+
|
|
321
|
+
# Query queues
|
|
322
|
+
query = client.table("environments").select("*").eq("organization_id", organization["id"])
|
|
323
|
+
|
|
324
|
+
if status_filter:
|
|
325
|
+
query = query.eq("status", status_filter)
|
|
326
|
+
|
|
327
|
+
query = query.order("created_at", desc=False)
|
|
328
|
+
result = query.execute()
|
|
329
|
+
|
|
330
|
+
if not result.data:
|
|
331
|
+
return []
|
|
332
|
+
|
|
333
|
+
# Note: Worker stats are now tracked at worker_queue level, not environment level
|
|
334
|
+
# For backward compatibility, we return 0 for environment-level worker counts
|
|
335
|
+
# Use worker_queues endpoints for detailed worker information
|
|
336
|
+
queues = []
|
|
337
|
+
for queue in result.data:
|
|
338
|
+
queues.append(
|
|
339
|
+
TaskQueueResponse(
|
|
340
|
+
**queue,
|
|
341
|
+
active_workers=0,
|
|
342
|
+
idle_workers=0,
|
|
343
|
+
busy_workers=0,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
logger.info(
|
|
348
|
+
"task_queues_listed",
|
|
349
|
+
count=len(queues),
|
|
350
|
+
org_id=organization["id"],
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return queues
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.error("task_queues_list_failed", error=str(e), org_id=organization["id"])
|
|
357
|
+
raise HTTPException(
|
|
358
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
359
|
+
detail=f"Failed to list task queues: {str(e)}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.get("/{queue_id}", response_model=TaskQueueResponse)
|
|
364
|
+
async def get_task_queue(
|
|
365
|
+
queue_id: str,
|
|
366
|
+
request: Request,
|
|
367
|
+
organization: dict = Depends(get_current_organization),
|
|
368
|
+
):
|
|
369
|
+
"""Get a specific task queue by ID"""
|
|
370
|
+
try:
|
|
371
|
+
client = get_supabase()
|
|
372
|
+
|
|
373
|
+
result = (
|
|
374
|
+
client.table("environments")
|
|
375
|
+
.select("*")
|
|
376
|
+
.eq("id", queue_id)
|
|
377
|
+
.eq("organization_id", organization["id"])
|
|
378
|
+
.single()
|
|
379
|
+
.execute()
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if not result.data:
|
|
383
|
+
raise HTTPException(status_code=404, detail="Task queue not found")
|
|
384
|
+
|
|
385
|
+
queue = result.data
|
|
386
|
+
|
|
387
|
+
# Note: Worker stats are now tracked at worker_queue level
|
|
388
|
+
# Return 0 for environment-level worker counts
|
|
389
|
+
return TaskQueueResponse(
|
|
390
|
+
**queue,
|
|
391
|
+
active_workers=0,
|
|
392
|
+
idle_workers=0,
|
|
393
|
+
busy_workers=0,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
except HTTPException:
|
|
397
|
+
raise
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error("task_queue_get_failed", error=str(e), queue_id=queue_id)
|
|
400
|
+
raise HTTPException(
|
|
401
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
402
|
+
detail=f"Failed to get task queue: {str(e)}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@router.patch("/{queue_id}", response_model=TaskQueueResponse)
|
|
407
|
+
async def update_task_queue(
|
|
408
|
+
queue_id: str,
|
|
409
|
+
queue_data: TaskQueueUpdate,
|
|
410
|
+
request: Request,
|
|
411
|
+
organization: dict = Depends(get_current_organization),
|
|
412
|
+
):
|
|
413
|
+
"""Update a task queue"""
|
|
414
|
+
try:
|
|
415
|
+
client = get_supabase()
|
|
416
|
+
|
|
417
|
+
# Check if queue exists
|
|
418
|
+
existing = (
|
|
419
|
+
client.table("environments")
|
|
420
|
+
.select("id")
|
|
421
|
+
.eq("id", queue_id)
|
|
422
|
+
.eq("organization_id", organization["id"])
|
|
423
|
+
.execute()
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if not existing.data:
|
|
427
|
+
raise HTTPException(status_code=404, detail="Task queue not found")
|
|
428
|
+
|
|
429
|
+
# Build update dict
|
|
430
|
+
update_data = queue_data.model_dump(exclude_unset=True)
|
|
431
|
+
update_data["updated_at"] = datetime.utcnow().isoformat()
|
|
432
|
+
|
|
433
|
+
# Update queue
|
|
434
|
+
result = (
|
|
435
|
+
client.table("environments")
|
|
436
|
+
.update(update_data)
|
|
437
|
+
.eq("id", queue_id)
|
|
438
|
+
.eq("organization_id", organization["id"])
|
|
439
|
+
.execute()
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if not result.data:
|
|
443
|
+
raise HTTPException(
|
|
444
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
445
|
+
detail="Failed to update task queue"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
queue = result.data[0]
|
|
449
|
+
|
|
450
|
+
logger.info(
|
|
451
|
+
"task_queue_updated",
|
|
452
|
+
queue_id=queue_id,
|
|
453
|
+
org_id=organization["id"],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Note: Worker stats are now tracked at worker_queue level
|
|
457
|
+
return TaskQueueResponse(
|
|
458
|
+
**queue,
|
|
459
|
+
active_workers=0,
|
|
460
|
+
idle_workers=0,
|
|
461
|
+
busy_workers=0,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
except HTTPException:
|
|
465
|
+
raise
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.error("task_queue_update_failed", error=str(e), queue_id=queue_id)
|
|
468
|
+
raise HTTPException(
|
|
469
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
470
|
+
detail=f"Failed to update task queue: {str(e)}"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@router.delete("/{queue_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
475
|
+
async def delete_task_queue(
|
|
476
|
+
queue_id: str,
|
|
477
|
+
request: Request,
|
|
478
|
+
organization: dict = Depends(get_current_organization),
|
|
479
|
+
):
|
|
480
|
+
"""Delete a task queue"""
|
|
481
|
+
try:
|
|
482
|
+
client = get_supabase()
|
|
483
|
+
|
|
484
|
+
# Prevent deleting default queue
|
|
485
|
+
queue_check = (
|
|
486
|
+
client.table("environments")
|
|
487
|
+
.select("name")
|
|
488
|
+
.eq("id", queue_id)
|
|
489
|
+
.eq("organization_id", organization["id"])
|
|
490
|
+
.single()
|
|
491
|
+
.execute()
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if queue_check.data and queue_check.data.get("name") == "default":
|
|
495
|
+
raise HTTPException(
|
|
496
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
497
|
+
detail="Cannot delete the default queue"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
result = (
|
|
501
|
+
client.table("environments")
|
|
502
|
+
.delete()
|
|
503
|
+
.eq("id", queue_id)
|
|
504
|
+
.eq("organization_id", organization["id"])
|
|
505
|
+
.execute()
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not result.data:
|
|
509
|
+
raise HTTPException(status_code=404, detail="Task queue not found")
|
|
510
|
+
|
|
511
|
+
logger.info("task_queue_deleted", queue_id=queue_id, org_id=organization["id"])
|
|
512
|
+
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
except HTTPException:
|
|
516
|
+
raise
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error("task_queue_delete_failed", error=str(e), queue_id=queue_id)
|
|
519
|
+
raise HTTPException(
|
|
520
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
521
|
+
detail=f"Failed to delete task queue: {str(e)}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@router.get("/{queue_name}/workers", response_model=List[WorkerHeartbeatResponse])
|
|
526
|
+
async def list_queue_workers(
|
|
527
|
+
queue_name: str,
|
|
528
|
+
request: Request,
|
|
529
|
+
organization: dict = Depends(get_current_organization),
|
|
530
|
+
):
|
|
531
|
+
"""
|
|
532
|
+
List all workers for a specific queue.
|
|
533
|
+
|
|
534
|
+
NOTE: This endpoint is deprecated. Workers are now organized by worker_queues.
|
|
535
|
+
Use GET /environments/{env_id}/worker-queues and worker_queues endpoints instead.
|
|
536
|
+
"""
|
|
537
|
+
logger.warning(
|
|
538
|
+
"deprecated_endpoint_called",
|
|
539
|
+
endpoint="/task-queues/{queue_name}/workers",
|
|
540
|
+
queue_name=queue_name,
|
|
541
|
+
org_id=organization["id"],
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Return empty list for backward compatibility
|
|
545
|
+
return []
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# Worker Registration
|
|
549
|
+
|
|
550
|
+
class WorkerCommandResponse(BaseModel):
|
|
551
|
+
"""Response with worker registration command"""
|
|
552
|
+
worker_token: str
|
|
553
|
+
task_queue_name: str
|
|
554
|
+
command: str
|
|
555
|
+
command_parts: dict # Broken down for UI display
|
|
556
|
+
namespace_status: str # pending, provisioning, ready, error
|
|
557
|
+
can_register: bool
|
|
558
|
+
provisioning_workflow_id: str | None = None
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@router.get("/{queue_id}/worker-command", response_model=WorkerCommandResponse)
|
|
562
|
+
async def get_worker_registration_command(
|
|
563
|
+
queue_id: str,
|
|
564
|
+
request: Request,
|
|
565
|
+
organization: dict = Depends(get_current_organization),
|
|
566
|
+
):
|
|
567
|
+
"""
|
|
568
|
+
Get the worker registration command for a task queue.
|
|
569
|
+
|
|
570
|
+
Returns the kubiya worker start command with the worker token that users
|
|
571
|
+
should run to start a worker for this task queue.
|
|
572
|
+
|
|
573
|
+
The UI should display this in a "Register Worker" dialog when the queue
|
|
574
|
+
is ready, and show provisioning status while the namespace is being created.
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
client = get_supabase()
|
|
578
|
+
|
|
579
|
+
# Get task queue
|
|
580
|
+
result = (
|
|
581
|
+
client.table("environments")
|
|
582
|
+
.select("*")
|
|
583
|
+
.eq("id", queue_id)
|
|
584
|
+
.eq("organization_id", organization["id"])
|
|
585
|
+
.single()
|
|
586
|
+
.execute()
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if not result.data:
|
|
590
|
+
raise HTTPException(status_code=404, detail="Task queue not found")
|
|
591
|
+
|
|
592
|
+
queue = result.data
|
|
593
|
+
worker_token = queue.get("worker_token")
|
|
594
|
+
|
|
595
|
+
# Generate worker_token if it doesn't exist (for existing queues)
|
|
596
|
+
if not worker_token:
|
|
597
|
+
worker_token = str(uuid.uuid4())
|
|
598
|
+
# Update the queue with the generated token
|
|
599
|
+
client.table("environments").update({
|
|
600
|
+
"worker_token": worker_token,
|
|
601
|
+
"updated_at": datetime.utcnow().isoformat(),
|
|
602
|
+
}).eq("id", queue_id).execute()
|
|
603
|
+
|
|
604
|
+
logger.info(
|
|
605
|
+
"worker_token_generated",
|
|
606
|
+
queue_id=queue_id,
|
|
607
|
+
org_id=organization["id"],
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
task_queue_name = queue["name"]
|
|
611
|
+
namespace_status = queue.get("status", "unknown")
|
|
612
|
+
provisioning_workflow_id = queue.get("provisioning_workflow_id")
|
|
613
|
+
|
|
614
|
+
# Check if namespace is ready
|
|
615
|
+
can_register = namespace_status in ["ready", "active"]
|
|
616
|
+
|
|
617
|
+
# Build command
|
|
618
|
+
command = f"kubiya worker start --token {worker_token} --task-queue {task_queue_name}"
|
|
619
|
+
|
|
620
|
+
command_parts = {
|
|
621
|
+
"binary": "kubiya",
|
|
622
|
+
"subcommand": "worker start",
|
|
623
|
+
"flags": {
|
|
624
|
+
"--token": worker_token,
|
|
625
|
+
"--task-queue": task_queue_name,
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
logger.info(
|
|
630
|
+
"worker_command_retrieved",
|
|
631
|
+
queue_id=queue_id,
|
|
632
|
+
can_register=can_register,
|
|
633
|
+
status=namespace_status,
|
|
634
|
+
org_id=organization["id"],
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
return WorkerCommandResponse(
|
|
638
|
+
worker_token=worker_token,
|
|
639
|
+
task_queue_name=task_queue_name,
|
|
640
|
+
command=command,
|
|
641
|
+
command_parts=command_parts,
|
|
642
|
+
namespace_status=namespace_status,
|
|
643
|
+
can_register=can_register,
|
|
644
|
+
provisioning_workflow_id=provisioning_workflow_id,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
except HTTPException:
|
|
648
|
+
raise
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.error("worker_command_get_failed", error=str(e), queue_id=queue_id)
|
|
651
|
+
raise HTTPException(
|
|
652
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
653
|
+
detail=f"Failed to get worker command: {str(e)}"
|
|
654
|
+
)
|