bolt 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8afdc9d66e35557fd6dee9433d0c5dd62f9620fb111a7b56d3ba11e7a0b66e73
4
- data.tar.gz: d2ea3502f583cfa8cd136f8e04eaebac50364c822f892595eb0782c8e6c6f175
3
+ metadata.gz: 59ccedf25067dbbeb8769b4cb77178679c6b5a7a312dcecba80841af2d472ffe
4
+ data.tar.gz: 358e3ce1e0e4e3241eb9442fac18b80b9e36bec98b3e3d1dbbd039c5708d458b
5
5
  SHA512:
6
- metadata.gz: 3c15b22e1ab464ba249372b62161da08c69e31db4c55ff2a89b590fde7abccde114dc0559e1b3b32eae3fcae69ef09afd1f142fe89e1976d682247f4e1e16bdc
7
- data.tar.gz: d1b82c13cb0faf8b80e5b37bf6800a24a7837e4ed572690257163fcf95da6dcd2a14f25a0f23e7fd33c7e797683e00eda3f0f2933a319785ce92a40f5bdfcc01
6
+ metadata.gz: 42734205200d757dd20e82aeed34f61734b4688fd8e72de40b5f5341fdd1f452537de9db1277b585a9cf274abb0dad0ba20cc2a5822e4769d40c505ca42da4f5
7
+ data.tar.gz: 6c906fafb671ab787e79fbcdb3bfbd544b31eeb2ade0f9937be4c3ebe7472dc9d7a399ed5ae8e357d2582e93250371a15beadf08d93cd9a68762e85e91dc9088
@@ -3,7 +3,18 @@
3
3
  require 'fileutils'
4
4
  require 'bolt/task'
5
5
 
6
+ # Installs the puppet-agent package on targets if needed then collects facts, including any custom
7
+ # facts found in Bolt's modulepath.
8
+ #
9
+ # Agent detection will be skipped if the target includes the 'puppet-agent' feature, either as a
10
+ # property of its transport (PCP) or by explicitly setting it as a feature in Bolt's inventory.
11
+ #
12
+ # If no agent is detected on the target using the 'puppet_agent::version' task, it's installed
13
+ # using 'puppet_agent::install' and the puppet service is stopped/disabled using the 'service' task.
6
14
  Puppet::Functions.create_function(:apply_prep) do
15
+ # @param targets A pattern or array of patterns identifying a set of targets.
16
+ # @example Prepare targets by name.
17
+ # apply_prep('target1,target2')
7
18
  dispatch :apply_prep do
8
19
  param 'Boltlib::TargetSpec', :targets
9
20
  end
@@ -22,6 +33,12 @@ Puppet::Functions.create_function(:apply_prep) do
22
33
  results
23
34
  end
24
35
 
36
+ # Returns true if the target has the puppet-agent feature defined, either from inventory or transport.
37
+ def agent?(target, executor, inventory)
38
+ inventory.features(target).include?('puppet-agent') ||
39
+ executor.transport(target.protocol).provided_features.include?('puppet-agent')
40
+ end
41
+
25
42
  def apply_prep(target_spec)
26
43
  applicator = Puppet.lookup(:apply_executor) { nil }
27
44
  executor = Puppet.lookup(:bolt_executor) { nil }
@@ -38,20 +55,25 @@ Puppet::Functions.create_function(:apply_prep) do
38
55
 
39
56
  executor.log_action('install puppet and gather facts', targets) do
40
57
  executor.without_default_logging do
