bolt 2.35.0 → 2.36.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  3. data/lib/bolt/analytics.rb +27 -8
  4. data/lib/bolt/apply_result.rb +3 -3
  5. data/lib/bolt/bolt_option_parser.rb +38 -15
  6. data/lib/bolt/cli.rb +13 -87
  7. data/lib/bolt/config.rb +131 -52
  8. data/lib/bolt/config/options.rb +42 -4
  9. data/lib/bolt/config/transport/base.rb +10 -19
  10. data/lib/bolt/config/transport/local.rb +0 -7
  11. data/lib/bolt/config/transport/ssh.rb +8 -14
  12. data/lib/bolt/config/validator.rb +231 -0
  13. data/lib/bolt/executor.rb +5 -17
  14. data/lib/bolt/outputter/rainbow.rb +1 -1
  15. data/lib/bolt/plugin.rb +0 -7
  16. data/lib/bolt/project.rb +30 -36
  17. data/lib/bolt/project_manager.rb +199 -0
  18. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +41 -4
  19. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  20. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  21. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  22. data/lib/bolt/puppetdb/config.rb +1 -2
  23. data/lib/bolt/shell/bash.rb +1 -1
  24. data/lib/bolt/task/run.rb +1 -1
  25. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  26. data/lib/bolt/util.rb +14 -7
  27. data/lib/bolt/version.rb +1 -1
  28. data/lib/bolt_server/base_config.rb +3 -1
  29. data/lib/bolt_server/config.rb +3 -1
  30. data/lib/bolt_server/schemas/partials/task.json +2 -2
  31. data/lib/bolt_server/transport_app.rb +5 -5
  32. data/libexec/apply_catalog.rb +1 -1
  33. data/libexec/custom_facts.rb +1 -1
  34. data/libexec/query_resources.rb +1 -1
  35. metadata +8 -13
  36. data/lib/bolt/project_migrator.rb +0 -80
@@ -256,10 +256,10 @@ module Bolt
256
256
  @logger.trace { "Failed to submit analytics event: #{e.message}" }
257
257
  end
258
258
 
259
- def with_node_logging(description, batch)
260
- @logger.info("#{description} on #{batch.map(&:safe_name)}")
259
+ def with_node_logging(description, batch, log_level = :info)
260
+ @logger.send(log_level, "#{description} on #{batch.map(&:safe_name)}")
261
261
  result = yield
262
- @logger.info(result.to_json)
262
+ @logger.send(log_level, result.to_json)
263
263
  result
264
264
  end
265
265
 
@@ -289,32 +289,20 @@ module Bolt
289
289
  end
290
290
  end
291
291
 
292
- def run_task(targets, task, arguments, options = {}, position = [])
292
+ def run_task(targets, task, arguments, options = {}, position = [], log_level = :info)
293
293
  description = options.fetch(:description, "task #{task.name}")
294
294
  log_action(description, targets) do
295
295
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
296
296
  arguments['_task'] = task.name
297
297
 
298
298
  batch_execute(targets) do |transport, batch|
299
- with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
299
+ with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch, log_level) do
300
300
  transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
301
301
  end
302
302
  end
303
303
  end
304
304
  end
305
305
 
306
- def run_task_with_minimal_logging(targets, task, arguments, options = {})
307
- description = options.fetch(:description, "task #{task.name}")
308
- log_action(description, targets) do
309
- options[:run_as] = run_as if run_as && !options.key?(:run_as)
310
- arguments['_task'] = task.name
311
-
312
- batch_execute(targets) do |transport, batch|
313
- transport.batch_task(batch, task, arguments, options, [], &method(:publish_event))
314
- end
315
- end
316
- end
317
-
318
306
  def run_task_with(target_mapping, task, options = {}, position = [])
319
307
  targets = target_mapping.keys
320
308
  description = options.fetch(:description, "task #{task.name}")
@@ -53,7 +53,7 @@ module Bolt
53
53
  @state = :normal if c == 'm'
54
54
  end
55
55
  end
56
- a.join('')
56
+ a.join
57
57
  else
58
58
  "\033[#{COLORS[color]}m#{string}\033[0m"
59
59
  end
@@ -122,13 +122,6 @@ module Bolt
122
122
  def self.setup(config, pal, analytics = Bolt::Analytics::NoopClient.new)
123
123
  plugins = new(config, pal, analytics)
124
124
 
