Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

223 lines
7.1 KiB
Go

package languages
import (
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// extractKustomizeYAML emits a KindKustomization node for the
// kustomization.yaml at filePath plus EdgeDependsOn / EdgeReferences
// edges for every base, component, resource, patch, and ConfigMap /
// Secret generator declared inside it.
//
// IDs:
//
// - The overlay node itself: `kustomize::<dir>` where dir is the
// directory containing the kustomization, normalised with `/`.
// - Bases / components: `kustomize::<resolved-dir>` (sibling
// overlays). The resolved-dir is computed by joining the
// overlay's dir with the base path; relative `..` segments are
// preserved by filepath.Clean.
// - Resource files: the resource path is left as-is and used as
// the EdgeReferences target; the same string ID is what the
// YAML / K8s extractor produces for the file node, so the edge
// lands on a real node when the file is indexed.
// - ConfigMap / Secret generators: synthetic K8s Resource IDs
// `k8s::ConfigMap::_default::<name>` etc., matching what the K8s
// manifest path would have produced if the same generator were
// materialised. EdgeDependsOn ties the overlay to each.
//
// Returns true when the file was a kustomization (i.e. parses as a
// mapping document); false otherwise so the caller can fall through.
func extractKustomizeYAML(filePath, fileID string, src []byte, result *parser.ExtractionResult) bool {
dec := yaml.NewDecoder(strings.NewReader(string(src)))
var doc yaml.Node
if err := dec.Decode(&doc); err != nil {
return false
}
root := documentMapping(&doc)
if root == nil {
return false
}
dir := filepath.ToSlash(filepath.Dir(filePath))
overlayID := kustomizationID(dir)
overlayMeta := map[string]any{
"dir": dir,
}
// Capture descriptive fields when present.
if v := scalarOf(mappingGet(root, "namespace")); v != "" {
overlayMeta["namespace"] = v
}
if v := scalarOf(mappingGet(root, "namePrefix")); v != "" {
overlayMeta["name_prefix"] = v
}
if v := scalarOf(mappingGet(root, "nameSuffix")); v != "" {
overlayMeta["name_suffix"] = v
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: overlayID, Kind: graph.KindKustomization, Name: dir,
FilePath: filePath, StartLine: 1, EndLine: 1,
Language: "yaml",
Meta: overlayMeta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: overlayID, Kind: graph.EdgeDefines,
FilePath: filePath, Line: 1,
})
// Bases / components: each entry is a path to another overlay
// directory or kustomization file.
for _, key := range []string{"bases", "components"} {
seq := mappingGet(root, key)
for _, item := range sequenceItems(seq) {
path := strings.TrimSpace(item.Value)
if path == "" {
continue
}
line := item.Line
if line <= 0 {
line = 1
}
target := kustomizationID(resolveKustomizePath(dir, path))
result.Edges = append(result.Edges, &graph.Edge{
From: overlayID, To: target, Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
Meta: map[string]any{"link": key},
})
}
}
// Resources: paths to YAML manifests (or sibling overlays — both
// shapes are accepted by Kustomize, so we emit EdgeReferences
// for either case and let the consumer follow whichever target
// resolves).
for _, item := range sequenceItems(mappingGet(root, "resources")) {
path := strings.TrimSpace(item.Value)
if path == "" {
continue
}
line := item.Line
if line <= 0 {
line = 1
}
resolved := resolveKustomizePath(dir, path)
// Try as a file first; if the path looks like a directory
// (no extension) treat it as another overlay.
ext := strings.ToLower(filepath.Ext(resolved))
switch ext {
case ".yaml", ".yml", ".json":
result.Edges = append(result.Edges, &graph.Edge{
From: overlayID, To: resolved, Kind: graph.EdgeReferences,
FilePath: filePath, Line: line,
Meta: map[string]any{"link": "resource_file"},
})
default:
result.Edges = append(result.Edges, &graph.Edge{
From: overlayID, To: kustomizationID(resolved),
Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
Meta: map[string]any{"link": "resource_overlay"},
})
}
}
// patches / patchesStrategicMerge / patchesJson6902 — each
// references a YAML file that overlays a sibling resource.
for _, key := range []string{"patches", "patchesStrategicMerge", "patchesJson6902"} {
for _, item := range sequenceItems(mappingGet(root, key)) {
// Each entry is either a scalar path or a mapping
// with a "path" field.
path := ""
line := item.Line
switch item.Kind {
case yaml.ScalarNode:
path = strings.TrimSpace(item.Value)
case yaml.MappingNode:
path = strings.TrimSpace(scalarOf(mappingGet(item, "path")))
}
if path == "" {
continue
}
if line <= 0 {
line = 1
}
resolved := resolveKustomizePath(dir, path)
result.Edges = append(result.Edges, &graph.Edge{
From: overlayID, To: resolved, Kind: graph.EdgeReferences,
FilePath: filePath, Line: line,
Meta: map[string]any{"link": key},
})
}
}
// configMapGenerator / secretGenerator — each entry materialises
// a synthetic Resource node so consumers can see "this overlay
// produces ConfigMap X."
emitGenerator := func(seq *yaml.Node, kind string) {
for _, item := range sequenceItems(seq) {
name := scalarOf(mappingGet(item, "name"))
if name == "" {
continue
}
ns := scalarOf(mappingGet(item, "namespace"))
if ns == "" {
ns = "_default"
}
line := item.Line
if line <= 0 {
line = 1
}
target := k8sResourceID(kind, ns, name)
result.Edges = append(result.Edges, &graph.Edge{
From: overlayID, To: target, Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
Meta: map[string]any{"link": "generator"},
})
}
}
emitGenerator(mappingGet(root, "configMapGenerator"), "ConfigMap")
emitGenerator(mappingGet(root, "secretGenerator"), "Secret")
return true
}
// kustomizationID is the canonical KindKustomization node ID. The
// directory string is normalised with forward slashes so cross-
// platform repos converge on a single ID.
func kustomizationID(dir string) string {
return "kustomize::" + filepath.ToSlash(dir)
}
// resolveKustomizePath joins a relative path declared inside a
// kustomization.yaml with the directory holding that kustomization.
// Absolute paths are returned untouched. The result is normalised
// with forward slashes.
func resolveKustomizePath(baseDir, p string) string {
p = strings.TrimSpace(p)
if p == "" {
return ""
}
if filepath.IsAbs(p) {
return filepath.ToSlash(filepath.Clean(p))
}
return filepath.ToSlash(filepath.Clean(filepath.Join(baseDir, p)))
}
// isKustomizationFile returns true when the basename matches one
// of the canonical kustomize.yaml / kustomization.yml / Kustomization
// names. The YAML extractor uses this to dispatch into the kustomize
// path before falling through to the generic top-level-keys walker.
func isKustomizationFile(filePath string) bool {
base := filepath.Base(filePath)
switch base {
case "kustomization.yaml", "kustomization.yml", "Kustomization":
return true
}
return false
}