41
- # Ensure Puppet is installed
42
- versions = run_task(executor, targets, 'puppet_agent::version')
43
- need_install, installed = versions.partition { |r| r['version'].nil? }
44
- installed.each do |r|
45
- Puppet.info "Puppet Agent #{r['version']} installed on #{r.target.name}"
46
- end
58
+ # Skip targets that include the puppet-agent feature, as we know an agent will be available.
59
+ agent_targets, unknown_targets = targets.partition { |target| agent?(target, executor, inventory) }
60
+ agent_targets.each { |target| Puppet.debug "Puppet Agent feature declared for #{target.name}" }
61
+ unless unknown_targets.empty?
62
+ # Ensure Puppet is installed
63
+ versions = run_task(executor, unknown_targets, 'puppet_agent::version')
64
+ need_install, installed = versions.partition { |r| r['version'].nil? }
65
+ installed.each do |r|
66
+ Puppet.debug "Puppet Agent #{r['version']} installed on #{r.target.name}"
67
+ end
47
68
 
48
- unless need_install.empty?
49
- need_install_targets = need_install.map(&:target)
50
- run_task(executor, need_install_targets, 'puppet_agent::install')
69
+ unless need_install.empty?
70
+ need_install_targets = need_install.map(&:target)
71
+ run_task(executor, need_install_targets, 'puppet_agent::install')
51
72
 
52
- # Ensure the Puppet service is stopped after new install
53
- run_task(executor, need_install_targets, 'service', 'action' => 'stop', 'name' => 'puppet')
54
- run_task(executor, need_install_targets, 'service', 'action' => 'disable', 'name' => 'puppet')
73
+ # Ensure the Puppet service is stopped after new install
74
+ run_task(executor, need_install_targets, 'service', 'action' => 'stop', 'name' => 'puppet')
75
+ run_task(executor, need_install_targets, 'service', 'action' => 'disable', 'name' => 'puppet')
76
+ end
55
77
  end
56
78
  targets.each { |target| inventory.set_feature(target, 'puppet-agent') }
57
79
 
@@ -23,6 +23,10 @@ module Bolt
23
23
  cli << "--#{setting}" << dir
24
24
  end
25
25
  Puppet.settings.send(:clear_everything_for_tests)
26
+ # Override module locations, Bolt includes vendored modules in its internal modulepath.
27
+ Puppet.settings.override_default(:basemodulepath, '')
28
+ Puppet.settings.override_default(:vendormoduledir, '')
29
+
26
30
  Puppet.initialize_settings(cli)
27
31
  Puppet.settings[:hiera_config] = hiera_config
28
32
 
@@ -378,7 +378,7 @@ module Bolt
378
378
  executor.start_plan(plan_context)
379
379
  result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
380
380
 
381
- # If a non-bolt exeception bubbles up the plan won't get finished
381
+ # If a non-bolt exception bubbles up the plan won't get finished
382
382
  executor.finish_plan(result)
383
383
  outputter.print_plan_result(result)
384
384
  result.ok? ? 0 : 1
@@ -13,9 +13,9 @@ module Bolt
13
13
  class ValidationError < Bolt::Error
14
14
  attr_accessor :path
15
15
  def initialize(message, offending_group)
16
- super(msg, 'bolt.inventory/validation-error')
16
+ super(message, 'bolt.inventory/validation-error')
17
17
  @_message = message
18
- @path = offending_group ? [offending_group] : []
18
+ @path = [offending_group].compact
19
19
  end
20
20
 
21
21
  def details
@@ -27,7 +27,11 @@ module Bolt
27
27
  end
28
28
 
29
29
  def message
30
- "#{@_message} for group at #{path}"
30
+ if path.empty?
31
+ @_message
32
+ else
33
+ "#{@_message} for group at #{path}"
34
+ end
31
35
  end
32
36
  end
33
37
 
@@ -63,6 +67,8 @@ module Bolt
63
67
  @target_vars = target_vars
64
68
  @target_facts = target_facts
65
69
  @target_features = target_features
70
+
71
+ @groups.resolve_aliases(@groups.node_aliases)
66
72
  collect_groups
67
73
  end
68
74
 
@@ -194,27 +200,26 @@ module Bolt
194
200
  private :update_target
195
201
 
196
202
  # If target is a group name, expand it to the members of that group.
197
- # If a wildcard string, match against nodes in inventory (or error if none found).
198
- # Else return [target].
203
+ # Else match against nodes in inventory by name or alias.
204
+ # If a wildcard string, error if no matches are found.
205
+ # Else fall back to [target] if no matches are found.
199
206
  def resolve_name(target)
