Module: Kamal::Lint::Loader

Defined in:
lib/kamal/lint/loader.rb

Class Method Summary collapse

Class Method Details

.build_line_index(text) ⇒ Object

Build a mapping from dot-path (“env.secret”) to source line numbers. Walks the Psych AST.



138
139
140
141
142
143
144
145
146
147
# File 'lib/kamal/lint/loader.rb', line 138

def build_line_index(text)
  index = {}
  stream = Psych.parse_stream(text)
  stream.children.each do |doc|
    walk_node(doc.root, [], index)
  end
  index
rescue Psych::SyntaxError
  index
end

.deep_merge(base, override) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/kamal/lint/loader.rb', line 122

def deep_merge(base, override)
  return override unless base.is_a?(Hash) && override.is_a?(Hash)

  result = base.dup
  override.each do |k, v|
    result[k] = if result[k].is_a?(Hash) && v.is_a?(Hash)
      deep_merge(result[k], v)
    else
      v
    end
  end
  result
end

.load(config_file:, destination: nil, kamal_version: nil) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/kamal/lint/loader.rb', line 51

def load(config_file:, destination: nil, kamal_version: nil)
  raise ConfigNotFoundError, "Config file not found: #{config_file}" unless File.exist?(config_file)

  working_dir = Pathname.new(config_file).realpath.dirname.dirname.to_s
  # If config_file is at config/deploy.yml inside a project, working_dir = project root.
  # If the user pointed somewhere else, fall back to its parent dir's parent.

  base_text = File.read(config_file)
  base_parsed = safe_parse_yaml(base_text)
  source_lines = base_text.lines

  override_parsed = nil
  if destination
    override_path = File.join(File.dirname(config_file), "deploy.#{destination}.yml")
    if File.exist?(override_path)
      override_parsed = safe_parse_yaml(File.read(override_path))
      source_lines = File.read(override_path).lines
    end
  end

  merged = override_parsed ? deep_merge(base_parsed, override_parsed) : base_parsed

  line_index = build_line_index(base_text)
  secrets_path = File.join(working_dir, ".kamal", "secrets")
  gitignore_path = File.join(working_dir, ".gitignore")
  secrets_keys = SecretsFile.read_keys(secrets_path)

  loaded = true
  load_error = nil
  begin
    # Run Kamal's own loader for parse-level validation. We don't use the
    # returned object — we keep working off the parsed Hash so we can
    # report line numbers — but we surface Kamal's own errors as findings.
    require "kamal"
    Dir.chdir(working_dir) do
      Kamal::Configuration.create_from(
        config_file: Pathname.new(config_file),
        destination: destination,
        version: "kamal-lint"
      )
    end
  rescue => e
    loaded = false
    load_error = e
  end

  Context.new(
    config_file: config_file,
    destination: destination,
    working_dir: working_dir,
    parsed: merged || {},
    base_parsed: base_parsed || {},
    override_parsed: override_parsed,
    source_lines: source_lines,
    line_index: line_index,
    secrets: secrets_keys,
    secrets_path: secrets_path,
    gitignore_path: gitignore_path,
    kamal_version: kamal_version || KamalVersion.detect,
    kamal_loaded: loaded,
    kamal_load_error: load_error
  )
end

.safe_parse_yaml(text) ⇒ Object



115
116
117
118
119
120
# File 'lib/kamal/lint/loader.rb', line 115

def safe_parse_yaml(text)
  result = YAML.safe_load(text, aliases: true, permitted_classes: [ Symbol ])
  result.is_a?(Hash) ? result : {}
rescue Psych::SyntaxError
  {}
end

.walk_node(node, path, index) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/kamal/lint/loader.rb', line 149

def walk_node(node, path, index)
  case node
  when Psych::Nodes::Mapping
    node.children.each_slice(2) do |key_node, value_node|
      next unless key_node && value_node

      key = key_node.value
      new_path = path + [ key ]
      index[new_path.join(".")] ||= key_node.start_line + 1
      walk_node(value_node, new_path, index)
    end
  when Psych::Nodes::Sequence
    node.children.each_with_index do |child, idx|
      new_path = path + [ idx.to_s ]
      # Index the position itself so checks can find sequence elements
      # by ordinal (e.g. "env.secret.0").
      index[new_path.join(".")] ||= child.start_line + 1
      walk_node(child, new_path, index)
    end
  when Psych::Nodes::Scalar
    # Scalars at non-root locations need no further indexing; their
    # parent already wrote the line for them.
  end
end