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 by walking the Psych AST.



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

def build_line_index(text)
  index = {}
  Psych.parse_stream(text).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

.derive_working_dir(config_file) ⇒ Object



79
80
81
# File 'lib/kamal/lint/loader.rb', line 79

def derive_working_dir(config_file)
  Pathname.new(config_file).realpath.dirname.dirname.to_s
end

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

Raises:



47
48
49
50
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
# File 'lib/kamal/lint/loader.rb', line 47

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

  working_dir = derive_working_dir(config_file)
  base_text, base_parsed = read_yaml(config_file)
  override_parsed = read_override(config_file, destination)
  kamal_loaded, kamal_load_error = try_kamal_load(config_file, destination, working_dir)

  base_secrets_path = File.join(working_dir, ".kamal", "secrets")
  destination_secrets_path = destination && File.join(working_dir, ".kamal", "secrets.#{destination}")
  secrets = SecretsFile.read_keys(base_secrets_path)
  secrets |= SecretsFile.read_keys(destination_secrets_path) if destination_secrets_path

  Context.new(
    config_file: config_file,
    destination: destination,
    working_dir: working_dir,
    parsed: override_parsed ? deep_merge(base_parsed, override_parsed) : base_parsed,
    base_parsed: base_parsed,
    override_parsed: override_parsed,
    line_index: build_line_index(base_text),
    secrets: secrets,
    secrets_path: base_secrets_path,
    destination_secrets_path: destination_secrets_path,
    gitignore_path: File.join(working_dir, ".gitignore"),
    kamal_version: kamal_version || KamalVersion.detect,
    kamal_loaded: kamal_loaded,
    kamal_load_error: kamal_load_error,
    include_kamal_errors: include_kamal_errors
  )
end

.read_override(config_file, destination) ⇒ Object



88
89
90
91
92
93
94
95
# File 'lib/kamal/lint/loader.rb', line 88

def read_override(config_file, destination)
  return nil unless destination

  override_path = File.join(File.dirname(config_file), "deploy.#{destination}.yml")
  return nil unless File.exist?(override_path)

  safe_parse_yaml(File.read(override_path))
end

.read_yaml(path) ⇒ Object



83
84
85
86
# File 'lib/kamal/lint/loader.rb', line 83

def read_yaml(path)
  text = File.read(path)
  [ text, safe_parse_yaml(text) ]
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

.try_kamal_load(config_file, destination, working_dir) ⇒ Object

Invoke Kamal's own loader so we can capture (and selectively surface) the errors it would raise at deploy time. The returned config object is discarded — checks operate on the parsed Hash so they retain source line numbers.



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/kamal/lint/loader.rb', line 101

def try_kamal_load(config_file, destination, working_dir)
  require "kamal"
  Dir.chdir(working_dir) do
    Kamal::Configuration.create_from(
      config_file: Pathname.new(config_file),
      destination: destination,
      version: "kamal-lint"
    )
  end
  [ true, nil ]
rescue => e
  [ false, e ]
end

.walk_node(node, path, index) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/kamal/lint/loader.rb', line 148

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

      new_path = path + [ key_node.value ]
      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[new_path.join(".")] ||= child.start_line + 1
      walk_node(child, new_path, index)
    end
  end
end