200
207
  if (group = @group_lookup[target])
201
208
  group.node_names
202
- elsif target.include?('*')
209
+ else
203
210
  # Try to wildcard match nodes in inventory
204
211
  # Ignore case because hostnames are generally case-insensitive
205
212
  regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
206
213
 
207
- nodes = []
208
- @groups.node_names.each do |node|
209
- if node =~ regexp
210
- nodes << node
211
- end
212
- end
214
+ nodes = @groups.node_names.select { |node| node =~ regexp }
215
+ nodes += @groups.node_aliases.select { |target_alias, _node| target_alias =~ regexp }.values
213
216
 
214
- raise(WildcardError, target) if nodes.empty?
215
- nodes
216
- else
217
- [target]
217
+ if nodes.empty?
218
+ raise(WildcardError, target) if target.include?('*')
219
+ [target]
220
+ else
221
+ nodes
222
+ end
218
223
  end
219
224
  end
220
225
  private :resolve_name
@@ -5,24 +5,20 @@ module Bolt
5
5
  # Group is a specific implementation of Inventory based on nested
6
6
  # structured data.
7
7
  class Group
8
- attr_accessor :name, :nodes, :groups, :config, :rest, :facts, :vars, :features
8
+ attr_accessor :name, :nodes, :aliases, :name_or_alias, :groups, :config, :rest, :facts, :vars, :features
9
+
10
+ # Regex used to validate group names and target aliases.
11
+ NAME_REGEX = /\A[a-z0-9_]+\Z/.freeze
9
12
 
10
13
  def initialize(data)
11
14
  @logger = Logging.logger[self]
12
15
 
13
- unless data.is_a?(Hash)
14
- raise ValidationError.new("Expected group to be a Hash, not #{data.class}", nil)
15
- end
16
+ raise ValidationError.new("Expected group to be a Hash, not #{data.class}", nil) unless data.is_a?(Hash)
17
+ raise ValidationError.new("Group does not have a name", nil) unless data.key?('name')
16
18
 
17
- if data.key?('name')
18
- if data['name'].is_a?(String)
19
- @name = data['name']
20
- else
21
- raise ValidationError.new("Group name must be a String, not #{data['name'].inspect}", nil)
22
- end
23
- else
24
- raise ValidationError.new("Group does not have a name", nil)
25
- end
19
+ @name = data['name']
20
+ raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
21
+ raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
26
22
 
27
23
  @vars = fetch_value(data, 'vars', Hash)
28
24
  @facts = fetch_value(data, 'facts', Hash)
@@ -33,25 +29,50 @@ module Bolt
33
29
  groups = fetch_value(data, 'groups', Array)
34
30
 
35
31
  @nodes = {}
36
- nodes.each do |node|
37
- node = { 'name' => node } if node.is_a? String
32
+ @aliases = {}
33
+ nodes.reject { |node| node.is_a?(String) }.each do |node|
38
34
  unless node.is_a?(Hash)
39
35
  raise ValidationError.new("Node entry must be a String or Hash, not #{node.class}", @name)
40
36
  end
41
- if @nodes.include? node['name']
37
+
38
+ if @nodes.include?(node['name'])
42
39
  @logger.warn("Ignoring duplicate node in #{@name}: #{node}")
43
- else
44
- @nodes[node['name']] = node
40
+ next
41
+ end
42
+
43
+ raise ValidationError.new("Node #{node} does not have a name", @name) unless node['name']
44
+ @nodes[node['name']] = node
45
+
46
+ next unless node.include?('alias')
47
+
48
+ aliases = node['alias']
49
+ aliases = [aliases] if aliases.is_a?(String)
50
+ unless aliases.is_a?(Array)
51
+ msg = "Alias entry on #{node['name']} must be a String or Array, not #{aliases.class}"
52
+ raise ValidationError.new(msg, @name)
53
+ end
54
+
55
+ aliases.each do |alia|
56
+ raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
57
+
58
+ if (found = @aliases[alia])
59
+ raise ValidationError.new(alias_conflict(alia, found, node['name']), @name)
60
+ end
61
+ @aliases[alia] = node['name']
45
62
  end