125
- # Initialize any plugins referenced in plugin config. This will also indirectly
126
- # initialize any plugins they depend on.
127
- if plugins.reference?(config.plugins)
128
- msg = "The 'plugins' setting cannot be set by a plugin reference"
129
- raise PluginError.new(msg, 'bolt/plugin-error')
130
- end
131
-
132
125
  config.plugins.each_key do |plugin|
133
126
  plugins.by_name(plugin)
134
127
  end
@@ -2,19 +2,14 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'bolt/config'
5
+ require 'bolt/config/validator'
5
6
  require 'bolt/pal'
6
7
  require 'bolt/module'
7
8
 
8
9
  module Bolt
9
10
  class Project
10
11
  BOLTDIR_NAME = 'Boltdir'
11
- PROJECT_SETTINGS = {
12
- "name" => "The name of the project",
13
- "plans" => "An array of plan names to show, if they exist in the project."\
14
- "These plans are included in `bolt plan show` and `Get-BoltPlan` output",
15
- "tasks" => "An array of task names to show, if they exist in the project."\
16
- "These tasks are included in `bolt task show` and `Get-BoltTask` output"
17
- }.freeze
12
+ CONFIG_NAME = 'bolt-project.yaml'
18
13
 
19
14
  attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
20
15
  :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
@@ -37,7 +32,7 @@ module Bolt
37
32
 
38
33
  if (dir + BOLTDIR_NAME).directory?
39
34
  create_project(dir + BOLTDIR_NAME, 'embedded', logs)
40
- elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
35
+ elsif (dir + 'bolt.yaml').file? || (dir + CONFIG_NAME).file?
41
36
  create_project(dir, 'local', logs)
42
37
  elsif dir.root?
43
38
  default_project(logs)
@@ -73,21 +68,37 @@ module Bolt
73
68
  )
74
69
  end
75
70
 
76
- project_file = File.join(fullpath, 'bolt-project.yaml')
77
- data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
78
- default = type =~ /user|system/ ? 'default ' : ''
79
- exist = File.exist?(File.expand_path(project_file))
71
+ project_file = File.join(fullpath, CONFIG_NAME)
72
+ data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
73
+ default = type =~ /user|system/ ? 'default ' : ''
74
+ exist = File.exist?(File.expand_path(project_file))
75
+ deprecations = []
76
+
80
77
  logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
81
- new(data, path, type, logs)
82
- end
83
78
 
