bolt 2.24.1 → 2.29.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/lib/bolt/analytics.rb +7 -3
  7. data/lib/bolt/applicator.rb +21 -21
  8. data/lib/bolt/bolt_option_parser.rb +77 -26
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +135 -147
  11. data/lib/bolt/config.rb +48 -25
  12. data/lib/bolt/config/options.rb +34 -2
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +8 -1
  15. data/lib/bolt/inventory/group.rb +1 -1
  16. data/lib/bolt/inventory/inventory.rb +1 -1
  17. data/lib/bolt/inventory/target.rb +1 -1
  18. data/lib/bolt/logger.rb +35 -21
  19. data/lib/bolt/outputter/logger.rb +1 -1
  20. data/lib/bolt/pal.rb +21 -10
  21. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  22. data/lib/bolt/plugin/puppetdb.rb +1 -1
  23. data/lib/bolt/project.rb +62 -17
  24. data/lib/bolt/puppetdb/client.rb +1 -1
  25. data/lib/bolt/puppetdb/config.rb +1 -1
  26. data/lib/bolt/puppetfile.rb +160 -0
  27. data/lib/bolt/puppetfile/installer.rb +43 -0
  28. data/lib/bolt/puppetfile/module.rb +89 -0
  29. data/lib/bolt/r10k_log_proxy.rb +1 -1
  30. data/lib/bolt/rerun.rb +2 -2
  31. data/lib/bolt/result.rb +23 -0
  32. data/lib/bolt/shell.rb +1 -1
  33. data/lib/bolt/task.rb +1 -1
  34. data/lib/bolt/transport/base.rb +5 -5
  35. data/lib/bolt/transport/docker/connection.rb +1 -1
  36. data/lib/bolt/transport/local/connection.rb +1 -1
  37. data/lib/bolt/transport/ssh.rb +1 -1
  38. data/lib/bolt/transport/ssh/connection.rb +1 -1
  39. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  40. data/lib/bolt/transport/winrm.rb +1 -1
  41. data/lib/bolt/transport/winrm/connection.rb +1 -1
  42. data/lib/bolt/util.rb +30 -11
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -12
  47. data/lib/bolt_server/transport_app.rb +125 -26
  48. data/lib/bolt_spec/bolt_context.rb +4 -4
  49. data/lib/bolt_spec/run.rb +3 -0
  50. metadata +11 -14
@@ -7,7 +7,7 @@ module Bolt
7
7
  class YamlPlan
8
8
  class Evaluator
9
9
  def initialize(analytics = Bolt::Analytics::NoopClient.new)
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  @analytics = analytics
12
12
  @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
13
  end
@@ -19,7 +19,7 @@ module Bolt
19
19
  def initialize(config:, context:)
20
20
  pdb_config = Bolt::PuppetDB::Config.load_config(config, context.boltdir)
21
21
  @puppetdb_client = Bolt::PuppetDB::Client.new(pdb_config)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  end
24
24
 
25
25
  def name
@@ -17,37 +17,53 @@ module Bolt
17
17
  }.freeze
18
18
 
19
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
20
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
+ :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
21
21
  :deprecations, :downloads, :plans_path
22
22
 
23
- def self.default_project
24
- create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
23
+ def self.default_project(logs = [])
24
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
25
25
  # If homedir isn't defined use the system config path
26
26
  rescue ArgumentError
27
- create_project(Bolt::Config.system_path, 'system')
27
+ create_project(Bolt::Config.system_path, 'system', logs)
28
28
  end
29
29
 
30
30
  # Search recursively up the directory hierarchy for the Project. Look for a
31
31
  # directory called Boltdir or a file called bolt.yaml (for a control repo
32
32
  # type Project). Otherwise, repeat the check on each directory up the
33
33
  # hierarchy, falling back to the default if we reach the root.
34
- def self.find_boltdir(dir)
34
+ def self.find_boltdir(dir, logs = [])
35
35
  dir = Pathname.new(dir)
36
36
 
37
37
  if (dir + BOLTDIR_NAME).directory?
38
- create_project(dir + BOLTDIR_NAME, 'embedded')
38
+ create_project(dir + BOLTDIR_NAME, 'embedded', logs)
39
39
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
40
- create_project(dir, 'local')
40
+ create_project(dir, 'local', logs)
41
41
  elsif dir.root?
42
- default_project
42
+ default_project(logs)
43
43
  else
44
- find_boltdir(dir.parent)
44
+ logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
45
+ "This directory won't be loaded as a project." }
46
+ find_boltdir(dir.parent, logs)
45
47
  end
46
48
  end
47
49
 
48
- def self.create_project(path, type = 'option')
50
+ def self.create_project(path, type = 'option', logs = [])
49
51
  fullpath = Pathname.new(path).expand_path
