Coverage for gco / models / manifest_models.py: 99%
101 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"""
2Manifest submission data models for GCO (Global Capacity Orchestrator on AWS).
4This module defines dataclasses for Kubernetes manifest processing:
5- KubernetesManifest: Single Kubernetes resource definition
6- ManifestSubmissionRequest: Request to submit one or more manifests
7- ManifestSubmissionResponse: Response with processing results
8- ResourceStatus: Status of a single resource operation
10These models are used by the manifest processor service to validate,
11process, and track Kubernetes resource submissions.
12"""
14from __future__ import annotations
16from dataclasses import dataclass
17from typing import Any
20@dataclass
21class KubernetesManifest:
22 """
23 Represents a single Kubernetes manifest.
25 Attributes:
26 apiVersion: Kubernetes API version (e.g., 'apps/v1', 'v1')
27 kind: Resource kind (e.g., 'Deployment', 'Service', 'ConfigMap')
28 metadata: Resource metadata including name and namespace
29 spec: Resource specification (for most resource types)
30 data: Resource data (for ConfigMaps and Secrets)
31 """
33 apiVersion: str
34 kind: str
35 metadata: dict[str, Any]
36 spec: dict[str, Any] | None = None
37 data: dict[str, Any] | None = None
39 def __post_init__(self) -> None:
40 """Validate Kubernetes manifest structure"""
41 if not self.apiVersion:
42 raise ValueError("apiVersion cannot be empty")
44 if not self.kind:
45 raise ValueError("kind cannot be empty")
47 if not isinstance(self.metadata, dict):
48 raise ValueError("metadata must be a dictionary")
50 if "name" not in self.metadata:
51 raise ValueError("metadata must contain a 'name' field")
53 # Either spec or data should be present (or both for some resources)
54 if self.spec is not None and not isinstance(self.spec, dict):
55 raise ValueError("spec must be a dictionary")
57 if self.data is not None and not isinstance(self.data, dict):
58 raise ValueError("data must be a dictionary")
60 # At least one of spec or data should be present for most resources
61 if (
62 self.spec is None
63 and self.data is None
64 and self.kind not in ["Namespace", "ServiceAccount"]
65 ):
66 raise ValueError("Either spec or data must be provided for most resource types")
68 def get_name(self) -> str:
69 """Get the resource name"""
70 return str(self.metadata.get("name", ""))
72 def get_namespace(self) -> str:
73 """Get the resource namespace"""
74 return str(self.metadata.get("namespace", "default"))
76 def to_dict(self) -> dict[str, Any]:
77 """Convert to dictionary representation"""
78 result = {"apiVersion": self.apiVersion, "kind": self.kind, "metadata": self.metadata}
80 if self.spec is not None:
81 result["spec"] = self.spec
83 if self.data is not None:
84 result["data"] = self.data
86 return result
88 @classmethod
89 def from_dict(cls, data: dict[str, Any]) -> KubernetesManifest:
90 """Create from dictionary representation"""
91 return cls(
92 apiVersion=data["apiVersion"],
93 kind=data["kind"],
94 metadata=data["metadata"],
95 spec=data.get("spec"),
96 data=data.get("data"),
97 )
100@dataclass
101class ManifestSubmissionRequest:
102 """Request to submit Kubernetes manifests"""
104 manifests: list[dict[str, Any]]
105 namespace: str | None = None
106 dry_run: bool = False
107 validate: bool = True
109 def __post_init__(self) -> None:
110 """Validate submission request"""
111 if not self.manifests:
112 raise ValueError("At least one manifest must be provided")
114 # Validate each manifest can be parsed
115 for i, manifest_data in enumerate(self.manifests):
116 try:
117 KubernetesManifest.from_dict(manifest_data)
118 except Exception as e:
119 raise ValueError(f"Invalid manifest at index {i}: {e}") from e
121 def get_kubernetes_manifests(self) -> list[KubernetesManifest]:
122 """Convert to KubernetesManifest objects"""
123 return [KubernetesManifest.from_dict(manifest) for manifest in self.manifests]
125 def get_resource_count(self) -> int:
126 """Get total number of resources to be created"""
127 return len(self.manifests)
130@dataclass
131class ResourceStatus:
132 """Status of a deployed Kubernetes resource"""
134 api_version: str
135 kind: str
136 name: str
137 namespace: str
138 status: str # 'created', 'updated', 'unchanged', 'failed'
139 message: str | None = None
140 uid: str | None = None # Kubernetes resource UID
142 def __post_init__(self) -> None:
143 """Validate resource status"""
144 valid_statuses = {"created", "updated", "unchanged", "failed", "deleted"}
145 if self.status not in valid_statuses:
146 raise ValueError(f"Status must be one of {valid_statuses}, got {self.status}")
148 if not self.name:
149 raise ValueError("Resource name cannot be empty")
151 if not self.namespace:
152 raise ValueError("Resource namespace cannot be empty")
154 def is_successful(self) -> bool:
155 """Check if the resource operation was successful"""
156 return self.status in {"created", "updated", "unchanged", "deleted"}
158 def get_resource_identifier(self) -> str:
159 """Get unique identifier for this resource"""
160 return f"{self.api_version}/{self.kind}/{self.namespace}/{self.name}"
163@dataclass
164class ManifestSubmissionResponse:
165 """Response from manifest submission"""
167 success: bool
168 cluster_id: str
169 region: str
170 resources: list[ResourceStatus]
171 errors: list[str] | None = None
173 def __post_init__(self) -> None:
174 """Validate submission response"""
175 if not self.cluster_id:
176 raise ValueError("Cluster ID cannot be empty")
178 if not self.region:
179 raise ValueError("Region cannot be empty")
181 if not isinstance(self.resources, list):
182 raise ValueError("Resources must be a list")
184 def get_successful_resources(self) -> list[ResourceStatus]:
185 """Get list of successfully processed resources"""
186 return [r for r in self.resources if r.is_successful()]
188 def get_failed_resources(self) -> list[ResourceStatus]:
189 """Get list of failed resources"""
190 return [r for r in self.resources if not r.is_successful()]
192 def get_summary(self) -> dict[str, int]:
193 """Get summary of resource processing results"""
194 summary = {"created": 0, "updated": 0, "unchanged": 0, "failed": 0}
195 for resource in self.resources:
196 summary[resource.status] += 1
197 return summary