bolt 2.13.0 → 2.14.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: 0fd39267ed3b68619b0e9f40f8415446d0061ebe5367650d50e6e5ab3229d8b9
4
- data.tar.gz: b4d88aee228eeccb8c9ed8ebcb6221960c3197f1ac43d4a195e5c3d4a8833a9d
3
+ metadata.gz: fbe6053f1a3801adb0f1d0684f1e4616311c93bf0513a74cde2c54c82d5de503
4
+ data.tar.gz: b51e4068dcceb096d5e6cec4a79d692b84d1472abe57065de1a219d8ed84a4ad
5
5
  SHA512:
6
- metadata.gz: 0a225b8e5e695da5268aa9b748c57c06944046ebc4865d76ec8d0f446ec37d0584f2fca2d19ee9910cd5ed5555f8cc15910693a66e813b0b0e6aab48dd9e8789
7
- data.tar.gz: 6d2853adea34d7b0006ac0d643f1af8cda862903618fde138cf64079d9b9c433815537c7f70c483fcbb00287462f4762285d5e8e5147b6327c7c5c71346412b4
6
+ metadata.gz: ea53a6643459311d007ad7b8c2a60e5d0d4af6b0005b037295edff063a2e1f0236054d9cb5c14cec039ee8d0eb0ffa94e887b6a1ba64c3bb196f2a10cd90ce6d
7
+ data.tar.gz: 7bfde56e613103081c5ce73095c82b897f5db8b973365e6fbd28dfc7f57873fce518d37fe55bb76e36f36ab9dc802a12e545621bbff44f3a389ff071898bbc2c
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lookup a resource in the target's data.
4
+ #
5
+ # For more information about resources see [the
6
+ # documentation](https://puppet.com/docs/puppet/latest/lang_resources.html).
7
+ #
8
+ # > **Note:** The `ResourceInstance` data type is under active development and is subject to
9
+ # change. You can read more about the data type in the [experimental features
10
+ # documentation](experimental_features.md#resourceinstance-data-type).
11
+ Puppet::Functions.create_function(:resource) do
12
+ # Lookup a resource in the target's data.
13
+ # @param target The Target object to add resources to. See {get_targets}.
14
+ # @param type The type of the resource
15
+ # @param title The title of the resource
16
+ # @return The ResourceInstance if found, or Undef
17
+ # @example Get the openssl package resource
18
+ # $target.apply_prep
19
+ # $resources = $target.get_resources(Package).first['resources']
20
+ # $target.set_resources($resources)
21
+ # $openssl = $target.resource('Package', 'openssl')
22
+ dispatch :resource do
23
+ param 'Target', :target
24
+ param 'Type[Resource]', :type
25
+ param 'String[1]', :title
26
+ return_type 'Optional[ResourceInstance]'
27
+ end
28
+
29
+ # Lookup a resource in the target's data, referring to resource as a string
30
+ # @param target The Target object to add resources to. See {get_targets}.
31
+ # @param type The type of the resource
32
+ # @param title The title of the resource
33
+ # @return The ResourceInstance if found, or Undef
34
+ dispatch :resource_from_string do
35
+ param 'Target', :target
36
+ param 'String[1]', :type
37
+ param 'String[1]', :title
38
+ return_type 'Optional[ResourceInstance]'
39
+ end
40
+
41
+ def resource(target, type, title)
42
+ inventory = Puppet.lookup(:bolt_inventory)
43
+ executor = Puppet.lookup(:bolt_executor) { nil }
44
+ executor&.report_function_call(self.class.name)
45
+
46
+ inventory.resource(target, type, title)
47
+ end
48
+
49
+ def resource_from_string(target, type, title)
50
+ resource(target, type, title)
51
+ end
52
+ end
@@ -187,7 +187,11 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
187
187
  params.each_with_object({}) do |(name, value), acc|
188
188
  model = models[name]
189
189
 
190
- if sensitive_type?(model.type_expr)
190
+ # Parameters passed to a plan that the plan is not expecting don't have a model,
191
+ # so keep the parameter as-is.
192
+ if model.nil?
193
+ acc[name] = value
194
+ elsif sensitive_type?(model.type_expr)
191
195
  acc[name] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value)
192
196
  else
193
197
  if model.type_expr.to_s.include?('Sensitive')
@@ -21,7 +21,7 @@ module Bolt
21
21
 
22
22
  @inventory = inventory
23
23
  @executor = executor
24
- @modulepath = modulepath
24
+ @modulepath = modulepath || []
25
25
  @plugin_dirs = plugin_dirs
26
26
  @project = project
27
27
  @pdb_client = pdb_client
@@ -146,6 +146,7 @@ module Bolt
146
146
 
147
147
  def apply(args, apply_body, scope)
148
148
  raise(ArgumentError, 'apply requires a TargetSpec') if args.empty?
149
+ raise(ArgumentError, 'apply requires at least one statement in the apply block') if apply_body.nil?
149
150
  type0 = Puppet.lookup(:pal_script_compiler).type('TargetSpec')
150
151
  Puppet::Pal.assert_type(type0, args[0], 'apply targets')
151
152
 
@@ -70,6 +70,10 @@ module Bolt
70
70
  @targets[target.name].features
71
71
  end
72
72
 
73
+ def resource(target, type, title)
74
+ @targets[target.name].resource(type, title)
75
+ end
76
+
73
77
  def add_to_group(*_params)
74
78
  raise InvalidFunctionCall, 'add_to_group'
75
79
  end
@@ -62,6 +62,10 @@ module Bolt
62
62
  @safe_name
63
63
  end
64
64
 
65
+ def resource(type, title)
66
+ resources[Bolt::ResourceInstance.format_reference(type, title)]
67
+ end
68
+
65
69
  def parse_uri(string)
