bolt 2.9.0 → 2.13.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.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +28 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
  8. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +61 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +4 -2
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -2
  13. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +144 -0
  14. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  15. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  17. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  18. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  19. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  20. data/lib/bolt/analytics.rb +21 -2
  21. data/lib/bolt/applicator.rb +7 -2
  22. data/lib/bolt/apply_result.rb +1 -1
  23. data/lib/bolt/apply_target.rb +3 -2
  24. data/lib/bolt/bolt_option_parser.rb +18 -8
  25. data/lib/bolt/catalog.rb +4 -1
  26. data/lib/bolt/cli.rb +15 -5
  27. data/lib/bolt/config.rb +44 -16
  28. data/lib/bolt/config/transport/ssh.rb +47 -1
  29. data/lib/bolt/inventory.rb +2 -1
  30. data/lib/bolt/inventory/inventory.rb +5 -0
  31. data/lib/bolt/inventory/target.rb +17 -1
  32. data/lib/bolt/node/output.rb +1 -1
  33. data/lib/bolt/pal.rb +3 -0
  34. data/lib/bolt/pal/yaml_plan.rb +1 -0
  35. data/lib/bolt/plugin.rb +13 -7
  36. data/lib/bolt/plugin/puppetdb.rb +5 -2
  37. data/lib/bolt/project.rb +25 -7
  38. data/lib/bolt/puppetdb/config.rb +14 -26
  39. data/lib/bolt/resource_instance.rb +129 -0
  40. data/lib/bolt/shell/bash.rb +1 -1
  41. data/lib/bolt/target.rb +12 -1
  42. data/lib/bolt/transport/ssh.rb +6 -2
  43. data/lib/bolt/transport/ssh/connection.rb +4 -0
  44. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  45. data/lib/bolt/transport/winrm/connection.rb +4 -0
  46. data/lib/bolt/version.rb +1 -1
  47. data/lib/bolt_server/pe/pal.rb +1 -38
  48. data/lib/bolt_spec/bolt_context.rb +1 -4
  49. data/lib/bolt_spec/plans/mock_executor.rb +1 -0
  50. data/lib/bolt_spec/run.rb +2 -5
  51. metadata +6 -2
@@ -15,6 +15,7 @@ module Bolt
15
15
 
16
16
  class ValidationError < Bolt::Error
17
17
  attr_accessor :path
18
+
18
19
  def initialize(message, offending_group)
19
20
  super(message, 'bolt.inventory/validation-error')
20
21
  @_message = message
@@ -83,7 +84,7 @@ module Bolt
83
84
 
84
85
  def self.empty
85
86
  config = Bolt::Config.default
86
- plugins = Bolt::Plugin.setup(config, nil, nil, Bolt::Analytics::NoopClient)
87
+ plugins = Bolt::Plugin.setup(config, nil)
87
88
 
88
89
  create_version({}, config.transport, config.transports, plugins)
89
90
  end
@@ -7,6 +7,7 @@ module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
9
  attr_reader :targets, :plugins, :config, :transport
10
+
10
11
  class WildcardError < Bolt::Error
11
12
  def initialize(target)
12
13
  super("Found 0 targets matching wildcard pattern #{target}", 'bolt.inventory/wildcard-error')
@@ -318,6 +319,10 @@ module Bolt
318
319
  def target_config(target)
319
320
  @targets[target.name].config
320
321
  end
322
+
323
+ def resources(target)
324
+ @targets[target.name].resources
325
+ end
321
326
  end
322
327
  end
323
328
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/resource_instance'
4
+
3
5
  module Bolt
4
6
  class Inventory
5
7
  # This class represents the active state of a target within the inventory.
6
8
  class Target
7
- attr_reader :name, :uri, :safe_name, :target_alias
9
+ attr_reader :name, :uri, :safe_name, :target_alias, :resources
8
10
 
9
11
  def initialize(target_data, inventory)
10
12
  unless target_data['name'] || target_data['uri']
