package parser import ( "fmt" "sort" "strings" "time" "kubeviz/internal/model" ) var clusterScopedKinds = map[string]bool{ "Namespace": true, "Node": true, "PersistentVolume": true, "CustomResourceDefinition": true, "ClusterRole": true, "ClusterRoleBinding": true, "MutatingWebhookConfiguration": true, "ValidatingWebhookConfiguration": true, "StorageClass": true, "PriorityClass": true, "APIService": true, } func ParseManifests(input []byte) (*model.Dataset, error) { dataset := &model.Dataset{ Resources: make(map[string]*model.Resource), CreatedAt: time.Now(), } docs, err := parseYAMLDocuments(input) if err != nil { return nil, err } for docNum, doc := range docs { if doc == nil { continue } if err := parseDocument(doc, docNum+1, dataset); err != nil { dataset.Summary.Issues = append(dataset.Summary.Issues, model.ParseIssue{ Document: docNum + 1, Message: err.Error(), }) } } dataset.Summary.Resources = len(dataset.Resources) dataset.ModifiedAt = time.Now() return dataset, nil } func parseDocument(doc any, docNum int, dataset *model.Dataset) error { normalized, ok := normalizeMap(doc).(map[string]any) if !ok { return fmt.Errorf("document is not an object") } kind, _ := normalized["kind"].(string) if kind == "List" { items, _ := normalized["items"].([]any) for idx, item := range items { itemMap, ok := normalizeMap(item).(map[string]any) if !ok { dataset.Summary.Issues = append(dataset.Summary.Issues, model.ParseIssue{ Document: docNum, Message: fmt.Sprintf("item %d is not an object", idx), }) continue } res, err := normalizeResource(itemMap) if err != nil { dataset.Summary.Issues = append(dataset.Summary.Issues, model.ParseIssue{ Document: docNum, Message: fmt.Sprintf("item %d: %v", idx, err), }) continue } if _, exists := dataset.Resources[res.ID]; exists { dataset.Duplicates = append(dataset.Duplicates, res.ID) dataset.Summary.Issues = append(dataset.Summary.Issues, model.ParseIssue{ Document: docNum, Message: fmt.Sprintf("duplicate resource id %q detected", res.ID), }) } dataset.Resources[res.ID] = res } return nil } res, err := normalizeResource(normalized) if err != nil { return err } if _, exists := dataset.Resources[res.ID]; exists { dataset.Duplicates = append(dataset.Duplicates, res.ID) } dataset.Resources[res.ID] = res return nil } func normalizeResource(raw map[string]any) (*model.Resource, error) { apiVersion, _ := raw["apiVersion"].(string) kind, _ := raw["kind"].(string) meta, _ := raw["metadata"].(map[string]any) name, _ := meta["name"].(string) if apiVersion == "" { return nil, fmt.Errorf("missing apiVersion") } if kind == "" { return nil, fmt.Errorf("missing kind") } if name == "" { return nil, fmt.Errorf("missing metadata.name") } namespace := "default" clusterScoped := clusterScopedKinds[kind] if ns, ok := meta["namespace"].(string); ok && ns != "" { namespace = ns } if clusterScoped { namespace = "" } labels := extractStringMap(meta["labels"]) id := resourceID(namespace, kind, name) res := &model.Resource{ ID: id, APIVersion: apiVersion, Kind: kind, Name: name, Namespace: namespace, ClusterScoped: clusterScoped, Labels: labels, Raw: deepCopy(raw), IsSensitive: strings.EqualFold(kind, "Secret"), CreatedAt: time.Now(), } if res.IsSensitive { res.KeyNames = extractSecretKeyNames(raw) redactSecretValues(res.Raw) } res.OwnerRefs = extractOwnerRefs(meta) res.References = append(res.References, extractGenericRefs(raw, namespace)...) res.References = append(res.References, extractTypedRefs(raw, kind, namespace)...) res.WorkloadMeta = extractWorkloadMeta(raw, kind) res.References = dedupeRefs(res.References) return res, nil } func resourceID(namespace, kind, name string) string { if namespace == "" { return fmt.Sprintf("%s/%s", kind, name) } return fmt.Sprintf("%s/%s/%s", namespace, kind, name) } func ResourceID(namespace, kind, name string) string { return resourceID(namespace, kind, name) } func normalizeMap(v any) any { switch t := v.(type) { case map[string]any: m := map[string]any{} for k, val := range t { m[k] = normalizeMap(val) } return m case map[any]any: m := map[string]any{} for k, val := range t { m[fmt.Sprint(k)] = normalizeMap(val) } return m case []any: out := make([]any, 0, len(t)) for _, item := range t { out = append(out, normalizeMap(item)) } return out default: return t } } func extractStringMap(v any) map[string]string { src, ok := v.(map[string]any) if !ok { return nil } out := make(map[string]string) for k, val := range src { if s, ok := val.(string); ok { out[k] = s } } if len(out) == 0 { return nil } return out } func extractOwnerRefs(meta map[string]any) []model.OwnerReference { owners, _ := meta["ownerReferences"].([]any) out := make([]model.OwnerReference, 0, len(owners)) for _, entry := range owners { m, ok := entry.(map[string]any) if !ok { continue } kind, _ := m["kind"].(string) name, _ := m["name"].(string) if kind == "" || name == "" { continue } out = append(out, model.OwnerReference{Kind: kind, Name: name}) } return out } func extractSecretKeyNames(raw map[string]any) []string { keys := map[string]struct{}{} if data, ok := raw["data"].(map[string]any); ok { for k := range data { keys[k] = struct{}{} } } if data, ok := raw["stringData"].(map[string]any); ok { for k := range data { keys[k] = struct{}{} } } out := make([]string, 0, len(keys)) for k := range keys { out = append(out, k) } sort.Strings(out) return out } func redactSecretValues(raw map[string]any) { for _, key := range []string{"data", "stringData"} { data, ok := raw[key].(map[string]any) if !ok { continue } for k := range data { data[k] = "" } } } func extractWorkloadMeta(raw map[string]any, kind string) *model.WorkloadMetadata { meta := &model.WorkloadMetadata{} switch kind { case "Deployment", "StatefulSet", "DaemonSet": spec, _ := raw["spec"].(map[string]any) tpl, _ := spec["template"].(map[string]any) tplMeta, _ := tpl["metadata"].(map[string]any) meta.PodTemplateLabels = extractStringMap(tplMeta["labels"]) case "Service": spec, _ := raw["spec"].(map[string]any) meta.ServiceSelectors = extractStringMap(spec["selector"]) } if len(meta.PodTemplateLabels) == 0 && len(meta.ServiceSelectors) == 0 { return nil } return meta } func extractTypedRefs(raw map[string]any, kind, defaultNamespace string) []model.ResourceReference { refs := make([]model.ResourceReference, 0) spec, _ := raw["spec"].(map[string]any) if spec != nil { refs = append(refs, extractRefsFromPodSpec(raw, defaultNamespace)...) if kind == "Ingress" { refs = append(refs, extractIngressRefs(spec, defaultNamespace)...) } if kind == "HorizontalPodAutoscaler" { if target, ok := spec["scaleTargetRef"].(map[string]any); ok { tKind, _ := target["kind"].(string) tName, _ := target["name"].(string) if tKind != "" && tName != "" { refs = append(refs, model.ResourceReference{Kind: tKind, Name: tName, Namespace: defaultNamespace, Relation: "scales"}) } } } } return refs } func extractIngressRefs(spec map[string]any, namespace string) []model.ResourceReference { refs := []model.ResourceReference{} if backend, ok := spec["defaultBackend"].(map[string]any); ok { if svc := serviceFromBackend(backend); svc != "" { refs = append(refs, model.ResourceReference{Kind: "Service", Name: svc, Namespace: namespace, Relation: "routesTo"}) } } rules, _ := spec["rules"].([]any) for _, r := range rules { rule, ok := r.(map[string]any) if !ok { continue } http, _ := rule["http"].(map[string]any) paths, _ := http["paths"].([]any) for _, p := range paths { path, ok := p.(map[string]any) if !ok { continue } backend, _ := path["backend"].(map[string]any) if svc := serviceFromBackend(backend); svc != "" { refs = append(refs, model.ResourceReference{Kind: "Service", Name: svc, Namespace: namespace, Relation: "routesTo"}) } } } return refs } func serviceFromBackend(backend map[string]any) string { svc, _ := backend["service"].(map[string]any) name, _ := svc["name"].(string) return name } func extractRefsFromPodSpec(raw map[string]any, namespace string) []model.ResourceReference { podSpec := findPodSpec(raw) if podSpec == nil { return nil } refs := []model.ResourceReference{} if vols, ok := podSpec["volumes"].([]any); ok { for _, v := range vols { vol, ok := v.(map[string]any) if !ok { continue } if cm, ok := vol["configMap"].(map[string]any); ok { if name, _ := cm["name"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "ConfigMap", Name: name, Namespace: namespace, Relation: "mounts"}) } } if sec, ok := vol["secret"].(map[string]any); ok { if name, _ := sec["secretName"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "Secret", Name: name, Namespace: namespace, Relation: "mounts"}) } } if pvc, ok := vol["persistentVolumeClaim"].(map[string]any); ok { if name, _ := pvc["claimName"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "PersistentVolumeClaim", Name: name, Namespace: namespace, Relation: "mounts"}) } } } } for _, containerType := range []string{"containers", "initContainers"} { containers, _ := podSpec[containerType].([]any) for _, c := range containers { container, ok := c.(map[string]any) if !ok { continue } env, _ := container["env"].([]any) for _, e := range env { envVar, ok := e.(map[string]any) if !ok { continue } valueFrom, _ := envVar["valueFrom"].(map[string]any) if cmRef, ok := valueFrom["configMapKeyRef"].(map[string]any); ok { if name, _ := cmRef["name"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "ConfigMap", Name: name, Namespace: namespace, Relation: "references"}) } } if secRef, ok := valueFrom["secretKeyRef"].(map[string]any); ok { if name, _ := secRef["name"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "Secret", Name: name, Namespace: namespace, Relation: "references"}) } } } envFrom, _ := container["envFrom"].([]any) for _, ef := range envFrom { entry, ok := ef.(map[string]any) if !ok { continue } if cmRef, ok := entry["configMapRef"].(map[string]any); ok { if name, _ := cmRef["name"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "ConfigMap", Name: name, Namespace: namespace, Relation: "references"}) } } if secRef, ok := entry["secretRef"].(map[string]any); ok { if name, _ := secRef["name"].(string); name != "" { refs = append(refs, model.ResourceReference{Kind: "Secret", Name: name, Namespace: namespace, Relation: "references"}) } } } } } return refs } func findPodSpec(raw map[string]any) map[string]any { spec, _ := raw["spec"].(map[string]any) if spec == nil { return nil } if template, ok := spec["template"].(map[string]any); ok { if tplSpec, ok := template["spec"].(map[string]any); ok { return tplSpec } } if containers, ok := spec["containers"]; ok { if _, valid := containers.([]any); valid { return spec } } if jobTemplate, ok := spec["jobTemplate"].(map[string]any); ok { if jtSpec, ok := jobTemplate["spec"].(map[string]any); ok { if template, ok := jtSpec["template"].(map[string]any); ok { if tplSpec, ok := template["spec"].(map[string]any); ok { return tplSpec } } } } return nil } func extractGenericRefs(raw map[string]any, namespace string) []model.ResourceReference { refs := []model.ResourceReference{} walkMap(raw, func(k string, v any) { if strings.HasSuffix(k, "Name") { if name, ok := v.(string); ok && name != "" { kind := guessKindFromField(k) if kind != "" { refs = append(refs, model.ResourceReference{Kind: kind, Name: name, Namespace: namespace, Relation: "references"}) } } } }) return refs } func walkMap(v any, fn func(string, any)) { switch m := v.(type) { case map[string]any: for k, value := range m { fn(k, value) walkMap(value, fn) } case []any: for _, item := range m { walkMap(item, fn) } } } func guessKindFromField(field string) string { lower := strings.ToLower(field) switch { case strings.Contains(lower, "secret"): return "Secret" case strings.Contains(lower, "configmap"): return "ConfigMap" case strings.Contains(lower, "service"): return "Service" case strings.Contains(lower, "claim"): return "PersistentVolumeClaim" default: return "" } } func deepCopy(src map[string]any) map[string]any { out := make(map[string]any, len(src)) for k, v := range src { switch typed := v.(type) { case map[string]any: out[k] = deepCopy(typed) case []any: copied := make([]any, len(typed)) for i := range typed { if m, ok := typed[i].(map[string]any); ok { copied[i] = deepCopy(m) } else { copied[i] = typed[i] } } out[k] = copied default: out[k] = v } } return out } func dedupeRefs(refs []model.ResourceReference) []model.ResourceReference { seen := map[string]struct{}{} out := make([]model.ResourceReference, 0, len(refs)) for _, ref := range refs { key := fmt.Sprintf("%s|%s|%s|%s", ref.Kind, ref.Name, ref.Namespace, ref.Relation) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, ref) } return out }