66
70
  require 'addressable/uri'
67
71
  if string.nil?
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/apply_inventory'
3
4
  require 'bolt/apply_target'
4
5
  require 'bolt/config'
5
6
  require 'bolt/error'
6
7
  require 'bolt/inventory'
7
- require 'bolt/apply_inventory'
8
8
  require 'bolt/pal'
9
9
  require 'bolt/puppetdb'
10
10
  require 'bolt/util'
@@ -19,7 +19,7 @@ module Bolt
19
19
  @log_level = log_level
20
20
  end
21
21
 
22
- def with_puppet_settings(hiera_config = {})
22
+ def with_puppet_settings(overrides = {})
23
23
  Dir.mktmpdir('bolt') do |dir|
24
24
  cli = []
25
25
  Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
@@ -31,7 +31,9 @@ module Bolt
31
31
  Puppet.settings.override_default(:vendormoduledir, '')
32
32
 
33
33
  Puppet.initialize_settings(cli)
34
- Puppet.settings[:hiera_config] = hiera_config
34
+ overrides.each do |setting, value|
35
+ Puppet.settings[setting] = value
36
+ end
35
37
 
36
38
  # Use a special logdest that serializes all log messages and their level to stderr.
37
39
  Puppet::Util::Log.newdestination(:stderr)
@@ -54,83 +56,51 @@ module Bolt
54
56
  end
55
57
 
56
58
  def compile_catalog(request)
57
- pal_main = request['code_ast'] || request['code_string']
58
- target = request['target']
59
59
  pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
60
- options = request['puppet_config'] || {}
61
60
  project = request['project'] || {}
62
61
  bolt_project = Struct.new(:name, :path).new(project['name'], project['path']) unless project.empty?
63
- with_puppet_settings(request['hiera_config']) do
64
- Puppet[:rich_data] = true
65
- Puppet[:node_name_value] = target['name']
66
- env_conf = { modulepath: request['modulepath'] || [],
67
- facts: target['facts'] || {} }
68
- env_conf[:variables] = {}
62
+ inv = Bolt::ApplyInventory.new(request['config'])
63
+ puppet_overrides = {
64
+ bolt_pdb_client: pdb_client,
65
+ bolt_inventory: inv,
66
+ bolt_project: bolt_project
67
+ }
68
+
69
+ # Facts will be set by the catalog compiler, so we need to ensure
70
+ # that any plan or target variables with the same name are not
71
+ # passed into the apply block to avoid a redefinition error.
72
+ # Filter out plan and target vars separately and raise a Puppet
73
+ # warning if there are any collisions for either. Puppet warning
74
+ # is the only way to log a message that will make it back to Bolt
75
+ # to be printed.
76
+ target = request['target']
77
+ plan_vars = shadow_vars('plan', request['plan_vars'], target['facts'])
78
+ target_vars = shadow_vars('target', target['variables'], target['facts'])
79
+ topscope_vars = target_vars.merge(plan_vars)
80
+ env_conf = { modulepath: request['modulepath'],
81
+ facts: target['facts'],
82
+ variables: topscope_vars }
83
+
84
+ puppet_settings = {
85
+ node_name_value: target['name'],
86
+ hiera_config: request['hiera_config']
87
+ }
88
+
89
+ with_puppet_settings(puppet_settings) do
69
90
  Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
70
- inv = Bolt::ApplyInventory.new(request['config'])
71
- Puppet.override(bolt_pdb_client: pdb_client,
72
- bolt_inventory: inv,
73
- bolt_project: bolt_project) do
91
+ Puppet.override(puppet_overrides) do
74
92
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
75
93
  pal.with_catalog_compiler do |compiler|
76
- # Deserializing needs to happen inside the catalog compiler so
77
- # loaders are initialized for loading
78
- plan_vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
79
-
80
- # Facts will be set by the catalog compiler, so we need to ensure
81
- # that any plan or target variables with the same name are not
82
- # passed into the apply block to avoid a redefinition error.
83
- # Filter out plan and target vars separately and raise a Puppet
84
- # warning if there are any collisions for either. Puppet warning
85
- # is the only way to log a message that will make it back to Bolt
86
- # to be printed.
87
- pv_collisions, pv_filtered = plan_vars.partition do |k, _|
88
- target['facts'].keys.include?(k)
89
- end.map(&:to_h)
90
- unless pv_collisions.empty?
91
- print_pv = pv_collisions.keys.map { |k| "$#{k}" }.join(', ')
92
- plural = pv_collisions.keys.length == 1 ? '' : 's'
93
- Puppet.warning("Plan variable#{plural} #{print_pv} will be overridden by fact#{plural} " \
94
- "of the same name in the apply block")
95
- end
96
-
97
- tv_collisions, tv_filtered = target['variables'].partition do |k, _|
98
- target['facts'].keys.include?(k)
99
- end.map(&:to_h)
100
- unless tv_collisions.empty?
101
- print_tv = tv_collisions.keys.map { |k| "$#{k}" }.join(', ')
102
- plural = tv_collisions.keys.length == 1 ? '' : 's'
103
- Puppet.warning("Target variable#{plural} #{print_tv} " \
104
- "will be overridden by fact#{plural} of the same name in the apply block")
105
- end
106
-
107
- pal.send(:add_variables, compiler.send(:topscope), tv_filtered.merge(pv_filtered))
108
-
94
+ options = request['puppet_config'] || {}
109
95
  # Configure language strictness in the CatalogCompiler. We want Bolt to be able
110
96
  # to compile most Puppet 4+ manifests, so we default to allowing deprecated functions.
111
97
  Puppet[:strict] = options['strict'] || :warning
