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 +4 -4
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +34 -12
- data/lib/bolt/catalog.rb +4 -0
- data/lib/bolt/cli.rb +1 -1
- data/lib/bolt/inventory.rb +21 -16
- data/lib/bolt/inventory/group.rb +100 -34
- data/lib/bolt/transport/base.rb +4 -0
- data/lib/bolt/transport/docker.rb +4 -2
- data/lib/bolt/transport/local.rb +4 -2
- data/lib/bolt/transport/orch.rb +3 -1
- data/lib/bolt/transport/ssh.rb +4 -2
- data/lib/bolt/transport/winrm.rb +4 -2
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/file_cache.rb +1 -1
- data/lib/bolt_spec/plans.rb +0 -1
- data/lib/bolt_spec/plans/mock_executor.rb +11 -0
- data/lib/plan_executor/app.rb +116 -0
- data/lib/plan_executor/schemas/run_plan.json +29 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59ccedf25067dbbeb8769b4cb77178679c6b5a7a312dcecba80841af2d472ffe
|
4
|
+
data.tar.gz: 358e3ce1e0e4e3241eb9442fac18b80b9e36bec98b3e3d1dbbd039c5708d458b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
|
data/lib/bolt/catalog.rb
CHANGED
@@ -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
|
|
data/lib/bolt/cli.rb
CHANGED
@@ -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
|
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
|
data/lib/bolt/inventory.rb
CHANGED
@@ -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(
|
16
|
+
super(message, 'bolt.inventory/validation-error')
|
17
17
|
@_message = message
|
18
|
-
@path =
|
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
|
-
|
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
|
-
#
|
198
|
-
#
|
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
|
-
|
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.
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
37
|
+
|
38
|
+
if @nodes.include?(node['name'])
|
42
39
|
@logger.warn("Ignoring duplicate node in #{@name}: #{node}")
|
43
|
-
|
44
|
-
|
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
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
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
|
127
|
+
Target.new(n)
|
78
128
|
rescue Addressable::URI::InvalidURIError => e
|
79
129
|
@logger.debug(e)
|
80
|
-
raise ValidationError.new("Invalid node name #{n
|
130
|
+
raise ValidationError.new("Invalid node name #{n}", @name)
|
81
131
|
end
|
82
132
|
|
83
|
-
raise ValidationError.new(
|
84
|
-
if
|
85
|
-
|
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
|
-
|
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|
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -11,7 +11,9 @@ module Bolt
|
|
11
11
|
%w[service-url service-options tmpdir]
|
12
12
|
end
|
13
13
|
|
14
|
-
|
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,
|
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']
|
data/lib/bolt/transport/local.rb
CHANGED
@@ -13,7 +13,9 @@ module Bolt
|
|
13
13
|
%w[tmpdir]
|
14
14
|
end
|
15
15
|
|
16
|
-
|
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,
|
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']
|
data/lib/bolt/transport/orch.rb
CHANGED
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -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
|
-
|
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,
|
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']
|
data/lib/bolt/transport/winrm.rb
CHANGED
@@ -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
|
-
|
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,
|
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']
|
data/lib/bolt/version.rb
CHANGED
@@ -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.
|
109
|
+
raise promise.reason if promise.rejected?
|
110
110
|
promise.value
|
111
111
|
end
|
112
112
|
|
data/lib/bolt_spec/plans.rb
CHANGED
@@ -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
|
+
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
|
+
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
|