amalgam_parser/
k8s_types.rs

1//! Kubernetes core types fetcher and generator
2
3use crate::{imports::TypeReference, ParserError};
4use amalgam_core::{
5    ir::{Module, TypeDefinition},
6    types::{Field, Type},
7};
8use indicatif::{ProgressBar, ProgressStyle};
9use reqwest;
10use serde_json::Value;
11use std::collections::{BTreeMap, HashMap};
12use std::time::Duration;
13
14/// Fetches and generates k8s.io core types
15pub struct K8sTypesFetcher {
16    client: reqwest::Client,
17}
18
19impl Default for K8sTypesFetcher {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl K8sTypesFetcher {
26    pub fn new() -> Self {
27        Self {
28            client: reqwest::Client::builder()
29                .timeout(Duration::from_secs(60))
30                .user_agent("amalgam")
31                .build()
32                .unwrap(),
33        }
34    }
35
36    /// Fetch the Kubernetes OpenAPI schema
37    pub async fn fetch_k8s_openapi(&self, version: &str) -> Result<Value, ParserError> {
38        let is_tty = atty::is(atty::Stream::Stdout);
39
40        let pb = if is_tty {
41            let pb = ProgressBar::new_spinner();
42            pb.set_style(
43                ProgressStyle::default_spinner()
44                    .template("{spinner:.cyan} {msg}")
45                    .unwrap()
46                    .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
47            );
48            pb.enable_steady_tick(Duration::from_millis(100));
49            pb.set_message(format!("Fetching Kubernetes {} OpenAPI schema...", version));
50            Some(pb)
51        } else {
52            println!("Fetching Kubernetes {} OpenAPI schema...", version);
53            None
54        };
55
56        // We can use the official k8s OpenAPI spec
57        let url = format!(
58            "https://raw.githubusercontent.com/kubernetes/kubernetes/{}/api/openapi-spec/swagger.json",
59            version
60        );
61
62        let response = self
63            .client
64            .get(&url)
65            .send()
66            .await
67            .map_err(|e| ParserError::Network(e.to_string()))?;
68
69        if !response.status().is_success() {
70            if let Some(pb) = pb {
71                pb.finish_with_message(format!(
72                    "✗ Failed to fetch k8s OpenAPI: {}",
73                    response.status()
74                ));
75            }
76            return Err(ParserError::Network(format!(
77                "Failed to fetch k8s OpenAPI: {}",
78                response.status()
79            )));
80        }
81
82        if let Some(ref pb) = pb {
83            pb.set_message("Parsing OpenAPI schema...");
84        }
85
86        let schema: Value = response
87            .json()
88            .await
89            .map_err(|e| ParserError::Parse(e.to_string()))?;
90
91        if let Some(pb) = pb {
92            pb.finish_with_message(format!("✓ Fetched Kubernetes {} OpenAPI schema", version));
93        } else {
94            println!("Successfully fetched Kubernetes {} OpenAPI schema", version);
95        }
96
97        Ok(schema)
98    }
99
100    /// Extract all k8s types from specific namespaces dynamically
101    pub fn extract_core_types(
102        &self,
103        openapi: &Value,
104    ) -> Result<HashMap<TypeReference, TypeDefinition>, ParserError> {
105        let mut types = HashMap::new();
106
107        // Namespaces we want to extract - these contain the most commonly used types
108        let target_namespaces = [
109            "io.k8s.apimachinery.pkg.apis.meta.v1", // ObjectMeta, TypeMeta, etc.
110            "io.k8s.api.core.v1",                   // Pod, Service, ConfigMap, etc.
111            "io.k8s.api.apps.v1",                   // Deployment, StatefulSet, etc.
112            "io.k8s.api.batch.v1",                  // Job, CronJob
113            "io.k8s.api.networking.v1",             // Ingress, NetworkPolicy
114            "io.k8s.api.rbac.v1",                   // Role, RoleBinding, etc.
115            "io.k8s.api.storage.v1",                // StorageClass
116            "io.k8s.api.autoscaling.v1",            // HorizontalPodAutoscaler
117            "io.k8s.api.policy.v1",                 // PodDisruptionBudget
118            "io.k8s.apimachinery.pkg.api.resource", // Quantity
119        ];
120
121        if let Some(definitions) = openapi.get("definitions").and_then(|d| d.as_object()) {
122            // Iterate through all definitions
123            for (full_name, schema) in definitions {
124                // Check if this type is in one of our target namespaces
125                let should_include = target_namespaces
126                    .iter()
127                    .any(|&namespace| full_name.starts_with(namespace));
128
129                if should_include {
130                    // Extract the short name from the full type name
131                    let short_name = full_name
132                        .split('.')
133                        .next_back()
134                        .unwrap_or(full_name.as_str())
135                        .to_string();
136
137                    // Try to parse this as a k8s type reference
138                    match self.parse_type_reference(full_name) {
139                        Ok(type_ref) => {
140                            match self.schema_to_type_definition(&short_name, schema) {
141                                Ok(type_def) => {
142                                    types.insert(type_ref, type_def);
143                                }
144                                Err(e) => {
145                                    // Log but don't fail - some types might not parse correctly
146                                    tracing::debug!("Failed to parse type {}: {}", full_name, e);
147                                }
148                            }
149                        }
150                        Err(e) => {
151                            tracing::debug!("Failed to parse reference {}: {}", full_name, e);
152                        }
153                    }
154                }
155            }
156        }
157
158        tracing::info!("Extracted {} k8s types from OpenAPI schema", types.len());
159        Ok(types)
160    }
161
162    fn parse_type_reference(&self, full_name: &str) -> Result<TypeReference, ParserError> {
163        // Parse "io.k8s.api.core.v1.Container" format
164        let parts: Vec<&str> = full_name.split('.').collect();
165
166        if parts.len() < 5 || parts[0] != "io" || parts[1] != "k8s" {
167            return Err(ParserError::Parse(format!(
168                "Invalid k8s type name: {}",
169                full_name
170            )));
171        }
172
173        let group = if parts[3] == "core" || parts[2] == "apimachinery" {
174            "k8s.io".to_string() // core and apimachinery types are under k8s.io
175        } else {
176            format!("{}.k8s.io", parts[3])
177        };
178
179        let version = parts[parts.len() - 2].to_string();
180        let kind = parts.last().unwrap().to_string();
181
182        Ok(TypeReference::new(group, version, kind))
183    }
184
185    fn schema_to_type_definition(
186        &self,
187        name: &str,
188        schema: &Value,
189    ) -> Result<TypeDefinition, ParserError> {
190        let ty = self.json_schema_to_type(schema)?;
191
192        Ok(TypeDefinition {
193            name: name.to_string(),
194            ty,
195            documentation: schema
196                .get("description")
197                .and_then(|d| d.as_str())
198                .map(String::from),
199            annotations: BTreeMap::new(),
200        })
201    }
202
203    #[allow(clippy::only_used_in_recursion)]
204    fn json_schema_to_type(&self, schema: &Value) -> Result<Type, ParserError> {
205        // Check for top-level $ref first
206        if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
207            let type_name = ref_path.trim_start_matches("#/definitions/");
208
209            // Resolve k8s type references to basic types
210            return Ok(match type_name {
211                name if name.ends_with(".Time") || name.ends_with(".MicroTime") => Type::String,
212                name if name.ends_with(".Duration") => Type::String,
213                name if name.ends_with(".IntOrString") => {
214                    Type::Union(vec![Type::Integer, Type::String])
215                }
216                name if name.ends_with(".Quantity") => Type::String,
217                name if name.ends_with(".FieldsV1") => Type::Any,
218                name if name.starts_with("io.k8s.") => {
219                    // For k8s internal references, use short name
220                    let short_name = name.split('.').next_back().unwrap_or(name);
221                    Type::Reference(short_name.to_string())
222                }
223                _ => Type::Reference(type_name.to_string()),
224            });
225        }
226
227        let schema_type = schema.get("type").and_then(|v| v.as_str());
228
229        match schema_type {
230            Some("string") => Ok(Type::String),
231            Some("number") => Ok(Type::Number),
232            Some("integer") => Ok(Type::Integer),
233            Some("boolean") => Ok(Type::Bool),
234            Some("array") => {
235                let items = schema
236                    .get("items")
237                    .map(|i| self.json_schema_to_type(i))
238                    .transpose()?
239                    .unwrap_or(Type::Any);
240                Ok(Type::Array(Box::new(items)))
241            }
242            Some("object") => {
243                let mut fields = BTreeMap::new();
244
245                if let Some(Value::Object(props)) = schema.get("properties") {
246                    let required = schema
247                        .get("required")
248                        .and_then(|r| r.as_array())
249                        .map(|arr| {
250                            arr.iter()
251                                .filter_map(|v| v.as_str())
252                                .map(String::from)
253                                .collect::<Vec<_>>()
254                        })
255                        .unwrap_or_default();
256
257                    for (field_name, field_schema) in props {
258                        // Check for $ref
259                        if let Some(ref_path) = field_schema.get("$ref").and_then(|r| r.as_str()) {
260                            // Convert ref to type reference
261                            let type_name = ref_path.trim_start_matches("#/definitions/");
262
263                            // For k8s types, resolve common references to basic types
264                            let resolved_type = match type_name {
265                                // Time types should be strings
266                                name if name.ends_with(".Time") || name.ends_with(".MicroTime") => {
267                                    Type::String
268                                }
269                                // Duration is a string
270                                name if name.ends_with(".Duration") => Type::String,
271                                // IntOrString can be either
272                                name if name.ends_with(".IntOrString") => {
273                                    Type::Union(vec![Type::Integer, Type::String])
274                                }
275                                // Quantity is a string (like "100m" or "1Gi")
276                                name if name.ends_with(".Quantity")
277                                    || name == "io.k8s.apimachinery.pkg.api.resource.Quantity" =>
278                                {
279                                    Type::String
280                                }
281                                // FieldsV1 is a complex type, represent as Any for now
282                                name if name.ends_with(".FieldsV1") => Type::Any,
283                                // For other k8s references within the same module, keep as reference
284                                // but only use the short name
285                                name if name.starts_with("io.k8s.") => {
286                                    // Extract just the type name (last part)
287                                    let short_name = name.split('.').next_back().unwrap_or(name);
288                                    Type::Reference(short_name.to_string())
289                                }
290                                // Keep full reference for non-k8s types
291                                _ => Type::Reference(type_name.to_string()),
292                            };
293
294                            fields.insert(
295                                field_name.clone(),
296                                Field {
297                                    ty: resolved_type,
298                                    required: required.contains(field_name),
299                                    description: field_schema
300                                        .get("description")
301                                        .and_then(|d| d.as_str())
302                                        .map(String::from),
303                                    default: None,
304                                },
305                            );
306                        } else {
307                            // Check if this is a type string reference
308                            if field_schema.get("type").is_none()
309                                && field_schema.get("$ref").is_none()
310                            {
311                                // Check for x-kubernetes fields or direct type strings
312                                if let Value::String(type_str) = field_schema {
313                                    // This is a direct type reference string
314                                    let resolved_type = match type_str.as_str() {
315                                        // Handle k8s type references
316                                        s if s.ends_with(".Time") || s.ends_with(".MicroTime") => {
317                                            Type::String
318                                        }
319                                        s if s.ends_with(".Duration") => Type::String,
320                                        s if s.ends_with(".IntOrString") => {
321                                            Type::Union(vec![Type::Integer, Type::String])
322                                        }
323                                        s if s.ends_with(".Quantity") => Type::String,
324                                        s if s.ends_with(".FieldsV1") => Type::Any,
325                                        s if s.starts_with("io.k8s.") => {
326                                            // Extract just the type name (last part)
327                                            let short_name = s.split('.').next_back().unwrap_or(s);
328                                            Type::Reference(short_name.to_string())
329                                        }
330                                        _ => Type::Reference(type_str.clone()),
331                                    };
332
333                                    fields.insert(
334                                        field_name.clone(),
335                                        Field {
336                                            ty: resolved_type,
337                                            required: required.contains(field_name),
338                                            description: None,
339                                            default: None,
340                                        },
341                                    );
342                                    continue;
343                                }
344                            }
345
346                            let field_type = self.json_schema_to_type(field_schema)?;
347                            fields.insert(
348                                field_name.clone(),
349                                Field {
350                                    ty: field_type,
351                                    required: required.contains(field_name),
352                                    description: field_schema
353                                        .get("description")
354                                        .and_then(|d| d.as_str())
355                                        .map(String::from),
356                                    default: field_schema.get("default").cloned(),
357                                },
358                            );
359                        }
360                    }
361                }
362
363                let open = schema
364                    .get("additionalProperties")
365                    .map(|v| !matches!(v, Value::Bool(false)))
366                    .unwrap_or(false);
367
368                Ok(Type::Record { fields, open })
369            }
370            _ => {
371                // Check for $ref
372                if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
373                    let type_name = ref_path.trim_start_matches("#/definitions/");
374                    Ok(Type::Reference(type_name.to_string()))
375                } else {
376                    Ok(Type::Any)
377                }
378            }
379        }
380    }
381}
382
383/// Generate a basic k8s.io package with common types
384pub fn generate_k8s_package() -> Module {
385    let mut module = Module {
386        name: "k8s.io".to_string(),
387        imports: Vec::new(),
388        types: Vec::new(),
389        constants: Vec::new(),
390        metadata: Default::default(),
391    };
392
393    // Add ObjectMeta type (simplified)
394    let object_meta = TypeDefinition {
395        name: "ObjectMeta".to_string(),
396        ty: Type::Record {
397            fields: {
398                let mut fields = BTreeMap::new();
399                fields.insert(
400                    "name".to_string(),
401                    Field {
402                        ty: Type::Optional(Box::new(Type::String)),
403                        required: false,
404                        description: Some("Name must be unique within a namespace".to_string()),
405                        default: None,
406                    },
407                );
408                fields.insert(
409                    "namespace".to_string(),
410                    Field {
411                        ty: Type::Optional(Box::new(Type::String)),
412                        required: false,
413                        description: Some(
414                            "Namespace defines the space within which each name must be unique"
415                                .to_string(),
416                        ),
417                        default: None,
418                    },
419                );
420                fields.insert(
421                    "labels".to_string(),
422                    Field {
423                        ty: Type::Optional(Box::new(Type::Map {
424                            key: Box::new(Type::String),
425                            value: Box::new(Type::String),
426                        })),
427                        required: false,
428                        description: Some(
429                            "Map of string keys and values for organizing and categorizing objects"
430                                .to_string(),
431                        ),
432                        default: None,
433                    },
434                );
435                fields.insert(
436                    "annotations".to_string(),
437                    Field {
438                        ty: Type::Optional(Box::new(Type::Map {
439                            key: Box::new(Type::String),
440                            value: Box::new(Type::String),
441                        })),
442                        required: false,
443                        description: Some(
444                            "Annotations is an unstructured key value map".to_string(),
445                        ),
446                        default: None,
447                    },
448                );
449                fields.insert(
450                    "uid".to_string(),
451                    Field {
452                        ty: Type::Optional(Box::new(Type::String)),
453                        required: false,
454                        description: Some(
455                            "UID is the unique in time and space value for this object".to_string(),
456                        ),
457                        default: None,
458                    },
459                );
460                fields.insert(
461                    "resourceVersion".to_string(),
462                    Field {
463                        ty: Type::Optional(Box::new(Type::String)),
464                        required: false,
465                        description: Some(
466                            "An opaque value that represents the internal version of this object"
467                                .to_string(),
468                        ),
469                        default: None,
470                    },
471                );
472                fields
473            },
474            open: true, // Allow additional fields
475        },
476        documentation: Some(
477            "ObjectMeta is metadata that all persisted resources must have".to_string(),
478        ),
479        annotations: BTreeMap::new(),
480    };
481
482    module.types.push(object_meta);
483
484    // Add other common types...
485    // This is simplified - in reality we'd fetch these from the k8s OpenAPI spec
486
487    module
488}