112
98
  Puppet[:strict_variables] = options['strict_variables'] || false
113
- ast = Puppet::Pops::Serialization::FromDataConverter.convert(pal_main)
114
- # This will be a Program when running via `bolt apply`, but will
115
- # only be a subset of the AST when compiling an apply block in a
116
- # plan. In that case, we need to discover the definitions (which
117
- # would ordinarily be stored on the Program) and construct a Program object.
118
- unless ast.is_a?(Puppet::Pops::Model::Program)
119
- # Node definitions must be at the top level of the apply block.
120
- # That means the apply body either a) consists of just a
121
- # NodeDefinition, b) consists of a BlockExpression which may
122
- # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
123
- definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
124
- ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
125
- elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
126
- [ast]
127
- else
128
- []
129
- end
130
- ast = Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
131
- end
99
+
100
+ pal_main = request['code_ast'] || request['code_string']
101
+ ast = build_program(pal_main)
132
102
  compiler.evaluate(ast)
133
- compiler.instance_variable_get(:@internal_compiler).send(:evaluate_ast_node)
103
+ compiler.evaluate_ast_node
134
104
  compiler.compile_additions
135
105
  compiler.with_json_encoding(&:encode)
136
106
  end
@@ -138,5 +108,45 @@ module Bolt
138
108
  end
139
109
  end
140
110
  end
111
+
112
+ # Warn and remove variables that will be shadowed by facts of the same
113
+ # name, which are set in scope earlier.
114
+ def shadow_vars(type, vars, facts)
115
+ collisions, valid = vars.partition do |k, _|
116
+ facts.include?(k)
117
+ end
118
+ if collisions.any?
119
+ names = collisions.map { |k, _| "$#{k}" }.join(', ')
120
+ plural = collisions.length == 1 ? '' : 's'
121
+ Puppet.warning("#{type.capitalize} variable#{plural} #{names} will be overridden by fact#{plural} " \
122
+ "of the same name in the apply block")
123
+ end
124
+ valid.to_h
125
+ end
126
+
127
+ def build_program(code)
128
+ ast = Puppet::Pops::Serialization::FromDataConverter.convert(code)
129
+
130
+ # This will be a Program when running via `bolt apply`, but will
131
+ # only be a subset of the AST when compiling an apply block in a
132
+ # plan. In that case, we need to discover the definitions (which
133
+ # would ordinarily be stored on the Program) and construct a Program object.
134
+ if ast.is_a?(Puppet::Pops::Model::Program)
135
+ ast
136
+ else
137
+ # Node definitions must be at the top level of the apply block.
138
+ # That means the apply body either a) consists of just a
139
+ # NodeDefinition, b) consists of a BlockExpression which may
140
+ # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
141
+ definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
142
+ ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
143
+ elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
144
+ [ast]
145
+ else
146
+ []
147
+ end
148
+ Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
149
+ end
150
+ end
141
151
  end
142
152
  end
@@ -115,7 +115,7 @@ module Bolt
115
115
  Bolt::Config.from_file(options[:configfile], options)
116
116
  else
117
117
  project = if options[:boltdir]
118
- Bolt::Project.new(options[:boltdir])
118
+ Bolt::Project.create_project(options[:boltdir])
119
119
  else
120
120
  Bolt::Project.find_boltdir(Dir.pwd)
121
121
  end
@@ -392,7 +392,7 @@ module Bolt
392
392
  end
393
393
  code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
394
394
  else
395
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
395
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
396
396
  targets = options[:targets]
397
397
 
398
398
  results = nil
@@ -512,7 +512,7 @@ module Bolt
512
512
  params: plan_arguments }
513
513
  plan_context[:description] = options[:description] if options[:description]
514
514
 
515
- executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
515
+ executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
516
516
  if options.fetch(:format, 'human') == 'human'
517
517
  executor.subscribe(outputter)
518
518
  else
@@ -548,7 +548,7 @@ module Bolt
548
548
  @logger.warn(message)
549
549
  end
550
550
 
551
- executor = Bolt::Executor.new(config.concurrency, analytics, noop)
551
+ executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
552
552
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
553
553
  executor.subscribe(log_outputter)
554
554
  # apply logging looks like plan logging, so tell the outputter we're in a
@@ -771,7 +771,7 @@ module Bolt
771
771
  end
772
772
 
773
773
  def pal