84
- def initialize(raw_data, path, type = 'option', logs = [])
85
- @path = Pathname.new(path).expand_path
79
+ # Validate the config against the schema. This will raise a single error
80
+ # with all validation errors.
81
+ schema = Bolt::Config::OPTIONS.slice(*Bolt::Config::BOLT_PROJECT_OPTIONS)
82
+
83
+ Bolt::Config::Validator.new.tap do |validator|
84
+ validator.validate(data, schema, project_file)
85
+
86
+ validator.warnings.each { |warning| logs << { warn: warning } }
87
+
88
+ validator.deprecations.each do |dep|
89
+ deprecations << { type: "#{CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
90
+ end
91
+ end
92
+
93
+ new(data, path, type, logs, deprecations)
94
+ end
86
95
 
87
- @project_file = @path + 'bolt-project.yaml'
96
+ def initialize(raw_data, path, type = 'option', logs = [], deprecations = [])
97
+ @path = Pathname.new(path).expand_path
98
+ @project_file = @path + CONFIG_NAME
99
+ @logs = logs
100
+ @deprecations = deprecations
88
101
 
89
- @logs = logs
90
- @deprecations = []
91
102
  if (@path + 'bolt.yaml').file? && project_file?
92
103
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
93
104
  "Transport config should be set in inventory.yaml, all other config should be set in "\
@@ -198,23 +209,6 @@ module Bolt
198
209
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
199
210
  @logs << { warn: message }
200
211
  end
201
-
202
- %w[tasks plans].each do |conf|
203
- unless @data.fetch(conf, []).is_a?(Array)
204
- raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
205
- end
206
- end
207
-
208
- if @data['modules']
209
- unless @data['modules'].is_a?(Array)
210
- raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
211
- end
212
-
213
- @data['modules'].each do |spec|
214
- next if spec.is_a?(Hash) || spec.is_a?(String)
215
- raise Bolt::ValidationError, "Module specification #{spec.inspect} must be a hash or string"
216
- end
217
- end
218
212
  end
219
213
 
220
214
  def check_deprecated_file
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/project_manager/config_migrator'
4
+ require 'bolt/project_manager/inventory_migrator'
5
+ require 'bolt/project_manager/module_migrator'
6
+
7
+ module Bolt
8
+ class ProjectManager
9
+ INVENTORY_TEMPLATE = <<~INVENTORY
10
+ # This is an example inventory.yaml
11
+ # To read more about inventory files, see https://pup.pt/bolt-inventory
12
+ #
13
+ # groups:
14
+ # - name: linux
15
+ # targets:
16
+ # - target1.example.com
17
+ # - target2.example.com
18
+ # config:
19
+ # transport: ssh
20
+ # ssh:
21
+ # private-key: /path/to/private_key.pem
22
+ # - name: windows
23
+ # targets:
24
+ # - name: win1
25
+ # uri: target3.example.com
26
+ # - name: win2
27
+ # uri: target4.example.com
28
+ # config:
29
+ # transport: winrm
30
+ # config:
31
+ # ssh:
32
+ # host-key-check: false
33
+ # winrm:
34
+ # user: Administrator
35
+ # password: Bolt!
36
+ # ssl: false
37
+ INVENTORY
38
+
39
+ def initialize(config, outputter, pal)
40
+ @config = config
41
+ @outputter = outputter
42
+ @pal = pal
43
+ end
44
+
45
+ # Creates a new project at the specified directory.
46
+ #
47
+ def create(path, name, modules)
48
+ require 'bolt/module_installer'
49
+
50
+ project = Pathname.new(File.expand_path(path))
51
+ old_config = project + 'bolt.yaml'
52
+ config = project + 'bolt-project.yaml'
53
+ puppetfile = project + 'Puppetfile'
54
+ moduledir = project + '.modules'
55
+ inventoryfile = project + 'inventory.yaml'
56
+ project_name = name || File.basename(project)
57
+
58
+ if config.exist?
59
+ if modules
60
+ command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
61
+ raise Bolt::Error.new(
62
+ "Found existing project directory with #{config.basename} at #{project}, "\
63
+ "unable to initialize project with modules. To add modules to the project, "\
64
+ "run '#{command} <module>' instead.",
65
+ 'bolt/existing-project-error'
66
+ )
67
+ else
68
+ raise Bolt::Error.new(
69
+ "Found existing project directory with #{config.basename} at #{project}, "\
70
+ "unable to initialize project.",
71
+ 'bolt/existing-project-error'
72
+ )
73
+ end
74
+ elsif old_config.exist?
75
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
76
+ raise Bolt::Error.new(
77
+ "Found existing project directory with #{old_config.basename} at #{project}, "\
78
+ "unable to initialize project. #{old_config.basename} is deprecated. To "\
79
+ "update the project to current best practices, run '#{command}'.",
80
+ 'bolt/existing-project-error'
81
+ )
82
+ elsif modules && puppetfile.exist?
83
+ raise Bolt::Error.new(
84
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize project "\
85
+ "with modules.",
86
+ 'bolt/existing-puppetfile-error'
87
+ )
88
+ elsif project_name !~ Bolt::Module::MODULE_NAME_REGEX
89
+ if name
90
+ raise Bolt::ValidationError,
91
+ "The provided project name '#{project_name}' is invalid; project name must "\
92
+ "begin with a lowercase letter and can include lowercase letters, "\
93
+ "numbers, and underscores."
94
+ else
95
+ command = Bolt::Util.powershell? ? 'New-BoltProject -Name' : 'bolt project init'
96
+ raise Bolt::ValidationError,
97
+ "The current directory name '#{project_name}' is an invalid project name. "\
98
+ "Please specify a name using '#{command} <name>'."
99
+ end
100
+ end
101
+
102
+ # If modules were specified, resolve and install first. We want to error
103
+ # early here and not initialize the project if the modules cannot be
104
+ # resolved and installed.
105
+ if modules
106
+ Bolt::ModuleInstaller.new(@outputter, @pal).install(modules, puppetfile, moduledir)
107
+ end
108
+
109
+ data = { 'name' => project_name }
110
+ data['modules'] = modules || []
111
+
112
+ begin
113
+ File.write(config.to_path, data.to_yaml)
114
+ rescue StandardError => e
115
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
116
+ end
117
+
118
+ unless inventoryfile.exist?
119
+ begin
120
+ File.write(inventoryfile.to_path, INVENTORY_TEMPLATE)
121
+ rescue StandardError => e
122
+ raise Bolt::FileError.new("Could not create inventory.yaml at #{project}: #{e.message}", nil)
123
+ end
124
+ end
125
+
126
+ @outputter.print_message("Successfully created Bolt project at #{project}")
127
+
128
+ 0
129
+ end
130
+
131
+ # Migrates a project to use the latest file versions and best practices.
132
+ #
133
+ def migrate
134
+ unless $stdin.tty?
135
+ raise Bolt::Error.new(
136
+ "stdin is not a tty, unable to migrate project",
137
+ 'bolt/stdin-not-a-tty-error'
138
+ )
139
+ end
140
+
141
+ @outputter.print_message("Migrating project #{@config.project.path}\n\n")
142
+
143
+ @outputter.print_action_step(
144
+ "Migrating a Bolt project may make irreversible changes to the project's "\
145
+ "configuration and inventory files. Before continuing, make sure the "\
146
+ "project has a backup or uses a version control system."
147
+ )
148
+
149
+ return 0 unless Bolt::Util.prompt_yes_no("Continue with project migration?", @outputter)
150
+
151
+ @outputter.print_message('')
152
+
153
+ ok = migrate_inventory && migrate_config && migrate_modules
154
+
155
+ if ok
156
+ @outputter.print_message("Project successfully migrated")
157
+ else
158
+ @outputter.print_error("Project could not be migrated completely")
159
+ end
160
+
161
+ ok ? 0 : 1
162
+ end
163
+
164
+ # Migrates the project-level configuration file to the latest version.
165
+ #
166
+ private def migrate_config
167
+ migrator = ConfigMigrator.new(@outputter)
168
+
169
+ migrator.migrate(
170
+ @config.project.config_file,
171
+ @config.project.project_file,
172
+ @config.inventoryfile || @config.project.inventory_file,
173
+ @config.project.backup_dir
174
+ )
175
+ end
176
+
177
+ # Migrates the inventory file to the latest version.
178
+ #
179
+ private def migrate_inventory
180
+ migrator = InventoryMigrator.new(@outputter)
181
+
182
+ migrator.migrate(
183
+ @config.inventoryfile || @config.project.inventory_file,
184
+ @config.project.backup_dir
185
+ )
186
+ end
187
+
188
+ # Migrates the project's modules to use current best practices.
189
+ #
190
+ private def migrate_modules
191
+ migrator = ModuleMigrator.new(@outputter)
192
+
193
+ migrator.migrate(
194
+ @config.project,
195
+ @config.modulepath
196
+ )
197
+ end
198
+ end
199
+ end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/project_migrator/base'
3
+ require 'bolt/project_manager/migrator'
4
4
 
5
5
  module Bolt
6
- class ProjectMigrator
7
- class Config < Base
6
+ class ProjectManager
7
+ class ConfigMigrator < Migrator
8
8
  def migrate(config_file, project_file, inventory_file, backup_dir)
9
- bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
9
+ bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir) &&
10
+ update_options(project_file)
10
11
  end