@@ -36,12 +38,26 @@ module Bolt
36
38
  # When alias is specified in a plan, the key will be `target_alias`, when
37
39
  # alias is specified in inventory the key will be `alias`.
38
40
  @target_alias = target_data['target_alias'] || target_data['alias'] || []
41
+ @resources = {}
39
42
 
40
43
  @inventory = inventory
41
44
 
42
45
  validate
43
46
  end
44
47
 
48
+ # rubocop:disable Naming/AccessorMethodName
49
+ def set_resource(resource)
50
+ if (existing_resource = resources[resource.reference])
51
+ existing_resource.overwrite_state(resource.state)
52
+ existing_resource.overwrite_desired_state(resource.desired_state)
53
+ existing_resource.events = existing_resource.events + resource.events
54
+ existing_resource
55
+ else
56
+ @resources[resource.reference] = resource
57
+ end
58
+ end
59
+ # rubocop:enable Naming/AccessorMethodName
60
+
45
61
  def vars
46
62
  group_cache['vars'].merge(@vars)
47
63
  end
@@ -12,7 +12,7 @@ module Bolt
12
12
  def initialize
13
13
  @stdout = StringIO.new
14
14
  @stderr = StringIO.new
15
- @exit_code = 'unkown'
15
+ @exit_code = 'unknown'
16
16
  end
17
17
  end
18
18
  end
@@ -190,6 +190,7 @@ module Bolt
190
190
  # versions of "core" types, which are already present on the agent,
191
191
  # but may cause issues on Puppet 5 agents.
192
192
  @original_modulepath,
193
+ @project,
193
194
  pdb_client,
194
195
  @hiera_config,
195
196
  @max_compiles,
@@ -359,6 +360,7 @@ module Bolt
359
360
  name = param.name
360
361
  if signature_params.include?(name)
361
362
  params[name] = { 'type' => param.types.first }
363
+ params[name]['sensitive'] = param.types.first =~ /\ASensitive(\[.*\])?\z/ ? true : false
362
364
  params[name]['default_value'] = defaults[name] if defaults.key?(name)
363
365
  params[name]['description'] = param.text unless param.text.empty?
364
366
  else
@@ -390,6 +392,7 @@ module Bolt
390
392
  param.type_expr
391
393
  end
392
394
  params[name] = { 'type' => type_str }
395
+ params[name]['sensitive'] = param.type_expr.instance_of?(Puppet::Pops::Types::PSensitiveType)
393
396
  params[name]['default_value'] = param.value
394
397
  params[name]['description'] = param.description if param.description
395
398
  end
@@ -99,6 +99,7 @@ module Bolt
99
99
  # logic.
100
100
  class EvaluableString
101
101
  attr_reader :value
102
+
102
103
  def initialize(value)
103
104
  @value = value
104
105
  end
@@ -119,13 +119,9 @@ module Bolt
119
119
  end
120
120
  end
121
121
 
122
- def self.setup(config, pal, pdb_client, analytics)
122
+ def self.setup(config, pal, analytics = Bolt::Analytics::NoopClient.new)
123
123
  plugins = new(config, pal, analytics)
124
124
 
125
- # PDB is special because it needs the PDB client. Since it has no config,
126
- # we can just add it first.
127
- plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
128
-
129
125
  # Initialize any plugins referenced in plugin config. This will also indirectly
130
126
  # initialize any plugins they depend on.
131
127
  if plugins.reference?(config.plugins)
@@ -142,7 +138,7 @@ module Bolt
142
138
  plugins
143
139
  end
144
140
 
145
- RUBY_PLUGINS = %w[task prompt env_var].freeze
141
+ RUBY_PLUGINS = %w[task prompt env_var puppetdb].freeze
146
142
  BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory
147
143
  yaml env_var gcloud_inventory].freeze
148
144
  DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }.freeze
@@ -161,6 +157,13 @@ module Bolt
161
157
  @unknown = Set.new
162
158
  @resolution_stack = []
163
159
  @unresolved_plugin_configs = config.plugins.dup