774
- project = config.project.load_as_module? ? config.project : nil
774
+ project = config.project.project_file? ? config.project : nil
775
775
  @pal ||= Bolt::PAL.new(config.modulepath,
776
776
  config.hiera_config,
777
777
  config.project.resource_types,
@@ -23,7 +23,7 @@ module Bolt
23
23
  end
24
24
 
25
25
  class Config
26
- attr_reader :config_files, :warnings, :data, :transports, :project
26
+ attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency
27
27
 
28
28
  TRANSPORT_CONFIG = {
29
29
  'ssh' => Bolt::Config::Transport::SSH,
@@ -34,6 +34,13 @@ module Bolt
34
34
  'remote' => Bolt::Config::Transport::Remote
35
35
  }.freeze
36
36
 
37
+ TRANSPORT_OPTION = { 'transport' => 'The default transport to use when the '\
38
+ 'transport for a target is not specified in the URL.' }.freeze
39
+
40
+ DEFAULT_TRANSPORT_OPTION = { 'transport' => 'ssh' }.freeze
41
+
42
+ CONFIG_IN_INVENTORY = TRANSPORT_CONFIG.merge(TRANSPORT_OPTION)
43
+
37
44
  # NOTE: All configuration options should have a corresponding schema property
38
45
  # in schemas/bolt-config.schema.json
39
46
  OPTIONS = {
@@ -55,8 +62,8 @@ module Bolt
55
62
  "save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
56
63
  "your target names include passwords, set this value to `false` to avoid "\
57
64
  "writing passwords to disk.",
58
- "transport" => "The default transport to use when the transport for a target is not "\
59
- "specified in the URL or inventory.",
65
+ "transport" => "The default transport to use when the transport for a target is not specified "\
66
+ "in the URL or inventory.",
60
67
  "trusted-external-command" => "The path to an executable on the Bolt controller that can produce "\
61
68
  "external trusted facts. **External trusted facts are experimental in both "\
62
69
  "Puppet and Bolt and this API may change or be removed.**"
@@ -104,14 +111,17 @@ module Bolt
104
111
  DEFAULT_DEFAULT_CONCURRENCY = 100
105
112
 
106
113
  def self.default
107
- new(Bolt::Project.new('.'), {})
114
+ new(Bolt::Project.create_project('.'), {})
108
115
  end
109
116
 
110
117
  def self.from_project(project, overrides = {})
111
- data = {
112
- filepath: project.config_file,
113
- data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
114
- }
118
+ conf = if project.project_file == project.config_file
119
+ project.data
120
+ else
121
+ Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
122
+ end
123
+
124
+ data = { filepath: project.config_file, data: conf }
115
125
 
116
126
  data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
117
127
 
@@ -119,12 +129,13 @@ module Bolt
119
129
  end
120
130
 
121
131
  def self.from_file(configfile, overrides = {})
122
- project = Bolt::Project.new(Pathname.new(configfile).expand_path.dirname)
123
-
124
- data = {
125
- filepath: project.config_file,
126
- data: Bolt::Util.read_yaml_hash(configfile, 'config')
127
- }
132
+ project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
133
+ conf = if project.project_file == project.config_file
134
+ project.data
135
+ else
136
+ Bolt::Util.read_yaml_hash(configfile, 'config')
137
+ end
138
+ data = { filepath: project.config_file, data: conf }
128
139
  data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
129
140
 
130
141
  new(project, data, overrides)
@@ -158,8 +169,8 @@ module Bolt
158
169
  end
159
170
 
160
171
  @logger = Logging.logger[self]
161
- @warnings = []
162
172
  @project = project
173
+ @warnings = @project.warnings.dup
163
174
  @transports = {}
164
175
  @config_files = []
165
176
 
@@ -187,15 +198,9 @@ module Bolt
187
198
 
188
199
  # If we need to lower concurrency and concurrency is not configured
189
200
  ld_concurrency = loaded_data.map(&:keys).flatten.include?('concurrency')
190
- if default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
191
- !ld_concurrency &&
192
- !override_data.key?('concurrency')
193
- concurrency_warning = { option: 'concurrency',
194
- msg: "Concurrency will default to #{default_concurrency} because ulimit "\
195
- "is low: #{Etc.sysconf(Etc::SC_OPEN_MAX)}. Set concurrency with "\
196
- "'--concurrency', or set your ulimit with 'ulimit -n <limit>'" }
197
- @warnings << concurrency_warning
198
- end
201
+ @modified_concurrency = default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
202
+ !ld_concurrency &&
203
+ !override_data.key?('concurrency')
199
204
 
200
205
  @data = merge_config_layers(default_data, *loaded_data, override_data)
201
206
 
@@ -483,7 +488,7 @@ module Bolt
483
488
  @default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
484
489
  DEFAULT_DEFAULT_CONCURRENCY
485
490
  else
486
- Etc.sysconf(Etc::SC_OPEN_MAX) / 3
491
+ Etc.sysconf(Etc::SC_OPEN_MAX) / 7
487
492
  end
488
493
  end
489
494
  end
@@ -12,94 +12,132 @@ module Bolt
12
12
  # NOTE: All transport configuration options should have a corresponding schema definition
13
13
  # in schemas/bolt-transport-definitions.json
14
14
  OPTIONS = {
15
- "cleanup" => { type: TrueClass,
16
- external: true,
17
- desc: "Whether to clean up temporary files created on targets." },
18
- "connect-timeout" => { type: Integer,
19
- desc: "How long to wait when establishing connections." },
20
- "copy-command" => { external: true,
21
- desc: "Command to use when copying files using ssh-command. "\
22
- "Bolt runs `<copy-command> <src> <dest>`. **This option is experimental.**" },
23
- "disconnect-timeout" => { type: Integer,
24
- desc: "How long to wait before force-closing a connection." },
25
- "host" => { type: String,
26
- external: true,
27
- desc: "Host name." },
28
- "host-key-check" => { type: TrueClass,
29
- external: true,
30
- desc: "Whether to perform host key validation when connecting." },
31
- "extensions" => { type: Array,
32
- desc: "List of file extensions that are accepted for scripts or tasks on Windows. "\
33
- "Scripts with these file extensions rely on the target's file type "\
34
- "association to run. For example, if Python is installed on the system, "\
35
- "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
36
- "`.pp` are always allowed and run via hard-coded executables." },
37
- "interpreters" => { type: Hash,
38
- external: true,
39
- desc: "A map of an extension name to the absolute path of an executable, "\
40
- "enabling you to override the shebang defined in a task executable. The "\
41
- "extension can optionally be specified with the `.` character (`.py` and "\
42
- "`py` both map to a task executable `task.py`) and the extension is case "\
43
- "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
44
- "Bolt Ruby interpreter by default." },
45
- "load-config" => { type: TrueClass,
46
- desc: "Whether to load system SSH configuration." },
47
- "login-shell" => { type: String,
48
- desc: "Which login shell Bolt should expect on the target. "\
49
- "Supported shells are #{LOGIN_SHELLS.join(', ')}. "\
50
- "**This option is experimental.**" },
51
- "password" => { type: String,
52
- desc: "Login password." },
53
- "port" => { type: Integer,
54
- external: true,
55
- desc: "Connection port." },
56
- "private-key" => { external: true,
57
- desc: "Either the path to the private key file to use for authentication, or a "\
58
- "hash with the key `key-data` and the contents of the private key." },
59
- "proxyjump" => { type: String,
60
- desc: "A jump host to proxy connections through, and an optional user to "\
61
- "connect with." },
62
- "run-as" => { type: String,
63
- external: true,
64
- desc: "A different user to run commands as after login." },
65
- "run-as-command" => { type: Array,
66
- external: true,
67
- desc: "The command to elevate permissions. Bolt appends the user and command "\
68
- "strings to the configured `run-as-command` before running it on the "\
69
- "target. This command must not require an interactive password prompt, "\
70
- "and the `sudo-password` option is ignored when `run-as-command` is "\
71
- "specified. The `run-as-command` must be specified as an array." },
72
- "script-dir" => { type: String,
73
- external: true,
74
- desc: "The subdirectory of the tmpdir to use in place of a randomized "\
75
- "subdirectory for uploading and executing temporary files on the "\
76
- "target. It's expected that this directory already exists as a subdir "\
77
- "of tmpdir, which is either configured or defaults to `/tmp`." },
78
- "ssh-command" => { external: true,
79
- desc: "Command and flags to use when SSHing. This enables the external "\
80
- "SSH transport which shells out to the specified command. "\
81
- "**This option is experimental.**" },
82
- "sudo-executable" => { type: String,
83
- external: true,
84
- desc: "The executable to use when escalating to the configured `run-as` "\
85
- "user. This is useful when you want to escalate using the configured "\
86
- "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
87
- "or support prompting. The command executed on the target is "\
88
- "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
89
- "**This option is experimental.**" },
90
- "sudo-password" => { type: String,
91
- external: true,
92
- desc: "Password to use when changing users via `run-as`." },
93
- "tmpdir" => { type: String,
94
- external: true,
95
- desc: "The directory to upload and execute temporary files on the target." },
96
- "tty" => { type: TrueClass,
97
- desc: "Request a pseudo tty for the session. This option is generally "\
98
- "only used in conjunction with the `run-as` option when the sudoers "\
99
- "policy requires a `tty`." },
100
- "user" => { type: String,
101
- external: true,
102
- desc: "Login user." }
15
+ "cleanup" => { type: TrueClass,
16
+ external: true,
17
+ desc: "Whether to clean up temporary files created on targets." },
18
+ "connect-timeout" => { type: Integer,
19
+ desc: "How long to wait when establishing connections." },
20
+ "copy-command" => { external: true,
21
+ desc: "Command to use when copying files using ssh-command. "\
22
+ "Bolt runs `<copy-command> <src> <dest>`. **This option is "\
23
+ "experimental.**" },
24
+ "disconnect-timeout" => { type: Integer,
25
+ desc: "How long to wait before force-closing a connection." },
26
+ "encryption-algorithms" => { type: Array,
27
+ desc: "List of encryption algorithms to use when establishing a "\
28
+ "connection with a target. Supported algorithms are "\
29
+ "defined by the Ruby net-ssh library and can be viewed "\
30
+ "[here](https://github.com/net-ssh/net-ssh#supported-algorithms). "\
31
+ "All supported, non-deprecated algorithms are available by default when "\
32
+ "this option is not used. To reference all default algorithms "\
33
+ "when using this option, add 'defaults' to the list of supported "\
34
+ "algorithms." },
35
+ "extensions" => { type: Array,
36
+ desc: "List of file extensions that are accepted for scripts or tasks on "\
37
+ "Windows. Scripts with these file extensions rely on the target's file "\
38
+ "type association to run. For example, if Python is installed on the "\
39
+ "system, a `.py` script runs with `python.exe`. The extensions `.ps1`, "\
40
+ "`.rb`, and `.pp` are always allowed and run via hard-coded "\
41
+ "executables." },
42
+ "host" => { type: String,
43
+ external: true,
44
+ desc: "Host name." },
45
+ "host-key-algorithms" => { type: Array,
46
+ desc: "List of host key algorithms to use when establishing a connection "\
47
+ "with a target. Supported algorithms are defined by the Ruby net-ssh "\
48
+ "library "\
49
+ "([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
50
+ "All supported, non-deprecated algorithms are available by default when "\
51
+ "this option is not used. To reference all default algorithms "\
52
+ "using this option, add 'defaults' to the list of supported "\
53
+ "algorithms." },
54
+ "host-key-check" => { type: TrueClass,
55
+ external: true,
56
+ desc: "Whether to perform host key validation when connecting." },
57
+ "interpreters" => { type: Hash,
58
+ external: true,
59
+ desc: "A map of an extension name to the absolute path of an executable, "\
60
+ "enabling you to override the shebang defined in a task executable. "\
61
+ "The extension can optionally be specified with the `.` character "\
62
+ "(`.py` and `py` both map to a task executable `task.py`) and the "\
63
+ "extension is case sensitive. When a target's name is `localhost`, "\
64
+ "Ruby tasks run with the Bolt Ruby interpreter by default." },
65
+ "kex-algorithms" => { type: Array,
66
+ desc: "List of key exchange algorithms to use when establishing a "\
67
+ "connection to a target. Supported algorithms are defined by the "\
68
+ "Ruby net-ssh library "\
69
+ "([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
70
+ "All supported, non-deprecated algorithms are available by default when "\
71
+ "this option is not used. To reference all default algorithms "\
72
+ "using this option, add 'defaults' to the list of supported "\
73
+ "algorithms." },
74
+ "load-config" => { type: TrueClass,
75
+ desc: "Whether to load system SSH configuration." },
76
+ "login-shell" => { type: String,
77
+ desc: "Which login shell Bolt should expect on the target. "\
78
+ "Supported shells are #{LOGIN_SHELLS.join(', ')}. "\
79
+ "**This option is experimental.**" },
80
+ "mac-algorithms" => { type: Array,
81
+ desc: "List of message authentication code algorithms to use when "\
82
+ "establishing a connection to a target. Supported algorithms are "\
83
+ "defined by the Ruby net-ssh library "\
84
+ "([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
85
+ "All supported, non-deprecated algorithms are available by default when "\
86
+ "this option is not used. To reference all default algorithms "\
87
+ "using this option, add 'defaults' to the list of supported "\
88
+ "algorithms." },
89
+ "password" => { type: String,
90
+ desc: "Login password." },
91
+ "port" => { type: Integer,
92
+ external: true,
93
+ desc: "Connection port." },
94
+ "private-key" => { external: true,
95
+ desc: "Either the path to the private key file to use for authentication, or "\
96
+ "a hash with the key `key-data` and the contents of the private key." },
97
+ "proxyjump" => { type: String,
98
+ desc: "A jump host to proxy connections through, and an optional user to "\
99
+ "connect with." },
100
+ "run-as" => { type: String,
101
+ external: true,
102
+ desc: "A different user to run commands as after login." },
103
+ "run-as-command" => { type: Array,
104
+ external: true,
105
+ desc: "The command to elevate permissions. Bolt appends the user and command "\
106
+ "strings to the configured `run-as-command` before running it on the "\
107
+ "target. This command must not require an interactive password prompt, "\
108
+ "and the `sudo-password` option is ignored when `run-as-command` is "\
109
+ "specified. The `run-as-command` must be specified as an array." },
110
+ "script-dir" => { type: String,
111
+ external: true,
112
+ desc: "The subdirectory of the tmpdir to use in place of a randomized "\
113
+ "subdirectory for uploading and executing temporary files on the "\
114
+ "target. It's expected that this directory already exists as a subdir "\
115
+ "of tmpdir, which is either configured or defaults to `/tmp`." },
116
+ "ssh-command" => { external: true,
117
+ desc: "Command and flags to use when SSHing. This enables the external "\
118
+ "SSH transport which shells out to the specified command. "\
119
+ "**This option is experimental.**" },
120
+ "sudo-executable" => { type: String,
121
+ external: true,
122
+ desc: "The executable to use when escalating to the configured `run-as` "\
123
+ "user. This is useful when you want to escalate using the configured "\
124
+ "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
125
+ "or support prompting. The command executed on the target is "\
126
+ "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
127
+ "**This option is experimental.**" },
128
+ "sudo-password" => { type: String,
129
+ external: true,
130
+ desc: "Password to use when changing users via `run-as`." },
131
+ "tmpdir" => { type: String,
132
+ external: true,
133
+ desc: "The directory to upload and execute temporary files on the target." },
134
+ "tty" => { type: TrueClass,
135
+ desc: "Request a pseudo tty for the session. This option is generally "\
136
+ "only used in conjunction with the `run-as` option when the sudoers "\
137
+ "policy requires a `tty`." },
138
+ "user" => { type: String,
139
+ external: true,
140
+ desc: "Login user." }
103
141
  }.freeze
104
142
 
105
143
  DEFAULTS = {
@@ -142,10 +180,11 @@ module Bolt
142
180
  "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
143
181
  end
144
182
 
145
- if (run_as_cmd = @config['run-as-command'])
146
- unless run_as_cmd.all? { |n| n.is_a?(String) }
183
+ %w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
184
+ next unless @config.key?(opt)
185
+ unless @config[opt].all? { |n| n.is_a?(String) }
147
186
  raise Bolt::ValidationError,
148
- "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
187
+ "#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
149
188
  end
150
189
  end
151
190
 
@@ -34,7 +34,8 @@ module Bolt
34
34
 
35
35
  def initialize(concurrency = 1,
36
36
  analytics = Bolt::Analytics::NoopClient.new,
37
- noop = false)
37
+ noop = false,
38
+ modified_concurrency = false)
38
39
  # lazy-load expensive gem code
39
40
  require 'concurrent'
40
41
 
@@ -64,6 +65,9 @@ module Bolt
64
65
  Concurrent.global_immediate_executor
65
66
  end
66
67
  @logger.debug { "Started with #{concurrency} max thread(s)" }
68
+
69
+ @concurrency = concurrency
70
+ @warn_concurrency = modified_concurrency
67
71
  end
68
72
 
69
73
  def transport(transport)
@@ -102,6 +106,15 @@ module Bolt
102
106
  # defined by the transport. Yields each batch, along with the corresponding
103
107
  # transport, to the block in turn and returns an array of result promises.
104
108
  def queue_execute(targets)
109
+ if @warn_concurrency && targets.length > @concurrency
110
+ @warn_concurrency = false
111
+ @logger.warn("The ulimit is low, which may cause file limit issues. Default concurrency has been set to "\
112
+ "'#{@concurrency}' to mitigate those issues, which may cause Bolt to run slow. "\
113
+ "Disable this warning by configuring ulimit using 'ulimit -n <limit>' in your shell "\
114
+ "configuration, or by configuring Bolt's concurrency. "\
115
+ "See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details.")
116
+ end
117
+
105
118
  targets.group_by(&:transport).flat_map do |protocol, protocol_targets|
106
119
  transport = transport(protocol)
107
120
  report_transport(transport, protocol_targets.count)
@@ -16,7 +16,7 @@ module Bolt
16
16
  DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
17
17
  TARGET_KEYS = DATA_KEYS + %w[name alias uri]
18
18
  GROUP_KEYS = DATA_KEYS + %w[name groups targets]
19
- CONFIG_KEYS = Bolt::Config::TRANSPORT_CONFIG.keys + ['transport']
19
+ CONFIG_KEYS = Bolt::Config::CONFIG_IN_INVENTORY.keys
20
20
 
21
21
  def initialize(input, plugins)
22
22
  @logger = Logging.logger[self]
@@ -323,6 +323,10 @@ module Bolt
323
323
  def resources(target)
324
324
  @targets[target.name].resources
325
325
  end
326
+
327
+ def resource(target, type, title)
328
+ @targets[target.name].resource(type, title)
329
+ end
326
330
  end
327
331
  end
328
332
  end
@@ -90,6 +90,10 @@ module Bolt
90
90
  end
91
91
  end
92
92
 
93
+ def resource(type, title)
94
+ resources[Bolt::ResourceInstance.format_reference(type, title)]
95
+ end
96
+
93
97
  def plugin_hooks
94
98
  # Merge plugin_hooks from the config file with any defined by the group
95
99
  # or assigned dynamically to the target
@@ -12,14 +12,14 @@ module Bolt
12
12
  "tasks" => "An array of task names to whitelist. Whitelisted plans are included in `bolt task show` output"
13
13
  }.freeze
14
14
 
15
- attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
16
- :puppetfile, :rerunfile, :type, :resource_types
15
+ attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
16
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
17
17
 
18
18
  def self.default_project
19
- Project.new(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
19
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
20
20
  # If homedir isn't defined use the system config path
21
21
  rescue ArgumentError
22
- Project.new(system_path, 'system')
22
+ create_project(system_path, 'system')
23
23
  end
24
24
 
25
25
  def self.system_path
@@ -37,9 +37,9 @@ module Bolt
37
37
  def self.find_boltdir(dir)
38
38
  dir = Pathname.new(dir)
39
39
  if (dir + BOLTDIR_NAME).directory?
40
- new(dir + BOLTDIR_NAME, 'embedded')
40
+ create_project(dir + BOLTDIR_NAME, 'embedded')
41
41
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
42
- new(dir, 'local')
42
+ create_project(dir, 'local')
43
43
  elsif dir.root?
44
44
  default_project
45
45
  else
@@ -47,9 +47,25 @@ module Bolt
47
47
  end
48
48
  end
49
49
 
50
- def initialize(path, type = 'option')
50
+ def self.create_project(path, type = 'option')
51
+ fullpath = Pathname.new(path).expand_path
52
+ project_file = File.join(fullpath, 'bolt-project.yaml')
53
+ data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
54
+ new(data, path, type)
55
+ end
56
+
57
+ def initialize(raw_data, path, type = 'option')
51
58
  @path = Pathname.new(path).expand_path
52
- @config_file = @path + 'bolt.yaml'
59
+ @project_file = @path + 'bolt-project.yaml'
60
+
61
+ @warnings = []
62
+ if (@path + 'bolt.yaml').file? && project_file?
63
+ msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
64
+ "Transport config should be set in inventory.yaml, all other config should be set in "\
65
+ "bolt-project.yaml."
66
+ @warnings << { msg: msg }
67
+ end
68
+
53
69
  @inventory_file = @path + 'inventory.yaml'
54
70
  @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
55
71
  @hiera_config = @path + 'hiera.yaml'
@@ -58,9 +74,26 @@ module Bolt
58
74
  @resource_types = @path + '.resource_types'
59
75
  @type = type
60
76
 
61
- @project_file = @path + 'bolt-project.yaml'
62
- @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
63
- validate if load_as_module?
77
+ tc = Bolt::Config::CONFIG_IN_INVENTORY.keys & raw_data.keys
78
+ if tc.any?
79
+ msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
80
+ @warnings << { msg: msg }
81
+ end
82
+
83
+ @data = raw_data.reject { |k, _| Bolt::Config::CONFIG_IN_INVENTORY.keys.include?(k) }
84
+
85
+ # Once bolt.yaml deprecation is removed, this attribute should be removed
86
+ # and replaced with .project_file in lib/bolt/config.rb
87
+ @config_file = if (Bolt::Config::OPTIONS.keys & @data.keys).any?
88
+ if (@path + 'bolt.yaml').file?
89
+ msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
90
+ @warnings << { msg: msg }
91
+ end
92
+ @project_file
93
+ else
94
+ @path + 'bolt.yaml'
95
+ end
96
+ validate if project_file?
64
97
  end
65
98
 
66
99
  def to_s
@@ -78,7 +111,7 @@ module Bolt
78
111
  end
79
112
  alias == eql?
80
113
 
81
- def load_as_module?
114
+ def project_file?
82
115
  @project_file.file?
83
116
  end
84
117
 
@@ -83,8 +83,12 @@ module Bolt
83
83
  to_hash.to_json(opts)
84
84
  end
85
85
 
86
+ def self.format_reference(type, title)
87
+ "#{type.capitalize}[#{title}]"
88
+ end
89
+
86
90
  def reference
87
- "#{type}[#{title}]"
91
+ self.class.format_reference(@type, @title)
88
92
  end
89
93
  alias to_s reference
90
94
 
@@ -20,6 +20,14 @@ module Bolt
20
20
  PS
21
21
  end
22
22
 
23
+ def exit_with_code(command)
24
+ <<~PS
25
+ #{command}
26
+ if (-not $? -and ($LASTEXITCODE -eq $null)) { exit 1 }
27
+ exit $LASTEXITCODE
28
+ PS
29
+ end
30
+
23
31
  def make_tmpdir(parent)
24
32
  <<~PS
25
33
  $parent = #{parent}
@@ -50,7 +50,8 @@ module Bolt
50
50
  # If it's already a powershell command then invoke it normally.
51
51
  # Otherwise, wrap it in powershell.exe.
52
52
  unless command.start_with?('powershell.exe')
53
- command = ['powershell.exe', *Bolt::Shell::Powershell::PS_ARGS, '-Command', command]
53
+ cmd = Bolt::Shell::Powershell::Snippets.exit_with_code(command)
54
+ command = ['powershell.exe', *Bolt::Shell::Powershell::PS_ARGS, '-Command', cmd]
54
55
  end
55
56
  end
56
57
 
@@ -78,6 +78,28 @@ module Bolt
78
78
 
79
79
  options[:proxy] = Net::SSH::Proxy::Jump.new(target.options['proxyjump']) if target.options['proxyjump']
80
80
 
81
+ # Override the default supported algorithms for net-ssh. By default, a subset of supported algorithms
82
+ # are enabled in 6.x, while several are deprecated and not enabled by default. The *-algorithms
83
+ # options can be used to specify a list of algorithms to enable in net-ssh. Any algorithms not in the
84
+ # list are disabled, including ones that are normally enabled by default. Support for deprecated
85
+ # algorithms will be removed in 7.x.
86
+ # https://github.com/net-ssh/net-ssh#supported-algorithms
87
+ if target.options['encryption-algorithms']
88
+ options[:encryption] = net_ssh_algorithms(:encryption, target.options['encryption-algorithms'])
89
+ end
90
+
91
+ if target.options['host-key-algorithms']
92
+ options[:host_key] = net_ssh_algorithms(:host_key, target.options['host-key-algorithms'])
93
+ end
94
+
95
+ if target.options['kex-algorithms']
96
+ options[:kex] = net_ssh_algorithms(:kex, target.options['kex-algorithms'])
97
+ end
98
+
99
+ if target.options['mac-algorithms']
100
+ options[:hmac] = net_ssh_algorithms(:hmac, target.options['mac-algorithms'])
101
+ end
102
+
81
103
  # This option was to address discrepency betwen net-ssh host-key-check and ssh(1)
82
104
  # For the net-ssh 5.x series it defaults to true, in 6.x it will default to false, and will be removed in 7.x
83
105
  # https://github.com/net-ssh/net-ssh/pull/663#issuecomment-469979931
@@ -246,6 +268,19 @@ module Bolt
246
268
  end
247
269
  end
248
270
 
271
+ # Add all default algorithms if the 'defaults' key is present and filter
272
+ # out any unsupported algorithms.
273
+ def net_ssh_algorithms(type, algorithms)
274
+ if algorithms.include?('defaults')
275
+ defaults = Net::SSH::Transport::Algorithms::DEFAULT_ALGORITHMS[type]
276
+ algorithms += defaults
277
+ end
278
+
279
+ known = Net::SSH::Transport::Algorithms::ALGORITHMS[type]
280
+
281
+ algorithms & known
282
+ end
283
+
249
284
  def shell
250
285
  @shell ||= if target.options['login-shell'] == 'powershell'
251
286
  Bolt::Shell::Powershell.new(target, self)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.13.0'
4
+ VERSION = '2.14.0'
5
5
  end
@@ -138,7 +138,7 @@ module BoltSpec
138
138
  # Override in your tests
139
139
  def config
140
140
  @config ||= begin
141
- conf = Bolt::Config.new(Bolt::Project.new('.'), {})
141
+ conf = Bolt::Config.default
142
142
  conf.modulepath = [modulepath].flatten
143
143
  conf
144
144
  end
@@ -144,7 +144,7 @@ module BoltSpec
144
144
  end
145
145
 
146
146
  def config
147
- @config ||= Bolt::Config.new(Bolt::Project.new(@project_path), @config_data)
147
+ @config ||= Bolt::Config.new(Bolt::Project.create_project(@project_path), @config_data)
148
148
  end
149
149
 
150
150
  def inventory
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: 2.13.0
4
+ version: 2.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-08 00:00:00.000000000 Z
11
+ date: 2020-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -184,7 +184,7 @@ dependencies:
184
184
  requirements:
185
185
  - - ">="
186
186
  - !ruby/object:Gem::Version
187
- version: 6.15.0
187
+ version: 6.16.0
188
188
  - - "<"
189
189
  - !ruby/object:Gem::Version
190
190
  version: '7'
@@ -194,7 +194,7 @@ dependencies:
194
194
  requirements:
195
195
  - - ">="
196
196
  - !ruby/object:Gem::Version
197
- version: 6.15.0
197
+ version: 6.16.0
198
198
  - - "<"
199
199
  - !ruby/object:Gem::Version
200
200
  version: '7'
@@ -407,6 +407,7 @@ files:
407
407
  - bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb
408
408
  - bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb
409
409
  - bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb
410
+ - bolt-modules/boltlib/lib/puppet/functions/resource.rb
410
411
  - bolt-modules/boltlib/lib/puppet/functions/run_command.rb
411
412
  - bolt-modules/boltlib/lib/puppet/functions/run_plan.rb
412
413
  - bolt-modules/boltlib/lib/puppet/functions/run_script.rb
@@ -588,7 +589,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
588
589
  - !ruby/object:Gem::Version
589
590
  version: '0'
590
591
  requirements: []
591
- rubygems_version: 3.0.6
592
+ rubygems_version: 3.0.8
592
593
  signing_key:
593
594
  specification_version: 4
594
595
  summary: Execute commands remotely over SSH and WinRM