11
12
 
12
13
  private def bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
@@ -63,6 +64,42 @@ module Bolt
63
64
 
64
65
  true
65
66
  end
67
+
68
+ private def update_options(project_file)
69
+ return true unless File.exist?(project_file)
70
+
71
+ @outputter.print_message("Updating project configuration options\n\n")
72
+ data = Bolt::Util.read_yaml_hash(project_file, 'config')
73
+ modified = false
74
+
75
+ [%w[apply_settings apply-settings], %w[plugin_hooks plugin-hooks]].each do |old, new|
76
+ next unless data.key?(old)
77
+
78
+ if data.key?(new)
79
+ @outputter.print_action_step("Removing deprecated option '#{old}'")
80
+ data.delete(old)
81
+ else
82
+ @outputter.print_action_step("Updating deprecated option '#{old}' to '#{new}'")
83
+ data[new] = data.delete(old)
84
+ end
85
+
86
+ modified = true
87
+ end
88
+
89
+ if modified
90
+ begin
91
+ File.write(project_file, data.to_yaml)
92
+ rescue StandardError => e
93
+ raise Bolt::FileError.new("#{e.message}; unable to write config.", project_file)
94
+ end
95
+
96
+ @outputter.print_action_step("Successfully updated project configuration #{project_file}")
97
+ else
98
+ @outputter.print_action_step("Project configuration is up to date, nothing to do.")
99
+ end
100
+
101
+ true
102
+ end
66
103
  end
67
104
  end
68
105
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/project_migrator/base'
3
+ require 'bolt/project_manager/migrator'
4
4
 
5
5
  module Bolt
6
- class ProjectMigrator
7
- class Inventory < Base
6
+ class ProjectManager
7
+ class InventoryMigrator < Migrator
8
8
  def migrate(inventory_file, backup_dir)
9
9
  inventory1to2(inventory_file, backup_dir)
10
10
  end