46
63
  end
47
64
 
65
+ # If node is a string, it can refer to either a node name or alias. Which can't be determined
66
+ # until all groups have been resolved, and requires a depth-first traversal to categorize them.
67
+ @name_or_alias = nodes.select { |node| node.is_a?(String) }
68
+
48
69
  @groups = groups.map { |g| Group.new(g) }
49
70
 
50
71
  # this allows arbitrary info for the top level
51
72
  @rest = data.reject { |k, _| %w[name nodes config groups vars facts features].include? k }
52
73
  end
53
74
 
54
- def fetch_value(data, key, type)
75
+ private def fetch_value(data, key, type)
55
76
  value = data.fetch(key, type.new)
56
77
  unless value.is_a?(type)
57
78
  raise ValidationError.new("Expected #{key} to be of type #{type}, not #{value.class}", @name)
@@ -59,38 +80,76 @@ module Bolt
59
80
  value
60
81
  end
61
82
 
62
- def validate(used_names = Set.new, node_names = Set.new, depth = 0)
63
- if used_names.include?(@name)
64
- raise ValidationError.new("Tried to redefine group #{@name}", @name)
65
- end
66
- raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ /\A[a-z0-9_]+\Z/
83
+ def resolve_aliases(aliases)
84
+ @name_or_alias.each do |name_or_alias|
85
+ # If an alias is found, insert the name into this group. Otherwise use the name as a new node.
86
+ node_name = aliases[name_or_alias] || name_or_alias
67
87
 
68
- if node_names.include?(@name)
69
- raise ValidationError.new("Group #{@name} conflicts with node of the same name", @name)
88
+ if @nodes.include?(node_name)
89
+ @logger.warn("Ignoring duplicate node in #{@name}: #{node_name}")
90
+ else
91
+ @nodes[node_name] = { 'name' => node_name }
92
+ end
70
93
  end
71
94
 
95
+ @groups.each { |g| g.resolve_aliases(aliases) }
96
+ end
97
+
98
+ private def alias_conflict(name, node1, node2)
99
+ "Alias #{name} refers to multiple targets: #{node1} and #{node2}"
100
+ end
101
+
102
+ private def group_alias_conflict(name)
103
+ "Group #{name} conflicts with alias of the same name"
104
+ end
105
+
106
+ private def group_node_conflict(name)
107
+ "Group #{name} conflicts with node of the same name"
108
+ end
109
+
110
+ private def alias_node_conflict(name)
111
+ "Node name #{name} conflicts with alias of the same name"
112
+ end
113
+
114
+ def validate(used_names = Set.new, node_names = Set.new, aliased = {}, depth = 0)
115
+ # Test if this group name conflicts with anything used before.
116
+ raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_names.include?(@name)
117
+ raise ValidationError.new(group_node_conflict(@name), @name) if node_names.include?(@name)
118
+ raise ValidationError.new(group_alias_conflict(@name), @name) if aliased.include?(@name)
119
+
72
120
  used_names << @name
73
121
 
74
- @nodes.each_value do |n|
122
+ # Collect node names and aliases into a list used to validate that subgroups don't conflict.
123
+ # Used names validate that previously used group names don't conflict with new node names/aliases.
124
+ @nodes.each_key do |n|
75
125
  # Require nodes to be parseable as a Target.
76
126
  begin
77
- Target.new(n['name'])
127
+ Target.new(n)
78
128
  rescue Addressable::URI::InvalidURIError => e
79
129
  @logger.debug(e)
80
- raise ValidationError.new("Invalid node name #{n['name']}", n['name'])
130
+ raise ValidationError.new("Invalid node name #{n}", @name)
81
131
  end
82
132
 
