Coverage for gco / services / api_routes / manifests.py: 97%
85 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 21:47 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 21:47 +0000
1"""Manifest submission, validation, and resource management endpoints."""
3from __future__ import annotations
5import logging
6from datetime import UTC, datetime
7from typing import Any
9from fastapi import APIRouter, HTTPException, Query
10from fastapi.responses import JSONResponse, Response
12from gco.models import ManifestSubmissionRequest
13from gco.services.api_shared import ManifestSubmissionAPIRequest, _check_processor
15router = APIRouter(prefix="/api/v1/manifests", tags=["Manifests"])
16logger = logging.getLogger(__name__)
19@router.post("")
20async def submit_manifests(request: ManifestSubmissionAPIRequest) -> Response:
21 """Submit Kubernetes manifests for processing."""
22 from gco.services.manifest_api import manifest_metrics
24 processor = _check_processor()
26 try:
27 logger.info(f"Received manifest submission request with {len(request.manifests)} manifests")
29 try:
30 submission_request = ManifestSubmissionRequest(
31 manifests=request.manifests,
32 namespace=request.namespace,
33 dry_run=request.dry_run,
34 validate=request.validate_manifests,
35 )
36 except ValueError as e:
37 # Client-side validation failures (empty manifests, unparseable
38 # payloads, etc.) should surface as 400, not 500.
39 logger.info(f"Rejected manifest submission as invalid input: {e}")
40 raise HTTPException(status_code=400, detail=str(e)) from e
42 response = await processor.process_manifest_submission(submission_request)
44 if manifest_metrics and not request.dry_run: 44 ↛ 62line 44 didn't jump to line 62 because the condition on line 44 was always true
45 try:
46 successful = sum(1 for r in response.resources if r.is_successful())
47 failed = sum(1 for r in response.resources if not r.is_successful())
48 validation_failures = sum(
49 1
50 for r in response.resources
51 if r.status == "failed" and "validation" in (r.message or "").lower()
52 )
53 manifest_metrics.publish_submission_metrics(
54 total_submissions=len(response.resources),
55 successful_submissions=successful,
56 failed_submissions=failed,
57 validation_failures=validation_failures,
58 )
59 except Exception as e:
60 logger.warning(f"Failed to publish manifest metrics: {e}")
62 api_response: dict[str, Any] = {
63 "success": response.success,
64 "cluster_id": response.cluster_id,
65 "region": response.region,
66 "timestamp": datetime.now(UTC).isoformat(),
67 "summary": response.get_summary(),
68 "resources": [
69 {
70 "api_version": r.api_version,
71 "kind": r.kind,
72 "name": r.name,
73 "namespace": r.namespace,
74 "status": r.status,
75 "message": r.message,
76 }
77 for r in response.resources
78 ],
79 }
81 if response.errors:
82 api_response["errors"] = response.errors
84 status_code = 200 if response.success else 400
85 return JSONResponse(status_code=status_code, content=api_response)
87 except HTTPException:
88 # Already a well-formed HTTP error — don't demote to 500.
89 raise
90 except Exception as e:
91 logger.error(f"Error processing manifest submission: {e}")
92 raise HTTPException(status_code=500, detail=f"Internal server error: {e!s}") from e
95@router.post("/validate")
96async def validate_manifests(request: ManifestSubmissionAPIRequest) -> Response:
97 """Validate manifests without applying them."""
98 processor = _check_processor()
100 try:
101 logger.info(f"Validating {len(request.manifests)} manifests")
103 validation_results = []
104 overall_valid = True
106 for i, manifest in enumerate(request.manifests):
107 is_valid, error_msg = processor.validate_manifest(manifest)
109 result: dict[str, Any] = {
110 "manifest_index": i,
111 "valid": is_valid,
112 "api_version": manifest.get("apiVersion", "unknown"),
113 "kind": manifest.get("kind", "unknown"),
114 "name": manifest.get("metadata", {}).get("name", f"manifest-{i + 1}"),
115 "namespace": manifest.get("metadata", {}).get(
116 "namespace", request.namespace or "default"
117 ),
118 }
120 if not is_valid:
121 result["error"] = error_msg
122 overall_valid = False
124 validation_results.append(result)
126 response = {
127 "valid": overall_valid,
128 "cluster_id": processor.cluster_id,
129 "region": processor.region,
130 "timestamp": datetime.now(UTC).isoformat(),
131 "total_manifests": len(request.manifests),
132 "valid_manifests": sum(1 for r in validation_results if r["valid"]),
133 "invalid_manifests": sum(1 for r in validation_results if not r["valid"]),
134 "results": validation_results,
135 }
137 return JSONResponse(status_code=200, content=response)
139 except Exception as e:
140 logger.error(f"Error validating manifests: {e}")
141 raise HTTPException(status_code=500, detail="Internal server error") from e
144@router.get("/{namespace}/{name}")
145async def get_resource_status(
146 namespace: str,
147 name: str,
148 api_version: str = Query("apps/v1", description="Kubernetes API version"),
149 kind: str = Query("Deployment", description="Resource kind"),
150) -> Response:
151 """Get the status of a specific resource."""
152 processor = _check_processor()
154 try:
155 resource_info = await processor.get_resource_status(
156 api_version=api_version, kind=kind, name=name, namespace=namespace
157 )
159 if resource_info is None:
160 raise HTTPException(status_code=500, detail="Failed to retrieve resource information")
162 response = {
163 "cluster_id": processor.cluster_id,
164 "region": processor.region,
165 "timestamp": datetime.now(UTC).isoformat(),
166 "resource": resource_info,
167 }
169 status_code = 200 if resource_info.get("exists", False) else 404
170 return JSONResponse(status_code=status_code, content=response)
172 except HTTPException:
173 raise
174 except Exception as e:
175 logger.error(f"Error getting resource status: {e}")
176 raise HTTPException(status_code=500, detail=f"Internal server error: {e!s}") from e
179@router.delete("/{namespace}/{name}")
180async def delete_resource(
181 namespace: str,
182 name: str,
183 api_version: str = Query("apps/v1", description="Kubernetes API version"),
184 kind: str = Query("Deployment", description="Resource kind"),
185) -> Response:
186 """Delete a specific resource from the cluster."""
187 processor = _check_processor()
189 try:
190 resource_status = await processor.delete_resource(
191 api_version=api_version, kind=kind, name=name, namespace=namespace
192 )
194 response = {
195 "success": resource_status.is_successful(),
196 "cluster_id": processor.cluster_id,
197 "region": processor.region,
198 "timestamp": datetime.now(UTC).isoformat(),
199 "resource": {
200 "api_version": resource_status.api_version,
201 "kind": resource_status.kind,
202 "name": resource_status.name,
203 "namespace": resource_status.namespace,
204 "status": resource_status.status,
205 "message": resource_status.message,
206 },
207 }
209 status_code = 200 if resource_status.is_successful() else 400
210 return JSONResponse(status_code=status_code, content=response)
212 except Exception as e:
213 logger.error(f"Error deleting resource: {e}")
214 raise HTTPException(status_code=500, detail=f"Internal server error: {e!s}") from e