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 using recursive discovery from seed types
101    pub fn extract_core_types(
102        &self,
103        openapi: &Value,
104    ) -> Result<HashMap<TypeReference, TypeDefinition>, ParserError> {
105        let mut types = HashMap::new();
106        let mut processed = std::collections::HashSet::new();
107        let mut to_process = std::collections::VecDeque::new();
108
109        // Seed types that will trigger recursive discovery
110        // Include types from various API versions to ensure comprehensive coverage
111        let seed_types = vec![
112            // Core metadata types (v1 - stable)
113            "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
114            "io.k8s.apimachinery.pkg.apis.meta.v1.TypeMeta",
115            "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta",
116            "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector",
117            "io.k8s.apimachinery.pkg.apis.meta.v1.Time",
118            "io.k8s.apimachinery.pkg.apis.meta.v1.MicroTime",
119            "io.k8s.apimachinery.pkg.apis.meta.v1.Status",
120            "io.k8s.apimachinery.pkg.apis.meta.v1.Condition",
121            // Unversioned runtime types
122            "io.k8s.apimachinery.pkg.runtime.RawExtension",
123            "io.k8s.apimachinery.pkg.util.intstr.IntOrString",
124            // Core v1 workloads and resources
125            "io.k8s.api.core.v1.Pod",
126            "io.k8s.api.core.v1.Service",
127            "io.k8s.api.core.v1.ConfigMap",
128            "io.k8s.api.core.v1.Secret",
129            "io.k8s.api.core.v1.Node",
130            "io.k8s.api.core.v1.Namespace",
131            "io.k8s.api.core.v1.PersistentVolume",
132            "io.k8s.api.core.v1.PersistentVolumeClaim",
133            "io.k8s.api.core.v1.ServiceAccount",
134            "io.k8s.api.core.v1.Endpoints",
135            "io.k8s.api.core.v1.Event",
136            // Apps v1 (stable)
137            "io.k8s.api.apps.v1.Deployment",
138            "io.k8s.api.apps.v1.StatefulSet",
139            "io.k8s.api.apps.v1.DaemonSet",
140            "io.k8s.api.apps.v1.ReplicaSet",
141            // Batch v1 (stable)
142            "io.k8s.api.batch.v1.Job",
143            "io.k8s.api.batch.v1.CronJob",
144            // Networking v1 (stable)
145            "io.k8s.api.networking.v1.Ingress",
146            "io.k8s.api.networking.v1.NetworkPolicy",
147            "io.k8s.api.networking.v1.IngressClass",
148            // RBAC v1 (stable)
149            "io.k8s.api.rbac.v1.Role",
150            "io.k8s.api.rbac.v1.RoleBinding",
151            "io.k8s.api.rbac.v1.ClusterRole",
152            "io.k8s.api.rbac.v1.ClusterRoleBinding",
153            // Storage v1 (stable)
154            "io.k8s.api.storage.v1.StorageClass",
155            "io.k8s.api.storage.v1.VolumeAttachment",
156            "io.k8s.api.storage.v1.CSIDriver",
157            "io.k8s.api.storage.v1.CSINode",
158            "io.k8s.api.storage.v1.CSIStorageCapacity",
159            // Storage v1alpha1 & v1beta1 (beta/alpha APIs)
160            "io.k8s.api.storage.v1alpha1.VolumeAttributesClass",
161            "io.k8s.api.storage.v1beta1.VolumeAttributesClass",
162            // Policy v1 (stable)
163            "io.k8s.api.policy.v1.PodDisruptionBudget",
164            "io.k8s.api.policy.v1.Eviction",
165            // Autoscaling v1, v2 (stable and versioned)
166            "io.k8s.api.autoscaling.v1.HorizontalPodAutoscaler",
167            "io.k8s.api.autoscaling.v2.HorizontalPodAutoscaler",
168            // Networking v1beta1 (beta APIs for newer features)
169            "io.k8s.api.networking.v1beta1.IPAddress",
170            "io.k8s.api.networking.v1beta1.ServiceCIDR",
171            // Resource quantities and other utilities
172            "io.k8s.apimachinery.pkg.api.resource.Quantity",
173        ];
174
175        // Initialize with seed types
176        for seed in seed_types {
177            to_process.push_back(seed.to_string());
178        }
179
180        if let Some(definitions) = openapi.get("definitions").and_then(|d| d.as_object()) {
181            while let Some(full_name) = to_process.pop_front() {
182                if processed.contains(&full_name) {
183                    continue;
184                }
185                processed.insert(full_name.clone());
186
187                if let Some(schema) = definitions.get(&full_name) {
188                    // Extract the short name from the full type name
189                    let short_name = full_name
190                        .split('.')
191                        .next_back()
192                        .unwrap_or(full_name.as_str())
193                        .to_string();
194
195                    // Try to parse this as a k8s type reference
196                    match self.parse_type_reference(&full_name) {
197                        Ok(type_ref) => {
198                            match self.schema_to_type_definition(&short_name, schema) {
199                                Ok(type_def) => {
200                                    // Collect all type references from this type
201                                    let mut refs = std::collections::HashSet::new();
202                                    Self::collect_schema_references(schema, &mut refs);
203
204                                    // Add referenced types to processing queue
205                                    for ref_name in refs {
206                                        if !processed.contains(&ref_name)
207                                            && definitions.contains_key(&ref_name)
208                                        {
209                                            to_process.push_back(ref_name);
210                                        }
211                                    }
212
213                                    types.insert(type_ref, type_def);
214                                }
215                                Err(e) => {
216                                    // Log but don't fail - some types might not parse correctly
217                                    tracing::debug!("Failed to parse type {}: {}", full_name, e);
218                                }
219                            }
220                        }
221                        Err(e) => {
222                            tracing::debug!("Failed to parse reference {}: {}", full_name, e);
223                        }
224                    }
225                }
226            }
227        }
228
229        tracing::info!(
230            "Extracted {} k8s types from OpenAPI schema using recursive discovery",
231            types.len()
232        );
233        Ok(types)
234    }
235
236    /// Recursively collect all type references from a JSON schema
237    fn collect_schema_references(schema: &Value, refs: &mut std::collections::HashSet<String>) {
238        match schema {
239            Value::Object(obj) => {
240                // Check for $ref
241                if let Some(ref_val) = obj.get("$ref").and_then(|r| r.as_str()) {
242                    if ref_val.starts_with("#/definitions/") {
243                        let type_name = ref_val.strip_prefix("#/definitions/").unwrap();
244                        refs.insert(type_name.to_string());
245                    }
246                }
247
248                // Recursively check all values in the object
249                for value in obj.values() {
250                    Self::collect_schema_references(value, refs);
251                }
252            }
253            Value::Array(arr) => {
254                // Recursively check all values in the array
255                for value in arr {
256                    Self::collect_schema_references(value, refs);
257                }
258            }
259            _ => {}
260        }
261    }
262
263    fn parse_type_reference(&self, full_name: &str) -> Result<TypeReference, ParserError> {
264        // Parse different k8s type name formats:
265        // - "io.k8s.api.core.v1.Container" (versioned)
266        // - "io.k8s.apimachinery.pkg.runtime.RawExtension" (unversioned)
267        let parts: Vec<&str> = full_name.split('.').collect();
268
269        if parts.len() < 4 || parts[0] != "io" || parts[1] != "k8s" {
270            return Err(ParserError::Parse(format!(
271                "Invalid k8s type name: {}",
272                full_name
273            )));
274        }
275
276        let group = if parts[3] == "core" || parts[2] == "apimachinery" {
277            "k8s.io".to_string() // core and apimachinery types are under k8s.io
278        } else {
279            format!("{}.k8s.io", parts[3])
280        };
281
282        // Check if this is an unversioned type (like runtime.RawExtension)
283        let is_unversioned = parts.contains(&"runtime") || parts.contains(&"util");
284
285        let (version, kind) = if is_unversioned {
286            // For unversioned types, use v0 and the last part as kind
287            ("v0".to_string(), parts.last().unwrap().to_string())
288        } else {
289            // For versioned types, use the standard pattern
290            if parts.len() < 5 {
291                return Err(ParserError::Parse(format!(
292                    "Invalid versioned k8s type name: {}",
293                    full_name
294                )));
295            }
296            (
297                parts[parts.len() - 2].to_string(),
298                parts.last().unwrap().to_string(),
299            )
300        };
301
302        Ok(TypeReference::new(group, version, kind))
303    }
304
305    fn schema_to_type_definition(
306        &self,
307        name: &str,
308        schema: &Value,
309    ) -> Result<TypeDefinition, ParserError> {
310        let ty = self.json_schema_to_type(schema)?;
311
312        Ok(TypeDefinition {
313            name: name.to_string(),
314            ty,
315            documentation: schema
316                .get("description")
317                .and_then(|d| d.as_str())
318                .map(String::from),
319            annotations: BTreeMap::new(),
320        })
321    }
322
323    #[allow(clippy::only_used_in_recursion)]
324    fn json_schema_to_type(&self, schema: &Value) -> Result<Type, ParserError> {
325        // Check for top-level $ref first
326        if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
327            let type_name = ref_path.trim_start_matches("#/definitions/");
328
329            // Resolve k8s type references to basic types
330            return Ok(match type_name {
331                name if name.ends_with(".Time") || name.ends_with(".MicroTime") => Type::String,
332                name if name.ends_with(".Duration") => Type::String,
333                name if name.ends_with(".IntOrString") => {
334                    Type::Union(vec![Type::Integer, Type::String])
335                }
336                name if name.ends_with(".Quantity") => Type::String,
337                name if name.ends_with(".FieldsV1") => Type::Any,
338                name if name.starts_with("io.k8s.") => {
339                    // For k8s internal references, use short name
340                    let short_name = name.split('.').next_back().unwrap_or(name);
341                    Type::Reference(short_name.to_string())
342                }
343                _ => Type::Reference(type_name.to_string()),
344            });
345        }
346
347        let schema_type = schema.get("type").and_then(|v| v.as_str());
348
349        match schema_type {
350            Some("string") => Ok(Type::String),
351            Some("number") => Ok(Type::Number),
352            Some("integer") => Ok(Type::Integer),
353            Some("boolean") => Ok(Type::Bool),
354            Some("array") => {
355                let items = schema
356                    .get("items")
357                    .map(|i| self.json_schema_to_type(i))
358                    .transpose()?
359                    .unwrap_or(Type::Any);
360                Ok(Type::Array(Box::new(items)))
361            }
362            Some("object") => {
363                let mut fields = BTreeMap::new();
364
365                if let Some(Value::Object(props)) = schema.get("properties") {
366                    let required = schema
367                        .get("required")
368                        .and_then(|r| r.as_array())
369                        .map(|arr| {
370                            arr.iter()
371                                .filter_map(|v| v.as_str())
372                                .map(String::from)
373                                .collect::<Vec<_>>()
374                        })
375                        .unwrap_or_default();
376
377                    for (field_name, field_schema) in props {
378                        // Check for $ref
379                        if let Some(ref_path) = field_schema.get("$ref").and_then(|r| r.as_str()) {
380                            // Convert ref to type reference
381                            let type_name = ref_path.trim_start_matches("#/definitions/");
382
383                            // For k8s types, resolve common references to basic types
384                            let resolved_type = match type_name {
385                                // Time types should be strings
386                                name if name.ends_with(".Time") || name.ends_with(".MicroTime") => {
387                                    Type::String
388                                }
389                                // Duration is a string
390                                name if name.ends_with(".Duration") => Type::String,
391                                // IntOrString can be either
392                                name if name.ends_with(".IntOrString") => {
393                                    Type::Union(vec![Type::Integer, Type::String])
394                                }
395                                // Quantity is a string (like "100m" or "1Gi")
396                                name if name.ends_with(".Quantity")
397                                    || name == "io.k8s.apimachinery.pkg.api.resource.Quantity" =>
398                                {
399                                    Type::String
400                                }
401                                // FieldsV1 is a complex type, represent as Any for now
402                                name if name.ends_with(".FieldsV1") => Type::Any,
403                                // For other k8s references within the same module, keep as reference
404                                // but only use the short name
405                                name if name.starts_with("io.k8s.") => {
406                                    // Extract just the type name (last part)
407                                    let short_name = name.split('.').next_back().unwrap_or(name);
408                                    Type::Reference(short_name.to_string())
409                                }
410                                // Keep full reference for non-k8s types
411                                _ => Type::Reference(type_name.to_string()),
412                            };
413
414                            fields.insert(
415                                field_name.clone(),
416                                Field {
417                                    ty: resolved_type,
418                                    required: required.contains(field_name),
419                                    description: field_schema
420                                        .get("description")
421                                        .and_then(|d| d.as_str())
422                                        .map(String::from),
423                                    default: None,
424                                },
425                            );
426                        } else {
427                            // Check if this is a type string reference
428                            if field_schema.get("type").is_none()
429                                && field_schema.get("$ref").is_none()
430                            {
431                                // Check for x-kubernetes fields or direct type strings
432                                if let Value::String(type_str) = field_schema {
433                                    // This is a direct type reference string
434                                    let resolved_type = match type_str.as_str() {
435                                        // Handle k8s type references
436                                        s if s.ends_with(".Time") || s.ends_with(".MicroTime") => {
437                                            Type::String
438                                        }
439                                        s if s.ends_with(".Duration") => Type::String,
440                                        s if s.ends_with(".IntOrString") => {
441                                            Type::Union(vec![Type::Integer, Type::String])
442                                        }
443                                        s if s.ends_with(".Quantity") => Type::String,
444                                        s if s.ends_with(".FieldsV1") => Type::Any,
445                                        s if s.starts_with("io.k8s.") => {
446                                            // Extract just the type name (last part)
447                                            let short_name = s.split('.').next_back().unwrap_or(s);
448                                            Type::Reference(short_name.to_string())
449                                        }
450                                        _ => Type::Reference(type_str.clone()),
451                                    };
452
453                                    fields.insert(
454                                        field_name.clone(),
455                                        Field {
456                                            ty: resolved_type,
457                                            required: required.contains(field_name),
458                                            description: None,
459                                            default: None,
460                                        },
461                                    );
462                                    continue;
463                                }
464                            }
465
466                            let field_type = self.json_schema_to_type(field_schema)?;
467                            fields.insert(
468                                field_name.clone(),
469                                Field {
470                                    ty: field_type,
471                                    required: required.contains(field_name),
472                                    description: field_schema
473                                        .get("description")
474                                        .and_then(|d| d.as_str())
475                                        .map(String::from),
476                                    default: field_schema.get("default").cloned(),
477                                },
478                            );
479                        }
480                    }
481                }
482
483                let open = schema
484                    .get("additionalProperties")
485                    .map(|v| !matches!(v, Value::Bool(false)))
486                    .unwrap_or(false);
487
488                Ok(Type::Record { fields, open })
489            }
490            _ => {
491                // Check for $ref
492                if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
493                    let type_name = ref_path.trim_start_matches("#/definitions/");
494                    Ok(Type::Reference(type_name.to_string()))
495                } else {
496                    Ok(Type::Any)
497                }
498            }
499        }
500    }
501}
502
503/// Generate a basic k8s.io package with common types
504pub fn generate_k8s_package() -> Module {
505    let mut module = Module {
506        name: "k8s.io".to_string(),
507        imports: Vec::new(),
508        types: Vec::new(),
509        constants: Vec::new(),
510        metadata: Default::default(),
511    };
512
513    // Add ObjectMeta type (simplified)
514    let object_meta = TypeDefinition {
515        name: "ObjectMeta".to_string(),
516        ty: Type::Record {
517            fields: {
518                let mut fields = BTreeMap::new();
519                fields.insert(
520                    "name".to_string(),
521                    Field {
522                        ty: Type::Optional(Box::new(Type::String)),
523                        required: false,
524                        description: Some("Name must be unique within a namespace".to_string()),
525                        default: None,
526                    },
527                );
528                fields.insert(
529                    "namespace".to_string(),
530                    Field {
531                        ty: Type::Optional(Box::new(Type::String)),
532                        required: false,
533                        description: Some(
534                            "Namespace defines the space within which each name must be unique"
535                                .to_string(),
536                        ),
537                        default: None,
538                    },
539                );
540                fields.insert(
541                    "labels".to_string(),
542                    Field {
543                        ty: Type::Optional(Box::new(Type::Map {
544                            key: Box::new(Type::String),
545                            value: Box::new(Type::String),
546                        })),
547                        required: false,
548                        description: Some(
549                            "Map of string keys and values for organizing and categorizing objects"
550                                .to_string(),
551                        ),
552                        default: None,
553                    },
554                );
555                fields.insert(
556                    "annotations".to_string(),
557                    Field {
558                        ty: Type::Optional(Box::new(Type::Map {
559                            key: Box::new(Type::String),
560                            value: Box::new(Type::String),
561                        })),
562                        required: false,
563                        description: Some(
564                            "Annotations is an unstructured key value map".to_string(),
565                        ),
566                        default: None,
567                    },
568                );
569                fields.insert(
570                    "uid".to_string(),
571                    Field {
572                        ty: Type::Optional(Box::new(Type::String)),
573                        required: false,
574                        description: Some(
575                            "UID is the unique in time and space value for this object".to_string(),
576                        ),
577                        default: None,
578                    },
579                );
580                fields.insert(
581                    "resourceVersion".to_string(),
582                    Field {
583                        ty: Type::Optional(Box::new(Type::String)),
584                        required: false,
585                        description: Some(
586                            "An opaque value that represents the internal version of this object"
587                                .to_string(),
588                        ),
589                        default: None,
590                    },
591                );
592                fields
593            },
594            open: true, // Allow additional fields
595        },
596        documentation: Some(
597            "ObjectMeta is metadata that all persisted resources must have".to_string(),
598        ),
599        annotations: BTreeMap::new(),
600    };
601
602    module.types.push(object_meta);
603
604    // Add other common types...
605    // This is simplified - in reality we'd fetch these from the k8s OpenAPI spec
606
607    module
608}