83
- raise ValidationError.new("Node #{n['name']} does not have a name", n['name']) unless n['name']
84
- if used_names.include?(n['name'])
85
- raise ValidationError.new("Group #{n['name']} conflicts with node of the same name", n['name'])
133
+ raise ValidationError.new(group_node_conflict(n), @name) if used_names.include?(n)
134
+ raise ValidationError.new(alias_node_conflict(n), @name) if aliased.include?(n)
135
+
136
+ node_names << n
137
+ end
138
+
139
+ @aliases.each do |n, target|
140
+ raise ValidationError.new(group_alias_conflict(n), @name) if used_names.include?(n)
141
+ raise ValidationError.new(alias_node_conflict(n), @name) if node_names.include?(n)
142
+
143
+ if aliased.include?(n) && aliased[n] != target
144
+ raise ValidationError.new(alias_conflict(n, target, aliased[n]), @name)
86
145
  end
87
146
 
88
- node_names << n['name']
147
+ aliased[n] = target
89
148
  end
90
149
 
91
150
  @groups.each do |g|
92
151
  begin
93
- g.validate(used_names, node_names, depth + 1)
152
+ g.validate(used_names, node_names, aliased, depth + 1)
94
153
  rescue ValidationError => e
95
154
  e.add_parent(@name)
96
155
  raise e
@@ -157,6 +216,13 @@ module Bolt
157
216
  end
158
217
  end
159
218
 
219
+ # Returns a mapping of aliases to nodes contained within the group, which includes subgroups.
220
+ def node_aliases
221
+ @groups.inject(@aliases) do |acc, g|
222
+ acc.merge(g.node_aliases)
223
+ end
224
+ end
225
+
160
226
  # Return a mapping of group names to group.
161
227
  def collect_groups
162
228
  @groups.inject(name => self) do |acc, g|
@@ -68,6 +68,10 @@ module Bolt
68
68
  result
69
69
  end
70
70
 
71
+ def provided_features
72
+ []
73
+ end
74
+
71
75
  def filter_options(target, options)
72
76
  if target.options['run-as']
73
77
  options.reject { |k, _v| k == '_run_as' }
@@ -11,7 +11,9 @@ module Bolt
11
11
  %w[service-url service-options tmpdir]
12
12
  end
13
13
 
14
- PROVIDED_FEATURES = ['shell'].freeze
14
+ def provided_features
15
+ ['shell']
16
+ end
15
17
 
16
18
  def self.validate(options)
17
19
  if (url = options['service-url'])
@@ -75,7 +77,7 @@ module Bolt
75
77
  end
76
78
 
77
79
  def run_task(target, task, arguments, _options = {})
78
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
80
+ implementation = task.select_implementation(target, provided_features)
79
81
  executable = implementation['path']
80
82
  input_method = implementation['input_method']
81
83
  extra_files = implementation['files']
@@ -13,7 +13,9 @@ module Bolt
13
13
  %w[tmpdir]
14
14
  end
15
15
 
16
- PROVIDED_FEATURES = ['shell'].freeze
16
+ def provided_features
17
+ ['shell']
18
+ end
17
19
 
18
20
  def self.validate(_options); end
19
21
 
@@ -82,7 +84,7 @@ module Bolt
82
84
  end
83
85
 
84
86
  def run_task(target, task, arguments, _options = {})
85
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
87
+ implementation = task.select_implementation(target, provided_features)
86
88
  executable = implementation['path']
87
89
  input_method = implementation['input_method'] || 'both'
88
90
  extra_files = implementation['files']
@@ -29,7 +29,9 @@ module Bolt
29
29
  %w[service-url cacert token-file task-environment]
30
30
  end
31
31
 
32
- PROVIDED_FEATURES = ['puppet-agent'].freeze
32
+ def provided_features
33
+ ['puppet-agent']
34
+ end
33
35
 
34
36
  def self.validate(options); end
35
37
 
