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

527 lines
16 KiB
Go

package languages
import (
"fmt"
"strconv"
"strings"
"gopkg.in/yaml.v3"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// extractKubernetesYAML scans a YAML stream for Kubernetes manifests
// (documents that have both `apiVersion:` and `kind:` keys) and emits
// KindResource / KindImage / KindConfigKey nodes plus the
// infrastructure edges (EdgeConfigures, EdgeMounts, EdgeExposes,
// EdgeDependsOn, EdgeUsesEnv).
//
// Returns true when at least one manifest was successfully extracted.
// The caller (YAMLExtractor.Extract) uses this signal to skip the
// generic top-level-keys fallback for K8s files — those keys would
// pollute search results without adding information beyond what the
// dedicated graph already captures. When the file has no manifests
// (or yaml parsing fails) the function returns false so the caller
// can run the generic path.
func extractKubernetesYAML(filePath, fileID string, src []byte, result *parser.ExtractionResult) bool {
dec := yaml.NewDecoder(strings.NewReader(string(src)))
emitted := false
for {
var doc yaml.Node
if err := dec.Decode(&doc); err != nil {
break
}
root := documentMapping(&doc)
if root == nil {
continue
}
apiVersion := scalarOf(mappingGet(root, "apiVersion"))
kind := scalarOf(mappingGet(root, "kind"))
if apiVersion == "" || kind == "" {
continue
}
extractK8sDoc(filePath, fileID, root, apiVersion, kind, result)
emitted = true
}
return emitted
}
// extractK8sDoc emits the graph fragment for a single K8s document.
func extractK8sDoc(filePath, fileID string, root *yaml.Node, apiVersion, kind string, result *parser.ExtractionResult) {
meta := mappingGet(root, "metadata")
name := scalarOf(mappingGet(meta, "name"))
namespace := scalarOf(mappingGet(meta, "namespace"))
if namespace == "" {
namespace = "_default"
}
if name == "" {
// Some manifests (e.g. List, Kustomization-as-resource)
// genuinely lack a metadata.name. Synthesise a stable
// fallback so the node ID stays unique.
name = fmt.Sprintf("__noname_%d", root.Line)
}
resID := k8sResourceID(kind, namespace, name)
resLine := root.Line
if resLine <= 0 {
resLine = 1
}
resMeta := map[string]any{
"api_version": apiVersion,
"k8s_kind": kind,
"namespace": namespace,
}
if labels := mappingGet(meta, "labels"); labels != nil && labels.Kind == yaml.MappingNode {
labelMap := make(map[string]string, len(labels.Content)/2)
for i := 0; i+1 < len(labels.Content); i += 2 {
k := labels.Content[i].Value
v := labels.Content[i+1].Value
if k != "" {
labelMap[k] = v
}
}
if len(labelMap) > 0 {
resMeta["labels"] = labelMap
}
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: resID, Kind: graph.KindResource, Name: name,
QualName: kind + "/" + name,
FilePath: filePath, StartLine: resLine, EndLine: resLine,
Language: "yaml",
Meta: resMeta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: resID, Kind: graph.EdgeDefines,
FilePath: filePath, Line: resLine,
})
switch kind {
case "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet", "Job", "Pod":
extractK8sPodSpec(filePath, resID, namespace, podSpecOf(root, kind), result)
case "CronJob":
// CronJob: spec.jobTemplate.spec.template.spec
spec := mappingGet(root, "spec")
jt := mappingGet(spec, "jobTemplate")
jtSpec := mappingGet(jt, "spec")
template := mappingGet(jtSpec, "template")
extractK8sPodSpec(filePath, resID, namespace, mappingGet(template, "spec"), result)
case "Service":
extractK8sServicePorts(filePath, resID, mappingGet(root, "spec"), result)
case "Ingress":
extractK8sIngressBackends(filePath, resID, namespace, mappingGet(root, "spec"), result)
case "ConfigMap":
extractK8sConfigMapData(filePath, resID, name, mappingGet(root, "data"), "k8s_cm", result)
case "Secret":
extractK8sConfigMapData(filePath, resID, name, mappingGet(root, "data"), "k8s_secret", result)
extractK8sConfigMapData(filePath, resID, name, mappingGet(root, "stringData"), "k8s_secret", result)
}
}
// podSpecOf returns the PodSpec node for a workload manifest. For Pod
// the spec lives at `.spec`; for the rest it's at `.spec.template.spec`.
func podSpecOf(root *yaml.Node, kind string) *yaml.Node {
spec := mappingGet(root, "spec")
if spec == nil {
return nil
}
if kind == "Pod" {
return spec
}
template := mappingGet(spec, "template")
return mappingGet(template, "spec")
}
// extractK8sPodSpec walks containers and volumes of a PodSpec.
// resID is the workload Resource node ID. namespace is propagated to
// referenced ConfigMap/Secret/PVC names so we can build cross-resource
// IDs.
func extractK8sPodSpec(filePath, resID, namespace string, podSpec *yaml.Node, result *parser.ExtractionResult) {
if podSpec == nil {
return
}
containers := append(sequenceItems(mappingGet(podSpec, "containers")),
sequenceItems(mappingGet(podSpec, "initContainers"))...)
volumes := mappingGet(podSpec, "volumes")
for _, c := range containers {
extractK8sContainer(filePath, resID, namespace, c, result)
}
if volumes != nil {
for _, v := range sequenceItems(volumes) {
extractK8sVolume(filePath, resID, namespace, v, result)
}
}
}
func extractK8sContainer(filePath, resID, namespace string, container *yaml.Node, result *parser.ExtractionResult) {
if container == nil {
return
}
line := container.Line
if line <= 0 {
line = 1
}
// container.image — link Resource → Image.
if image := scalarOf(mappingGet(container, "image")); image != "" {
imageID := imageNodeID(image)
ref, tag := splitImageRef(image)
result.Nodes = append(result.Nodes, &graph.Node{
ID: imageID, Kind: graph.KindImage, Name: image,
FilePath: filePath, StartLine: line, EndLine: line,
Language: "yaml",
Meta: map[string]any{
"role": "base",
"ref": ref,
"tag": tag,
},
})
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: imageID, Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
})
}
// container.env[] — direct env vars.
for _, env := range sequenceItems(mappingGet(container, "env")) {
envName := scalarOf(mappingGet(env, "name"))
if envName == "" {
continue
}
envLine := env.Line
if envLine <= 0 {
envLine = line
}
keyID := configKeyEnvID(envName)
emitConfigKeyNode(result, keyID, envName, "env", "k8s",
filePath, envLine)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: keyID, Kind: graph.EdgeUsesEnv,
FilePath: filePath, Line: envLine,
Meta: map[string]any{"scope": "runtime"},
})
// valueFrom: configMapKeyRef / secretKeyRef → EdgeConfigures
// to the source resource AND a typed config_key node for
// the source key.
if vf := mappingGet(env, "valueFrom"); vf != nil {
if cmRef := mappingGet(vf, "configMapKeyRef"); cmRef != nil {
cmName := scalarOf(mappingGet(cmRef, "name"))
cmKey := scalarOf(mappingGet(cmRef, "key"))
if cmName != "" {
target := k8sResourceID("ConfigMap", namespace, cmName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeConfigures,
FilePath: filePath, Line: envLine,
Meta: map[string]any{
"via": "configMapKeyRef",
"key": cmKey,
"into": envName,
},
})
}
}
if secRef := mappingGet(vf, "secretKeyRef"); secRef != nil {
secName := scalarOf(mappingGet(secRef, "name"))
secKey := scalarOf(mappingGet(secRef, "key"))
if secName != "" {
target := k8sResourceID("Secret", namespace, secName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeConfigures,
FilePath: filePath, Line: envLine,
Meta: map[string]any{
"via": "secretKeyRef",
"key": secKey,
"into": envName,
},
})
}
}
}
}
// container.envFrom[] — bulk env from ConfigMap or Secret.
for _, envFrom := range sequenceItems(mappingGet(container, "envFrom")) {
envLine := envFrom.Line
if envLine <= 0 {
envLine = line
}
if cmRef := mappingGet(envFrom, "configMapRef"); cmRef != nil {
if cmName := scalarOf(mappingGet(cmRef, "name")); cmName != "" {
target := k8sResourceID("ConfigMap", namespace, cmName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeConfigures,
FilePath: filePath, Line: envLine,
Meta: map[string]any{"via": "envFrom.configMapRef"},
})
}
}
if secRef := mappingGet(envFrom, "secretRef"); secRef != nil {
if secName := scalarOf(mappingGet(secRef, "name")); secName != "" {
target := k8sResourceID("Secret", namespace, secName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeConfigures,
FilePath: filePath, Line: envLine,
Meta: map[string]any{"via": "envFrom.secretRef"},
})
}
}
}
// container.ports[] — EdgeExposes.
for _, p := range sequenceItems(mappingGet(container, "ports")) {
port := mappingScalarInt(mappingGet(p, "containerPort"))
if port == 0 {
continue
}
proto := strings.ToLower(scalarOf(mappingGet(p, "protocol")))
if proto == "" {
proto = "tcp"
}
pLine := p.Line
if pLine <= 0 {
pLine = line
}
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: portTargetID(proto, port),
Kind: graph.EdgeExposes,
FilePath: filePath, Line: pLine,
Meta: map[string]any{"proto": proto, "port": port},
})
}
}
// extractK8sVolume walks a single PodSpec.spec.volumes[] entry and
// emits EdgeMounts from the Resource to the volume source's
// referenced ConfigMap / Secret / PVC.
func extractK8sVolume(filePath, resID, namespace string, vol *yaml.Node, result *parser.ExtractionResult) {
if vol == nil {
return
}
line := vol.Line
if line <= 0 {
line = 1
}
if cm := mappingGet(vol, "configMap"); cm != nil {
if cmName := scalarOf(mappingGet(cm, "name")); cmName != "" {
target := k8sResourceID("ConfigMap", namespace, cmName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeMounts,
FilePath: filePath, Line: line,
Meta: map[string]any{"via": "volume.configMap"},
})
}
}
if sec := mappingGet(vol, "secret"); sec != nil {
if secName := scalarOf(mappingGet(sec, "secretName")); secName != "" {
target := k8sResourceID("Secret", namespace, secName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeMounts,
FilePath: filePath, Line: line,
Meta: map[string]any{"via": "volume.secret"},
})
}
}
if pvc := mappingGet(vol, "persistentVolumeClaim"); pvc != nil {
if pvcName := scalarOf(mappingGet(pvc, "claimName")); pvcName != "" {
target := k8sResourceID("PersistentVolumeClaim", namespace, pvcName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeMounts,
FilePath: filePath, Line: line,
Meta: map[string]any{"via": "volume.pvc"},
})
}
}
}
func extractK8sServicePorts(filePath, resID string, spec *yaml.Node, result *parser.ExtractionResult) {
if spec == nil {
return
}
for _, p := range sequenceItems(mappingGet(spec, "ports")) {
port := mappingScalarInt(mappingGet(p, "port"))
if port == 0 {
continue
}
proto := strings.ToLower(scalarOf(mappingGet(p, "protocol")))
if proto == "" {
proto = "tcp"
}
pLine := p.Line
if pLine <= 0 {
pLine = 1
}
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: portTargetID(proto, port),
Kind: graph.EdgeExposes,
FilePath: filePath, Line: pLine,
Meta: map[string]any{"proto": proto, "port": port},
})
}
}
// extractK8sIngressBackends links an Ingress to each backing Service
// referenced from its rules. Both v1 (.backend.service.name) and v1
// path entries are supported; older v1beta1 shapes
// (.backend.serviceName) are handled too.
func extractK8sIngressBackends(filePath, resID, namespace string, spec *yaml.Node, result *parser.ExtractionResult) {
if spec == nil {
return
}
emitBackend := func(backend *yaml.Node, line int) {
if backend == nil {
return
}
// v1: backend.service.name
if svc := mappingGet(backend, "service"); svc != nil {
if svcName := scalarOf(mappingGet(svc, "name")); svcName != "" {
target := k8sResourceID("Service", namespace, svcName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
Meta: map[string]any{"via": "ingress.service"},
})
}
}
// v1beta1: backend.serviceName
if svcName := scalarOf(mappingGet(backend, "serviceName")); svcName != "" {
target := k8sResourceID("Service", namespace, svcName)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: target, Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: line,
Meta: map[string]any{"via": "ingress.serviceName"},
})
}
}
// Default backend.
if def := mappingGet(spec, "defaultBackend"); def != nil {
emitBackend(def, def.Line)
}
for _, rule := range sequenceItems(mappingGet(spec, "rules")) {
http := mappingGet(rule, "http")
for _, p := range sequenceItems(mappingGet(http, "paths")) {
emitBackend(mappingGet(p, "backend"), p.Line)
}
}
}
// extractK8sConfigMapData materialises one KindConfigKey per data-map
// key. source ∈ "k8s_cm" | "k8s_secret".
func extractK8sConfigMapData(filePath, resID, resName string, data *yaml.Node, source string, result *parser.ExtractionResult) {
if data == nil || data.Kind != yaml.MappingNode {
return
}
for i := 0; i+1 < len(data.Content); i += 2 {
k := data.Content[i]
if k == nil {
continue
}
key := k.Value
if key == "" {
continue
}
line := k.Line
if line <= 0 {
line = 1
}
id := "cfg::" + source + "::" + resName + "::" + key
emitConfigKeyNode(result, id, key, source, "k8s",
filePath, line)
result.Edges = append(result.Edges, &graph.Edge{
From: resID, To: id, Kind: graph.EdgeDefines,
FilePath: filePath, Line: line,
})
}
}
func emitConfigKeyNode(result *parser.ExtractionResult, id, name, source, origin string, filePath string, line int) {
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindConfigKey, Name: name,
FilePath: filePath, StartLine: line, EndLine: line,
Language: "yaml",
Meta: map[string]any{
"source": source,
"origin": origin,
},
})
}
// k8sResourceID is the canonical ID for a Kubernetes Resource node.
// It is shared by every site that needs to refer to the same
// resource across documents — the workload pointing at a ConfigMap
// (envFrom) and the ConfigMap document itself produce the same
// target ID, so the cross-doc edge always lands on a single node.
func k8sResourceID(kind, namespace, name string) string {
if namespace == "" {
namespace = "_default"
}
return "k8s::" + kind + "::" + namespace + "::" + name
}
// ----- yaml.Node helpers ------------------------------------------------
// documentMapping returns the top-level MappingNode for a parsed YAML
// document. yaml.v3 wraps each Decode result in a DocumentNode whose
// single content child is the actual root.
func documentMapping(doc *yaml.Node) *yaml.Node {
if doc == nil {
return nil
}
if doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 {
doc = doc.Content[0]
}
if doc == nil || doc.Kind != yaml.MappingNode {
return nil
}
return doc
}
// mappingGet returns the value node for the given key inside a
// MappingNode. Returns nil when the parent isn't a mapping or the
// key is absent.
func mappingGet(parent *yaml.Node, key string) *yaml.Node {
if parent == nil || parent.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(parent.Content); i += 2 {
k := parent.Content[i]
if k != nil && k.Value == key {
return parent.Content[i+1]
}
}
return nil
}
// scalarOf returns the .Value of a scalar node, or "" when nil/non-scalar.
func scalarOf(n *yaml.Node) string {
if n == nil || n.Kind != yaml.ScalarNode {
return ""
}
return n.Value
}
// sequenceItems returns the items of a SequenceNode, or nil for non-sequences.
func sequenceItems(n *yaml.Node) []*yaml.Node {
if n == nil || n.Kind != yaml.SequenceNode {
return nil
}
out := make([]*yaml.Node, 0, len(n.Content))
for _, c := range n.Content {
if c != nil {
out = append(out, c)
}
}
return out
}
// mappingScalarInt parses a scalar yaml.Node as an integer. Returns 0
// when the node is missing, non-scalar, or non-numeric. Used for
// parsing port numbers, replica counts, etc.
func mappingScalarInt(n *yaml.Node) int {
if n == nil || n.Kind != yaml.ScalarNode {
return 0
}
if v, err := strconv.Atoi(strings.TrimSpace(n.Value)); err == nil {
return v
}
return 0
}