160
+ # The puppetdb plugin config comes from the puppetdb section, not from
161
+ # the plugins section
162
+ if @unresolved_plugin_configs.key?('puppetdb')
163
+ msg = "Configuration for the PuppetDB plugin must be in the 'puppetdb' config section, not 'plugins'"
164
+ raise Bolt::Error.new(msg, 'bolt/plugin-error')
165
+ end
166
+ @unresolved_plugin_configs['puppetdb'] = config.puppetdb if config.puppetdb
164
167
  @plugin_hooks = DEFAULT_PLUGIN_HOOKS.dup
165
168
  end
166
169
 
@@ -168,7 +171,6 @@ module Bolt
168
171
  @modules ||= Bolt::Module.discover(@pal.modulepath)
169
172
  end
170
173
 
171
- # Generally this is private. Puppetdb is special though
172
174
  def add_plugin(plugin)
173
175
  @plugins[plugin.name] = plugin
174
176
  end
@@ -235,6 +237,10 @@ module Bolt
235
237
  end
236
238
  end
237
239
 
240
+ def puppetdb_client
241
+ by_name('puppetdb').puppetdb_client
242
+ end
243
+
238
244
  # Evaluate all _plugin references in a data structure. Leaves are
239
245
  # evaluated and then their parents are evaluated with references replaced
240
246
  # by their values. If the result of a reference contains more references,
@@ -14,8 +14,11 @@ module Bolt
14
14
  TEMPLATE_OPTS = %w[alias config facts features name uri vars].freeze
15
15
  PLUGIN_OPTS = %w[_plugin query target_mapping].freeze
16
16
 
17
- def initialize(pdb_client)
18
- @puppetdb_client = pdb_client
17
+ attr_reader :puppetdb_client
18
+
19
+ def initialize(config:, context:)
20
+ pdb_config = Bolt::PuppetDB::Config.load_config(config, context.boltdir)
21
+ @puppetdb_client = Bolt::PuppetDB::Client.new(pdb_config)
19
22
  @logger = Logging.logger[self]
20
23
  end
21
24
 
@@ -16,7 +16,18 @@ module Bolt
16
16
  :puppetfile, :rerunfile, :type, :resource_types
17
17
 
18
18
  def self.default_project