@@ -12,7 +12,9 @@ module Bolt
12
12
  %w[port user password sudo-password private-key host-key-check connect-timeout tmpdir run-as tty run-as-command]
13
13
  end
14
14
 
15
- PROVIDED_FEATURES = ['shell'].freeze
15
+ def provided_features
16
+ ['shell']
17
+ end
16
18
 
17
19
  def self.validate(options)
18
20
  logger = Logging.logger[self]
@@ -119,7 +121,7 @@ module Bolt
119
121
  end
120
122
 
121
123
  def run_task(target, task, arguments, options = {})
122
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
124
+ implementation = task.select_implementation(target, provided_features)
123
125
  executable = implementation['path']
124
126
  input_method = implementation['input_method']
125
127
  extra_files = implementation['files']
@@ -14,7 +14,9 @@ module Bolt
14
14
  %w[port user password connect-timeout ssl ssl-verify tmpdir cacert extensions]
15
15
  end
16
16
 
17
- PROVIDED_FEATURES = ['powershell'].freeze
17
+ def provided_features
18
+ ['powershell']
19
+ end
18
20
 
19
21
  def self.validate(options)
20
22
  ssl_flag = options['ssl']
@@ -108,7 +110,7 @@ catch
108
110
  end
109
111
 
110
112
  def run_task(target, task, arguments, _options = {})
111
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
113
+ implementation = task.select_implementation(target, provided_features)
112
114
  executable = implementation['path']
113
115
  input_method = implementation['input_method']
114
116
  extra_files = implementation['files']
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.4.0'
4
+ VERSION = '1.5.0'
5
5
  end
@@ -106,7 +106,7 @@ module BoltServer
106
106
 
107
107
  def serial_execute(&block)
108
108
  promise = Concurrent::Promise.new(executor: @executor, &block).execute.wait
109
- raise promise.reason if promise.state == :rejected
109
+ raise promise.reason if promise.rejected?
110
110
  promise.value
111
111
  end
112
112
 
@@ -202,7 +202,6 @@ module BoltSpec
202
202
  end
203
203
 
204
204
  def allow_apply_prep
205
- allow_task('puppet_agent::version').always_return('version' => '6.0')
206
205
  allow_task('apply_helpers::custom_facts')
207
206
  nil
208
207
  end
@@ -148,6 +148,17 @@ module BoltSpec
148
148
  Bolt::ResultSet.new(promises.map { |target| Bolt::ApplyResult.new(target) })
149
149
  end
150
150
  # End Apply mocking
151
+
152
+ # Mocked for apply_prep
153
+ def transport(_protocol)
154
+ # Always return a transport that includes the puppet-agent feature so version/install are skipped.
155
+ Class.new do
156
+ def provided_features
157
+ ['puppet-agent']
158
+ end
159
+ end.new
160
+ end
161
+ # End apply_prep mocking
151
162
  end
152
163
  end