50
52
 
53
+ if type == 'user'
54
+ begin
55
+ # This is already expanded if the type is user
56
+ FileUtils.mkdir_p(path)
57
+ rescue StandardError
58
+ logs << { warn: "Could not create default project at #{path}. Continuing without a writeable project. "\
59
+ "Log and rerun files will not be written." }
60
+ end
61
+ end
62
+
63
+ if type == 'option' && !File.directory?(path)
64
+ raise Bolt::Error.new("Could not find project at #{path}", "bolt/project-error")
65
+ end
66
+
51
67
  if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
52
68
  raise Bolt::Error.new(
53
69
  "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
@@ -58,15 +74,18 @@ module Bolt
58
74
 
59
75
  project_file = File.join(fullpath, 'bolt-project.yaml')
60
76
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
61
- new(data, path, type)
77
+ default = type =~ /user|system/ ? 'default ' : ''
78
+ exist = File.exist?(File.expand_path(project_file))
79
+ logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
80
+ new(data, path, type, logs)
62
81
  end
63
82
 
64
- def initialize(raw_data, path, type = 'option')
83
+ def initialize(raw_data, path, type = 'option', logs = [])
65
84
  @path = Pathname.new(path).expand_path
66
85
 
67
86
  @project_file = @path + 'bolt-project.yaml'
68
87
 
69
- @warnings = []
88
+ @logs = logs
70
89
  @deprecations = []
71
90
  if (@path + 'bolt.yaml').file? && project_file?
72
91
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
@@ -88,7 +107,7 @@ module Bolt
88
107
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
89
108
  if tc.any?
90
109
  msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
91
- @warnings << { msg: msg }
110
+ @logs << { warn: msg }
92
111
  end
93
112
 
94
113
  @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
@@ -98,7 +117,7 @@ module Bolt
98
117
  @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
99
118
  if (@path + 'bolt.yaml').file?
100
119
  msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
101
- @warnings << { msg: msg }
120
+ @logs << { warn: msg }
102
121
  end
103
122
  @project_file
104
123
  else
@@ -114,7 +133,9 @@ module Bolt
114
133
  # This API is used to prepend the project as a module to Puppet's internal
115
134
  # module_references list. CHANGE AT YOUR OWN RISK
116
135
  def to_h
117
- { path: @path.to_s, name: name }
136
+ { path: @path.to_s,
137
+ name: name,
138
+ load_as_module?: load_as_module? }
118
139
  end
119
140
 
120
141
  def eql?(other)
@@ -126,6 +147,10 @@ module Bolt
126
147
  @project_file.file?
127
148
  end
128
149
 
150
+ def load_as_module?
151
+ !name.nil?
152
+ end
153
+
129
154
  def name
130
155
  @data['name']
131
156
  end
@@ -138,6 +163,10 @@ module Bolt
138
163
  @data['plans']
139
164
  end
140
165
 
166
+ def modules
167
+ @data['modules']
168
+ end
169
+
141
170
  def validate
142
171
  if name
143
172
  if name !~ Bolt::Module::MODULE_NAME_REGEX
@@ -151,7 +180,7 @@ module Bolt
151
180
  end
152
181
  else
153
182
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
154
- @warnings << { msg: message }
183
+ @logs << { warn: message }
155
184
  end
156
185
 
157
186
  %w[tasks plans].each do |conf|
@@ -159,6 +188,22 @@ module Bolt
159
188
  raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
160
189
  end
161
190
  end
191
+
192
+ if @data['modules']
193
+ unless @data['modules'].is_a?(Array)
194
+ raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
195
+ end
196
+
197
+ @data['modules'].each do |mod|
198
+ next if mod.is_a?(Hash)
199
+ raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash"
200
+ end
201
+
202
+ unknown_keys = data['modules'].flat_map(&:keys).uniq - %w[name version_requirement]
203
+ if unknown_keys.any?
204
+ @logs << { warn: "Ignoring unknown keys in module declarations: #{unknown_keys.join(', ')}." }
205
+ end
206
+ end
162
207
  end
163
208
 
164
209
  def check_deprecated_file
@@ -13,7 +13,7 @@ module Bolt
13
13
  @config = config
14
14
  @bad_urls = []
15
15
  @current_url = nil
16
- @logger = Logging.logger[self]
16
+ @logger = Bolt::Logger.logger(self)
17
17
  end
18
18
 
19
19
  def query_certnames(query)
@@ -35,7 +35,7 @@ module Bolt
35
35
  begin
36
36
  config = JSON.parse(File.read(filepath)) if filepath
37
37
  rescue StandardError => e
38
- Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
38
+ Bolt::Logger.logger(self).error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
39
39
  end
40
40
 
41
41
  config = config.fetch('puppetdb', {})
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/puppetfile/module'
5
+
6
+ # This class manages the logical contents of a Puppetfile. It includes methods
7
+ # for parsing a Puppetfile and its modules, resolving module dependencies,
8
+ # and writing a Puppetfile.
9
+ #
10
+ module Bolt
11
+ class Puppetfile
12
+ attr_reader :modules
13
+
14
+ def initialize(modules = [])
15
+ @modules = Set.new
16
+ add_modules(modules)
17
+ end
18
+
19
+ # Loads a Puppetfile and parses its module specifications, returning a
20
+ # Bolt::Puppetfile object with the modules set.
21
+ #
22
+ def self.parse(path)
23
+ require 'puppetfile-resolver'
24
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval'
25
+
26
+ begin
27
+ parsed = ::PuppetfileResolver::Puppetfile::Parser::R10KEval.parse(File.read(path))
28
+ rescue StandardError => e
29
+ raise Bolt::Error.new(
30
+ "Unable to parse Puppetfile #{path}: #{e.message}",
31
+ 'bolt/puppetfile-parsing'
32
+ )
33
+ end
34
+
35
+ unless parsed.valid?
36
+ raise Bolt::ValidationError,
37
+ "Unable to parse Puppetfile #{path}"
38
+ end
39
+
40
+ modules = parsed.modules.map do |mod|
41
+ Bolt::Puppetfile::Module.new(mod.owner, mod.name, mod.version)
42
+ end
43
+
44
+ new(modules)
45
+ end
46
+
47
+ # Writes a Puppetfile that includes specifications for each of the
48
+ # modules.
49
+ #
50
+ def write(path, force: false)
51
+ if File.exist?(path) && !force
52
+ raise Bolt::FileError.new(
53
+ "Cannot overwrite existing Puppetfile at #{path}. To forcibly overwrite, "\
54
+ "run with the '--force' option.",
55
+ path
56
+ )
57
+ end
58
+
59
+ File.open(path, 'w') do |file|
60
+ file.puts '# This Puppetfile is managed by Bolt. Do not edit.'
61
+ modules.each { |mod| file.puts mod.to_spec }
62
+ file.puts
63
+ end
64
+ rescue SystemCallError => e
65
+ raise Bolt::FileError.new(
66
+ "#{e.message}: unable to write Puppetfile.",
67
+ path
68
+ )
69
+ end
70
+
71
+ # Resolves module dependencies using the puppetfile-resolver library. The
72
+ # resolver will return a document model including all module dependencies
73
+ # and the latest version that can be installed for each. The document model
74
+ # is parsed and turned into a Set of Bolt::Puppetfile::Module objects.
75
+ #
76
+ def resolve
77
+ require 'puppetfile-resolver'
78
+
79
+ # Build the document model from the modules.
80
+ model = PuppetfileResolver::Puppetfile::Document.new('')
81
+
82
+ @modules.each do |mod|
83
+ model.add_module(
84
+ PuppetfileResolver::Puppetfile::ForgeModule.new(mod.title).tap do |tap|
85
+ tap.version = mod.version || :latest
86
+ end
87
+ )
88
+ end
89
+
90
+ # Make sure the Puppetfile model is valid.
91
+ unless model.valid?
92
+ raise Bolt::ValidationError,
93
+ "Unable to resolve dependencies for modules: #{@modules.map(&:title).join(', ')}"
94
+ end
95
+
96
+ # Create the resolver using the Puppetfile model. nil disables Puppet
97
+ # version restrictions.
98
+ resolver = PuppetfileResolver::Resolver.new(model, nil)
99
+
100
+ # Configure and resolve the dependency graph, catching any errors
101
+ # raised by puppetfile-resolver and re-raising them as Bolt errors.
102
+ begin
103
+ result = resolver.resolve(
104
+ cache: nil,
105
+ ui: nil,
106
+ module_paths: [],
107
+ allow_missing_modules: true
108
+ )
109
+ rescue StandardError => e
110
+ raise Bolt::Error.new(e.message, 'bolt/puppetfile-resolver-error')
111
+ end
112
+
113
+ # Validate that the modules exist.
114
+ missing_graph = result.specifications.select do |_name, spec|
115
+ spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
116
+ end
117
+
118
+ if missing_graph.any?
119
+ titles = model.modules.each_with_object({}) do |mod, acc|
120
+ acc[mod.name] = mod.title
121
+ end
122
+
123
+ names = titles.values_at(*missing_graph.keys)
124
+ plural = names.count == 1 ? '' : 's'
125
+
126
+ raise Bolt::Error.new(
127
+ "Unknown module name#{plural} #{names.join(', ')}",
128
+ 'bolt/unknown-modules'
129
+ )
130
+ end
131
+
132
+ # Filter the dependency graph to only include module specifications. This
133
+ # will only remove the Puppet version specification, which is not needed.
134
+ specs = result.specifications.select do |_name, spec|
135
+ spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
136
+ end
137
+
138
+ @modules = specs.map do |_name, spec|
139
+ Bolt::Puppetfile::Module.new(spec.owner, spec.name, spec.version.to_s)
140
+ end.to_set
141
+ end
142
+
143
+ # Adds to the set of modules.
144
+ #
145
+ def add_modules(modules)
146
+ modules.each do |mod|
147
+ case mod
148
+ when Bolt::Puppetfile::Module
149
+ @modules << mod
150
+ when Hash
151
+ @modules << Bolt::Puppetfile::Module.from_hash(mod)
152
+ else
153
+ raise Bolt::ValidationError, "Module must be a Bolt::Puppetfile::Module or Hash."
154
+ end
155
+ end
156
+
157
+ @modules
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'r10k/cli'
4
+ require 'bolt/r10k_log_proxy'
5
+ require 'bolt/error'
6
+
7
+ # This class is used to install modules from a Puppetfile to a module directory.
8
+ #
9
+ module Bolt
10
+ class Puppetfile
11
+ class Installer
12
+ def initialize(config = {})
13
+ @config = config
14
+ end
15
+
16
+ def install(path, moduledir)
17
+ unless File.exist?(path)
18
+ raise Bolt::FileError.new(
19
+ "Could not find a Puppetfile at #{path}",
20
+ path
21
+ )
22
+ end
23
+
24
+ r10k_opts = {
25
+ root: File.dirname(path),
26
+ puppetfile: path.to_s,
27
+ moduledir: moduledir.to_s
28
+ }
29
+
30
+ settings = R10K::Settings.global_settings.evaluate(@config)
31
+ R10K::Initializers::GlobalInitializer.new(settings).call
32
+ install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
33
+
34
+ # Override the r10k logger with a proxy to our own logger
35
+ R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
36
+
37
+ install_action.call
38
+ rescue R10K::Error => e
39
+ raise PuppetfileError, e
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class represents a module specification. It used by the Bolt::Puppetfile
6
+ # class to have a consistent API for accessing a module's attributes.
7
+ #
8
+ module Bolt
9
+ class Puppetfile
10
+ class Module
11
+ attr_reader :owner, :name, :version
12
+
13
+ def initialize(owner, name, version = nil)
14
+ @owner = owner
15
+ @name = name
16
+ @version = version
17
+ end
18
+
19
+ # Creates a new module from a hash.
20
+ #
21
+ def self.from_hash(mod)
22
+ unless mod['name'].is_a?(String)
23
+ raise Bolt::ValidationError,
24
+ "Module name must be a String, not #{mod['name'].inspect}"
25
+ end
26
+
27
+ owner, name = mod['name'].tr('/', '-').split('-', 2)
28
+
29
+ unless owner && name
30
+ raise Bolt::ValidationError, "Module name #{mod['name']} must include both the owner and module name."
31
+ end
32
+
33
+ new(owner, name, mod['version_requirement'])
34
+ end
35
+
36
+ # Returns the module's title.
37
+ #
38
+ def title
39
+ "#{@owner}-#{@name}"
40
+ end
41
+
42
+ # Checks two modules for equality.
43
+ #
44
+ def eql?(other)
45
+ self.class == other.class &&
46
+ @owner == other.owner &&
47
+ @name == other.name &&
48
+ versions_intersect?(other)
49
+ end
50
+ alias == eql?
51
+
52
+ # Returns true if the versions of two modules intersect. Used to determine
53
+ # if an installed module satisfies the version requirement of another.
54
+ #
55
+ def versions_intersect?(other)
56
+ range = ::SemanticPuppet::VersionRange.parse(@version || '')
57
+ other_range = ::SemanticPuppet::VersionRange.parse(other.version || '')
58
+
59
+ range.intersection(other_range) != ::SemanticPuppet::VersionRange::EMPTY_RANGE
60
+ end
61
+
62
+ # Hashes the module.
63
+ #
64
+ def hash
65
+ [@owner, @name].hash
66
+ end
67
+
68
+ # Returns a hash representation similar to the module
69
+ # declaration.
70
+ #
71
+ def to_hash
72
+ {
73
+ 'name' => title,
74
+ 'version_requirement' => version
75
+ }.compact
76
+ end
77
+
78
+ # Returns the Puppetfile specification for the module.
79
+ #
80
+ def to_spec
81
+ if @version
82
+ "mod #{title.inspect}, #{@version.inspect}"
83
+ else
84
+ "mod #{title.inspect}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end