19
- Project.new(File.join('~', '.puppetlabs', 'bolt'), 'user')
19
+ Project.new(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
20
+ # If homedir isn't defined use the system config path
21
+ rescue ArgumentError
22
+ Project.new(system_path, 'system')
23
+ end
24
+
25
+ def self.system_path
26
+ if Bolt::Util.windows?
27
+ File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc')
28
+ else
29
+ File.join('/etc', 'puppetlabs', 'bolt')
30
+ end
20
31
  end
21
32
 
22
33
  # Search recursively up the directory hierarchy for the Project. Look for a
@@ -27,7 +38,7 @@ module Bolt
27
38
  dir = Pathname.new(dir)
28
39
  if (dir + BOLTDIR_NAME).directory?
29
40
  new(dir + BOLTDIR_NAME, 'embedded')
30
- elsif (dir + 'bolt.yaml').file?
41
+ elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
31
42
  new(dir, 'local')
32
43
  elsif dir.root?
33
44
  default_project
@@ -47,7 +58,7 @@ module Bolt
47
58
  @resource_types = @path + '.resource_types'
48
59
  @type = type
49
60
 
50
- @project_file = @path + 'project.yaml'
61
+ @project_file = @path + 'bolt-project.yaml'
51
62
  @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
52
63
  validate if load_as_module?
53
64
  end
@@ -72,7 +83,7 @@ module Bolt
72
83
  end
73
84
 
74
85
  def name
75
- # If the project is in mymod/Boltdir/project.yaml, use mymod as the project name
86
+ # If the project is in mymod/Boltdir/bolt-project.yaml, use mymod as the project name
76
87
  dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
77
88
  pname = @data['name'] || dirname
78
89
  pname.include?('-') ? pname.split('-', 2)[1] : pname
@@ -100,7 +111,7 @@ module Bolt
100
111
  n = @data['name']
101
112
  if n && !project_directory_name?(n) && !project_namespaced_name?(n)
102
113
  raise Bolt::ValidationError, <<~ERROR_STRING
103
- Invalid project name '#{n}' in project.yaml; project names must match either:
114
+ Invalid project name '#{n}' in bolt-project.yaml; project names must match either:
104
115
  An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
105
116
  A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
106
117
  ERROR_STRING
@@ -110,7 +121,7 @@ module Bolt
110
121
  A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
111
122
  A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
112
123
 
113
- Configure project name in <project_dir>/project.yaml
124
+ Configure project name in <project_dir>/bolt-project.yaml
114
125
  ERROR_STRING
115
126
  # If the project name is the same as one of the built-in modules raise a warning
116
127
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
@@ -120,9 +131,16 @@ module Bolt
120
131
 
121
132
  %w[tasks plans].each do |conf|
122
133
  unless @data.fetch(conf, []).is_a?(Array)
123
- raise Bolt::ValidationError, "'#{conf}' in project.yaml must be an array"
134
+ raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
124
135
  end
125
136
  end
126
137
  end
138
+
139
+ def check_deprecated_file
140
+ if (@path + 'project.yaml').file?
141
+ logger = Logging.logger[self]
142
+ logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
143
+ end
144
+ end
127
145
  end
128
146
  end
@@ -22,27 +22,20 @@ module Bolt
22
22
  File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs/client-tools/puppetdb.conf'))
23
23
  end
24
24
 
25
- def self.load_config(filename, options, project_path = nil)
25
+ def self.load_config(options, project_path = nil)
26
26
  config = {}
27
27
  global_path = Bolt::Util.windows? ? default_windows_config : DEFAULT_CONFIG[:global]
28
- if filename
29
- if File.exist?(filename)
30
- config = JSON.parse(File.read(filename))
31
- else
32
- raise Bolt::PuppetDBError, "config file #{filename} does not exist"
33
- end
34
- else
35
- if File.exist?(DEFAULT_CONFIG[:user])
36
- filepath = DEFAULT_CONFIG[:user]
37
- elsif File.exist?(global_path)
38
- filepath = global_path
39
- end
40
28
 
41
- begin
42
- config = JSON.parse(File.read(filepath)) if filepath
43
- rescue StandardError => e
44
- Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
45
- end
29
+ if File.exist?(DEFAULT_CONFIG[:user])
30
+ filepath = DEFAULT_CONFIG[:user]
31
+ elsif File.exist?(global_path)
32
+ filepath = global_path
33
+ end
34
+
35
+ begin
36
+ config = JSON.parse(File.read(filepath)) if filepath
37
+ rescue StandardError => e
38
+ Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
46
39
  end
47
40
 
48
41
  config = config.fetch('puppetdb', {})
@@ -51,8 +44,7 @@ module Bolt
51
44
 
52
45
  def initialize(settings, project_path = nil)
53
46
  @settings = settings
54
- @project_path = project_path
55
- expand_paths
47
+ expand_paths(project_path)
56
48
  end
57
49
 
58
50
  def token
@@ -68,14 +60,10 @@ module Bolt
68
60
  @token = @token.strip if @token
69
61
  end
70
62
 
71
- def expand_paths
63
+ def expand_paths(project_path)
72
64
  %w[cacert cert key token].each do |file|
73
65
  next unless @settings[file]
74
- @settings[file] = if @project_path
75
- File.expand_path(@settings[file], @project_path)
76
- else
77
- File.expand_path(@settings[file])
78
- end
66
+ @settings[file] = File.expand_path(@settings[file], project_path)
79
67
  end
80
68
  end
81
69
 
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Bolt
6
+ class ResourceInstance
7
+ attr_reader :target, :type, :title, :state, :desired_state
8
+ attr_accessor :events
9
+
10
+ # Needed by Puppet to recognize Bolt::ResourceInstance as a Puppet object when deserializing
11
+ def self._pcore_type
12
+ ResourceInstance
13
+ end
14
+
15
+ # Needed by Puppet to serialize with _pcore_init_hash instead of the object's attributes
16
+ def self._pcore_init_from_hash(_init_hash)
17
+ raise "ResourceInstance shouldn't be instantiated from a pcore_init class method. "\
18
+ "How did this get called?"
19
+ end
20
+
21
+ def _pcore_init_from_hash(init_hash)
22
+ initialize(init_hash)
23
+ end
24
+
25
+ # Parameters will already be validated when calling ResourceInstance.new or
26
+ # set_resources() from a plan. We don't perform any validation in the class
27
+ # itself since Puppet will pass an empty hash to the initializer as part of
28
+ # the deserialization process before passing the _pcore_init_hash.
29
+ def initialize(resource_hash)
30
+ @target = resource_hash['target']
31
+ @type = resource_hash['type'].to_s.capitalize
32
+ @title = resource_hash['title']
33
+ @state = resource_hash['state'] || {}
34
+ @desired_state = resource_hash['desired_state'] || {}
35
+ @events = resource_hash['events'] || []
36
+ end
37
+
38
+ # Creates a ResourceInstance from a data hash in a plan when calling
39
+ # ResourceInstance.new($resource_hash) or $target.set_resources($resource_hash)
40
+ def self.from_asserted_hash(resource_hash)
41
+ new(resource_hash)
42
+ end
43
+
44
+ # Creates a ResourceInstance from positional arguments in a plan when
45
+ # calling ResourceInstance.new(target, type, title, ...)
46
+ def self.from_asserted_args(target,
47
+ type,
48
+ title,
49
+ state = nil,
50
+ desired_state = nil,
51
+ events = nil)
52
+ new(
53
+ 'target' => target,
54
+ 'type' => type,
55
+ 'title' => title,
56
+ 'state' => state,
57
+ 'desired_state' => desired_state,
58
+ 'events' => events
59
+ )
60
+ end
61
+
62
+ def eql?(other)
63
+ self.class.equal?(other.class) &&
64
+ target == other.target &&
65
+ type == other.type &&
66
+ title == other.title
67
+ end
68
+ alias == eql?
69
+
70
+ def to_hash
71
+ {
72
+ 'target' => target,
73
+ 'type' => type,
74
+ 'title' => title,
75
+ 'state' => state,
76
+ 'desired_state' => desired_state,
77
+ 'events' => events
78
+ }
79
+ end
80
+ alias _pcore_init_hash to_hash
81
+
82
+ def to_json(opts = nil)
83
+ to_hash.to_json(opts)
84
+ end
85
+
86
+ def reference
87
+ "#{type}[#{title}]"
88
+ end
89
+ alias to_s reference
90
+
91
+ def [](attribute)
92
+ @state[attribute]
93
+ end
94
+
95
+ def add_event(event)
96
+ @events << event
97
+ end
98
+
99
+ # rubocop:disable Naming/AccessorMethodName
100
+ def set_state(state)
101
+ assert_hash('state', state)
102
+ @state.merge!(state)
103
+ end
104
+ # rubocop:enable Naming/AccessorMethodName
105
+
106
+ def overwrite_state(state)
107
+ assert_hash('state', state)
108
+ @state = state
109
+ end
110
+
111
+ # rubocop:disable Naming/AccessorMethodName
112
+ def set_desired_state(desired_state)
113
+ assert_hash('desired_state', desired_state)
114
+ @desired_state.merge!(desired_state)
115
+ end
116
+ # rubocop:enable Naming/AccessorMethodName
117
+
118
+ def overwrite_desired_state(desired_state)
119
+ assert_hash('desired_state', desired_state)
120
+ @desired_state = desired_state
121
+ end
122
+
123
+ def assert_hash(loc, value)
124
+ unless value.is_a?(Hash)
125
+ raise Bolt::ValidationError, "#{loc} must be of type Hash; got #{value.class}"
126
+ end
127
+ end
128
+ end
129
+ end