Class: Bolt::PAL::YamlPlan::Step

Inherits:
Object
  • Object
show all
Defined in:
lib/bolt/pal/yaml_plan/step.rb

Constant Summary collapse

COMMON_STEP_KEYS =
%w[name description target].freeze
STEP_KEYS =
{
  'command' => {
    'allowed_keys' => Set['command'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set['target']
  },
  'script' => {
    'allowed_keys' => Set['script', 'parameters', 'arguments'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set['target']
  },
  'task' => {
    'allowed_keys' => Set['task', 'parameters'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set['target']
  },
  'plan' => {
    'allowed_keys' => Set['plan', 'parameters'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set.new
  },
  'source' => {
    'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set['target', 'source', 'destination']
  },
  'destination' => {
    'allowed_keys' => Set['source', 'destination'].merge(COMMON_STEP_KEYS),
    'required_keys' => Set['target', 'source', 'destination']
  },
  'eval' => {
    'allowed_keys' => Set['eval', 'name', 'description'],
    'required_keys' => Set.new
  }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(step_body, step_number) ⇒ Step

Returns a new instance of Step.



43
44
45
46
47
48
49
50
51
52
# File 'lib/bolt/pal/yaml_plan/step.rb', line 43

def initialize(step_body, step_number)
  @body = step_body
  @name = @body['name']
  # For error messages
  @step_number = step_number
  validate_step

  @type = STEP_KEYS.keys.find { |key| @body.key?(key) }
  @target = @body['target']
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



9
10
11
# File 'lib/bolt/pal/yaml_plan/step.rb', line 9

def body
  @body
end

#nameObject (readonly)

Returns the value of attribute name.



9
10
11
# File 'lib/bolt/pal/yaml_plan/step.rb', line 9

def name
  @name
end

#targetObject (readonly)

Returns the value of attribute target.



9
10
11
# File 'lib/bolt/pal/yaml_plan/step.rb', line 9

def target
  @target
end

#typeObject (readonly)

Returns the value of attribute type.



9
10
11
# File 'lib/bolt/pal/yaml_plan/step.rb', line 9

def type
  @type
end

Instance Method Details

#parse_code_string(code, quote = false) ⇒ Object

Parses the an evaluable string, optionally quote it before parsing



192
193
194
195
196
197
198
199
# File 'lib/bolt/pal/yaml_plan/step.rb', line 192

def parse_code_string(code, quote = false)
  if quote
    quoted = Puppet::Pops::Parser::EvaluatingParser.quote(code)
    Puppet::Pops::Parser::EvaluatingParser.new.parse_string(quoted)
  else
    Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code)
  end
end

#step_err_msg(message) ⇒ Object



183
184
185
186
187
188
189
# File 'lib/bolt/pal/yaml_plan/step.rb', line 183

def step_err_msg(message)
  if @name
    "Parse error in step number #{@step_number} with name #{@name.inspect}: \n #{message}"
  else
    "Parse error in step number #{@step_number}: \n #{message}"
  end
end

#transpile(plan_path) ⇒ Object



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
# File 'lib/bolt/pal/yaml_plan/step.rb', line 54

def transpile(plan_path)
  result = String.new("  ")
  result << "$#{@name} = " if @name

  description = body.fetch('description', nil)
  parameters = body.fetch('parameters', {})
  if @type == 'script' && body.key?('arguments')
    parameters['arguments'] = body['arguments']
  end

  case @type
  when 'command', 'task', 'script', 'plan'
    result << "run_#{@type}(#{Bolt::Util.to_code(body[@type])}"
    result << ", #{Bolt::Util.to_code(@target)}" if @target
    result << ", #{Bolt::Util.to_code(description)}" if description && type != 'plan'
    result << ", #{Bolt::Util.to_code(parameters)}" unless parameters.empty?
    result << ")"
  when 'source'
    result << "upload_file(#{Bolt::Util.to_code(body['source'])}, #{Bolt::Util.to_code(body['destination'])}"
    result << ", #{Bolt::Util.to_code(@target)}" if @target
    result << ", #{Bolt::Util.to_code(description)}" if description
    result << ")"
  when 'eval'
    # We have to do a little extra parsing here, since we only need
    # with() for eval blocks
    code = Bolt::Util.to_code(body['eval'])
    if @name && code.lines.count > 1
      # A little indented niceness
      indented = code.gsub(/\n/, "\n    ").chomp("  ")
      result << "with() || {\n    #{indented}}"
    else
      result << code
    end
  else
    # We should never get here
    raise Bolt::YamlTranspiler::ConvertError.new("Can't convert unsupported step type #{@name}", plan_path)
  end
  result << "\n"
  result
end

#validate_puppet_code(step_key, value) ⇒ Object

Recursively ensure all puppet code can be parsed



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/bolt/pal/yaml_plan/step.rb', line 156

def validate_puppet_code(step_key, value)
  case value
  when Array
    value.map { |element| validate_puppet_code(step_key, element) }
  when Hash
    value.each_with_object({}) do |(k, v), o|
      key = k.is_a?(Bolt::PAL::YamlPlan::EvaluableString) ? k.value : k
      o[key] = validate_puppet_code(key, v)
    end
    # CodeLiterals can be parsed directly
  when Bolt::PAL::YamlPlan::CodeLiteral
    parse_code_string(value.value)
    # BareString is parsed directly if it starts with '$'
  when Bolt::PAL::YamlPlan::BareString
    if value.value.start_with?('$')
      parse_code_string(value.value)
    else
      parse_code_string(value.value, true)
    end
  when Bolt::PAL::YamlPlan::EvaluableString
    # Must quote parsed strings to evaluate them
    parse_code_string(value.value, true)
  end
rescue Puppet::Error => e
  raise Bolt::Error.new("Error parsing #{step_key.inspect}: #{e.basic_message}", "bolt/invalid-plan")
end

#validate_stepObject



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/bolt/pal/yaml_plan/step.rb', line 95

def validate_step
  validate_step_keys

  begin
    @body.each { |k, v| validate_puppet_code(k, v) }
  rescue Bolt::Error => e
    err = step_err_msg(e.msg)
    raise Bolt::Error.new(err, 'bolt/invalid-plan')
  end

  unless body.fetch('parameters', {}).is_a?(Hash)
    msg = "Parameters key must be a hash"
    raise Bolt::Error.new(step_err_msg(msg), "bolt/invalid-plan")
  end

  if @name
    unless @name.is_a?(String) && @name.match?(Bolt::PAL::YamlPlan::VAR_NAME_PATTERN)
      error_message = "Invalid step name: #{@name.inspect}"
      err = step_err_msg(error_message)
      raise Bolt::Error.new(err, "bolt/invalid-plan")
    end
  end
end

#validate_step_keysObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/bolt/pal/yaml_plan/step.rb', line 119

def validate_step_keys
  step_keys = @body.keys.to_set
  action = step_keys.intersection(STEP_KEYS.keys.to_set).to_a
  unless action.count == 1
    if action.count > 1
      # Upload step is special in that it is identified by both `source` and `destination`
      unless action.to_set == Set['source', 'destination']
        error_message = "Multiple action keys detected: #{action.inspect}"
        err = step_err_msg(error_message)
        raise Bolt::Error.new(err, "bolt/invalid-plan")
      end
    else
      error_message = "No valid action detected"
      err = step_err_msg(error_message)
      raise Bolt::Error.new(err, "bolt/invalid-plan")
    end
  end

  # For validated step action, ensure only valid keys
  unless STEP_KEYS[action.first]['allowed_keys'].superset?(step_keys)
    illegal_keys = step_keys - STEP_KEYS[action.first]['allowed_keys']
    error_message = "The #{action.first.inspect} step does not support: #{illegal_keys.to_a.inspect} key(s)"
    err = step_err_msg(error_message)
    raise Bolt::Error.new(err, "bolt/invalid-plan")
  end

  # Ensure all required keys are present
  STEP_KEYS[action.first]['required_keys'].each do |k|
    next if step_keys.include?(k)
    missing_keys = STEP_KEYS[action.first]['required_keys'] - step_keys
    error_message = "The #{action.first.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
    err = step_err_msg(error_message)
    raise Bolt::Error.new(err, "bolt/invalid-plan")
  end
end