153
164
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra'
4
+ require 'bolt'
5
+ require 'bolt/error'
6
+ require 'bolt/executor'
7
+ require 'bolt/inventory'
8
+ require 'bolt/pal'
9
+ require 'bolt/puppetdb'
10
+ require 'concurrent'
11
+ require 'json'
12
+ require 'json-schema'
13
+
14
+ module PlanExecutor
15
+ class App < Sinatra::Base
16
+ # This disables Sinatra's error page generation
17
+ set :show_exceptions, false
18
+ # Global var to capture output for testing
19
+ result = nil
20
+
21
+ helpers do
22
+ def puppetdb_client
23
+ return @puppetdb_client if @puppetdb_client
24
+ @puppetdb_client = Bolt::PuppetDB::Client.new({})
25
+ end
26
+ end
27
+
28
+ def initialize(modulepath, executor = nil)
29
+ @schema = JSON.parse(File.read(File.join(__dir__, 'schemas', 'run_plan.json')))
30
+ @worker = Concurrent::SingleThreadExecutor.new
31
+
32
+ # Create a basic executor, leave concurrency up to Orchestrator.
33
+ @executor = executor || Bolt::Executor.new(0, load_config: false)
34
+ # Use an empty inventory until we figure out where this data comes from.
35
+ @inventory = Bolt::Inventory.new(nil)
36
+ # TODO: what should max compiles be set to for apply?
37
+ @pal = Bolt::PAL.new(modulepath, nil)
38
+
39
+ super(nil)
40
+ end
41
+
42
+ def validate_schema(schema, body)
43
+ schema_error = JSON::Validator.fully_validate(schema, body)
44
+ if schema_error.any?
45
+ Bolt::Error.new("There was an error validating the request body.",
46
+ 'boltserver/schema-error',
47
+ schema_error)
48
+ end
49
+ end
50
+
51
+ get '/' do
52
+ 200
53
+ end
54
+
55
+ if ENV['RACK_ENV'] == 'dev'
56
+ get '/admin/gc' do
57
+ GC.start
58
+ 200
59
+ end
60
+
61
+ get '/admin/gc_stat' do
62
+ [200, GC.stat.to_json]
63
+ end
64
+ end
65
+
66
+ get '/500_error' do
67
+ raise 'Unexpected error'
68
+ end
69
+
70
+ post '/plan/run' do
71
+ content_type :json
72
+
73
+ body = JSON.parse(request.body.read)
74
+ error = validate_schema(@schema, body)
75
+ return [400, error.to_json] unless error.nil?
76
+
77
+ name = body['plan_name']
78
+ # Errors if plan is not found
79
+ @pal.get_plan_info(name)
80
+
81
+ params = body['params']
82
+ # This provides a wait function, which promise doesn't
83
+ result = Concurrent::Future.execute(executor: @worker) do
84
+ # Stores result in result for testing
85
+ @pal.run_plan(name, params, @executor, @inventory, puppetdb_client)
86
+ end
87
+
88
+ [200, { status: 'running' }.to_json]
89
+ end
90
+
91
+ # Provided for testing
92
+ get '/plan/result' do
93
+ result.wait_or_cancel(20)
94
+ if result.fulfilled?
95
+ return [200, result.value.to_json]
96
+ elsif result.rejected?
97
+ raise result.reason.to_s
98
+ else
99
+ return [200, result.state.to_s]
100
+ end
101
+ end
102
+
103
+ error 404 do
104
+ err = Bolt::Error.new("Could not find route #{request.path}",
105
+ 'boltserver/not-found')
106
+ [404, err.to_json]
107
+ end
108
+
109
+ error 500 do
110
+ e = env['sinatra.error']
111
+ err = Bolt::Error.new("500: Unknown error: #{e.message}",
112
+ 'boltserver/server-error')
113
+ [500, err.to_json]
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "run_plan request",
4
+ "description": "POST plan/run request schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "plan_name": {
8
+ "type": "string",
9
+ "description": "Name of the plan"
10
+ },
11
+ "job_id": {
12
+ "type": "string",
13
+ "description": "The job ID initialized in Orchestrator"
14
+ },
15
+ "environment": {
16
+ "type": "string",
17
+ "description": "Environment used for plan execution"
18
+ },
19
+ "description": {
20
+ "type": "string",
21
+ "description": "Describes this execution of the plan"
22
+ },
23
+ "params": {
24
+ "type": "object",
25
+ "description": "JSON formatted parameters to be provided to plan"
26
+ }
27
+ },
28
+ "required": ["plan_name", "job_id", "params"]
29
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-30 00:00:00.000000000 Z
11
+ date: 2018-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -368,6 +368,8 @@ files:
368
368
  - lib/bolt_spec/plans/action_stubs/upload_stub.rb
369
369
  - lib/bolt_spec/plans/mock_executor.rb
370
370
  - lib/bolt_spec/run.rb
371
+ - lib/plan_executor/app.rb
372
+ - lib/plan_executor/schemas/run_plan.json
371
373
  - libexec/apply_catalog.rb
372
374
  - libexec/bolt_catalog
373
375
  - libexec/custom_facts.rb