Module: Kdep::YamlDuplicateKeys

Defined in:
lib/kdep/yaml_duplicate_keys.rb

Overview

Finds keys declared twice in the same mapping, at any depth.

YAML resolves a repeated key by keeping the LAST one and discarding the earlier block ENTIRELY, in silence. Nothing in the pipeline warns: helm template, helm upgrade and kdep all receive the hash already collapsed by the parser. In repleadfy/llm-api2#1 a redis values.yaml declared metrics: twice; the first block -- the one redirecting the exporter registry to bitnamilegacy, after Bitnami closed the free repo -- was dropped at parse time, and the wrong image ran in production for 73 days, surviving only on a node's image cache.

This walks the Psych AST rather than calling Psych.load, because load has already applied last-one-wins and destroyed the evidence. The AST also gives real line numbers on both sides, works at any depth, and knows that key: value text inside a block scalar is data, not a key -- which a grep over the raw file cannot.

Defined Under Namespace

Classes: Duplicate

Class Method Summary collapse

Class Method Details

.in_dir(dir) ⇒ Object

Top-level only: everything kdep reads as input lives in the deploy dir root (app.yml, app..yml, secrets.yml, values.yaml), while nested dirs hold rendered output and templates.



43
44
45
# File 'lib/kdep/yaml_duplicate_keys.rb', line 43

def self.in_dir(dir)
  Dir.glob(File.join(dir, "*.{yml,yaml}")).sort.flat_map { |f| in_file(f) }
end

.in_file(path) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
# File 'lib/kdep/yaml_duplicate_keys.rb', line 28

def self.in_file(path)
  ast = Psych.parse_stream(File.read(path))
  scan(ast).map do |dup|
    Duplicate.new(File.basename(path), dup[:path], dup[:first], dup[:repeat])
  end
rescue Psych::SyntaxError
  # A file that doesn't parse is a different problem, reported elsewhere
  # with better context (Config#load, helm). Silently collapsing is the
  # failure this scanner exists for; don't turn it into a crash here.
  []
end

.scan(node, path = []) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/kdep/yaml_duplicate_keys.rb', line 47

def self.scan(node, path = [])
  found = []

  case node
  when Psych::Nodes::Mapping
    seen = {}
    node.children.each_slice(2) do |key, value|
      name = key.respond_to?(:value) ? key.value : key.to_s
      if seen.key?(name)
        found << {
          :path   => (path + [name]).join("."),
          :first  => seen[name],
          :repeat => key.start_line + 1,
        }
      end
      seen[name] = key.start_line + 1
      found.concat(scan(value, path + [name]))
    end
  when Psych::Nodes::Sequence, Psych::Nodes::Document, Psych::Nodes::Stream
    node.children.each { |child| found.concat(scan(child, path)) }
  end

  found
end