bolt 2.35.0 → 2.40.2

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/lib/bolt/analytics.rb +27 -8
  5. data/lib/bolt/apply_result.rb +3 -3
  6. data/lib/bolt/bolt_option_parser.rb +45 -18
  7. data/lib/bolt/cli.rb +92 -110
  8. data/lib/bolt/config.rb +184 -80
  9. data/lib/bolt/config/options.rb +144 -83
  10. data/lib/bolt/config/transport/base.rb +10 -19
  11. data/lib/bolt/config/transport/local.rb +1 -7
  12. data/lib/bolt/config/transport/options.rb +11 -68
  13. data/lib/bolt/config/transport/ssh.rb +8 -19
  14. data/lib/bolt/executor.rb +5 -17
  15. data/lib/bolt/inventory.rb +25 -0
  16. data/lib/bolt/inventory/group.rb +0 -8
  17. data/lib/bolt/inventory/options.rb +130 -0
  18. data/lib/bolt/inventory/target.rb +10 -11
  19. data/lib/bolt/module_installer.rb +21 -13
  20. data/lib/bolt/module_installer/resolver.rb +1 -1
  21. data/lib/bolt/outputter.rb +19 -5
  22. data/lib/bolt/outputter/human.rb +20 -1
  23. data/lib/bolt/outputter/json.rb +1 -1
  24. data/lib/bolt/outputter/logger.rb +1 -1
  25. data/lib/bolt/outputter/rainbow.rb +13 -2
  26. data/lib/bolt/plugin.rb +41 -12
  27. data/lib/bolt/plugin/cache.rb +76 -0
  28. data/lib/bolt/plugin/module.rb +4 -4
  29. data/lib/bolt/plugin/puppetdb.rb +1 -1
  30. data/lib/bolt/project.rb +59 -40
  31. data/lib/bolt/project_manager.rb +201 -0
  32. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +49 -4
  33. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  34. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  35. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +5 -3
  36. data/lib/bolt/puppetdb/client.rb +8 -0
  37. data/lib/bolt/puppetdb/config.rb +1 -2
  38. data/lib/bolt/rerun.rb +1 -5
  39. data/lib/bolt/shell/bash.rb +8 -2
  40. data/lib/bolt/shell/powershell.rb +21 -3
  41. data/lib/bolt/target.rb +4 -0
  42. data/lib/bolt/task/run.rb +1 -1
  43. data/lib/bolt/transport/local.rb +13 -0
  44. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  45. data/lib/bolt/util.rb +36 -7
  46. data/lib/bolt/validator.rb +227 -0
  47. data/lib/bolt/version.rb +1 -1
  48. data/lib/bolt_server/base_config.rb +3 -1
  49. data/lib/bolt_server/config.rb +3 -1
  50. data/lib/bolt_server/plugin.rb +13 -0
  51. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  52. data/lib/bolt_server/schemas/connect-data.json +22 -0
  53. data/lib/bolt_server/schemas/partials/task.json +3 -3
  54. data/lib/bolt_server/transport_app.rb +68 -40
  55. data/libexec/apply_catalog.rb +1 -1
  56. data/libexec/custom_facts.rb +1 -1
  57. data/libexec/query_resources.rb +1 -1
  58. metadata +23 -17
  59. data/lib/bolt/project_migrator.rb +0 -80
@@ -12,15 +12,15 @@ module Bolt
12
12
  end
13
13
  end
14
14
 
15
- def self.load(name, modules, opts)
16
- mod = modules[name]
17
- if mod&.plugin?
15
+ # mod should not be nil
16
+ def self.load(mod, opts)
17
+ if mod.plugin?
18
18
  opts[:mod] = mod
19
19
  plugin = Bolt::Plugin::Module.new(**opts)
20
20
  plugin.setup
21
21
  plugin
22
22
  else
23
- raise PluginError::Unknown, name
23
+ raise PluginError::Unknown, mod.name
24
24
  end
25
25
  end
26
26
 
@@ -12,7 +12,7 @@ module Bolt
12
12
  end
13
13
 
14
14
  TEMPLATE_OPTS = %w[alias config facts features name uri vars].freeze
15
- PLUGIN_OPTS = %w[_plugin query target_mapping].freeze
15
+ PLUGIN_OPTS = %w[_plugin _cache query target_mapping].freeze
16
16
 
17
17
  attr_reader :puppetdb_client
18
18
 
@@ -2,24 +2,19 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'bolt/config'
5
+ require 'bolt/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,
21
16
  :deprecations, :downloads, :plans_path, :modulepath, :managed_moduledir,
22
- :backup_dir
17
+ :backup_dir, :cache_file
23
18
 
24
19
  def self.default_project(logs = [])
25
20
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
@@ -32,23 +27,31 @@ module Bolt
32
27
  # directory called Boltdir or a file called bolt.yaml (for a control repo
33
28
  # type Project). Otherwise, repeat the check on each directory up the
34
29
  # hierarchy, falling back to the default if we reach the root.
35
- def self.find_boltdir(dir, logs = [])
30
+ def self.find_boltdir(dir, logs = [], deprecations = [])
36
31
  dir = Pathname.new(dir)
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?
36
+ command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
37
+ msg = "Configuration file #{dir + 'bolt.yaml'} is deprecated and will be "\
38
+ "removed in Bolt 3.0.\nUpdate your Bolt project to the latest Bolt practices "\
39
+ "using #{command}"
40
+ deprecations << { type: "Project level bolt.yaml",
41
+ msg: msg }
42
+ create_project(dir, 'local', logs, deprecations)
43
+ elsif (dir + CONFIG_NAME).file?
41
44
  create_project(dir, 'local', logs)
42
45
  elsif dir.root?
43
46
  default_project(logs)
44
47
  else
45
48
  logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
46
49
  "This directory won't be loaded as a project." }
47
- find_boltdir(dir.parent, logs)
50
+ find_boltdir(dir.parent, logs, deprecations)
48
51
  end
49
52
  end
50
53
 
51
- def self.create_project(path, type = 'option', logs = [])
54
+ def self.create_project(path, type = 'option', logs = [], deprecations = [])
52
55
  fullpath = Pathname.new(path).expand_path
53
56
 
54
57
  if type == 'user'
@@ -73,21 +76,42 @@ module Bolt
73
76
  )
74
77
  end
75
78
 
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))
79
+ project_file = File.join(fullpath, CONFIG_NAME)
80
+ data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
81
+ default = type =~ /user|system/ ? 'default ' : ''
82
+ exist = File.exist?(File.expand_path(project_file))
83
+
80
84
  logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
81
- new(data, path, type, logs)
85
+
86
+ Bolt::Validator.new.tap do |validator|
87
+ validator.validate(data, schema, project_file)
88
+
89
+ validator.warnings.each { |warning| logs << { warn: warning } }
90
+
91
+ validator.deprecations.each do |dep|
92
+ deprecations << { type: "#{CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
93
+ end
94
+ end
95
+
96
+ new(data, path, type, logs, deprecations)
82
97
  end
83
98
 
84
- def initialize(raw_data, path, type = 'option', logs = [])
85
- @path = Pathname.new(path).expand_path
99
+ # Builds the schema for bolt-project.yaml used by the validator.
100
+ #
101
+ def self.schema
102
+ {
103
+ type: Hash,
104
+ properties: Bolt::Config::BOLT_PROJECT_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
105
+ definitions: Bolt::Config::OPTIONS
106
+ }
107
+ end
86
108
 
87
- @project_file = @path + 'bolt-project.yaml'
109
+ def initialize(raw_data, path, type = 'option', logs = [], deprecations = [])
110
+ @path = Pathname.new(path).expand_path
111
+ @project_file = @path + CONFIG_NAME
112
+ @logs = logs
113
+ @deprecations = deprecations
88
114
 
89
- @logs = logs
90
- @deprecations = []
91
115
  if (@path + 'bolt.yaml').file? && project_file?
92
116
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
93
117
  "Transport config should be set in inventory.yaml, all other config should be set in "\
@@ -105,6 +129,7 @@ module Bolt
105
129
  @plans_path = @path + 'plans'
106
130
  @managed_moduledir = @path + '.modules'
107
131
  @backup_dir = @path + '.bolt-bak'
132
+ @cache_file = @path + '.plugin_cache.json'
108
133
 
109
134
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
110
135
  if tc.any?
@@ -173,6 +198,14 @@ module Bolt
173
198
  @data['plans']
174
199
  end
175
200
 
201
+ def plugin_cache
202
+ @data['plugin-cache']
203
+ end
204
+
205
+ def module_install
206
+ @data['module-install']
207
+ end
208
+
176
209
  def modules
177
210
  @modules ||= @data['modules']&.map do |mod|
178
211
  if mod.is_a?(String)
@@ -194,27 +227,13 @@ module Bolt
194
227
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
195
228
  "with a built-in Bolt module of the same name."
196
229
  end
197
- else
230
+ elsif name.nil? &&
231
+ (File.directory?(plans_path) ||
232
+ File.directory?(@path + 'tasks') ||
233
+ File.directory?(@path + 'files'))
198
234
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
199
235
  @logs << { warn: message }
200
236
  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
237
  end
219
238
 
220
239
  def check_deprecated_file
@@ -0,0 +1,201 @@
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
+ @outputter.start_spin
107
+ Bolt::ModuleInstaller.new(@outputter, @pal).install(modules, puppetfile, moduledir)
108
+ @outputter.stop_spin
109
+ end
110
+
111
+ data = { 'name' => project_name }
112
+ data['modules'] = modules || []
113
+
114
+ begin
115
+ File.write(config.to_path, data.to_yaml)
116
+ rescue StandardError => e
117
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
118
+ end
119
+
120
+ unless inventoryfile.exist?
121
+ begin
122
+ File.write(inventoryfile.to_path, INVENTORY_TEMPLATE)
123
+ rescue StandardError => e
124
+ raise Bolt::FileError.new("Could not create inventory.yaml at #{project}: #{e.message}", nil)
125
+ end
126
+ end
127
+
128
+ @outputter.print_message("Successfully created Bolt project at #{project}")
129
+
130
+ 0
131
+ end
132
+
133
+ # Migrates a project to use the latest file versions and best practices.
134
+ #
135
+ def migrate
136
+ unless $stdin.tty?
137
+ raise Bolt::Error.new(
138
+ "stdin is not a tty, unable to migrate project",
139
+ 'bolt/stdin-not-a-tty-error'
140
+ )
141
+ end
142
+
143
+ @outputter.print_message("Migrating project #{@config.project.path}\n\n")
144
+
145
+ @outputter.print_action_step(
146
+ "Migrating a Bolt project may make irreversible changes to the project's "\
147
+ "configuration and inventory files. Before continuing, make sure the "\
148
+ "project has a backup or uses a version control system."
149
+ )
150
+
151
+ return 0 unless Bolt::Util.prompt_yes_no("Continue with project migration?", @outputter)
152
+
153
+ @outputter.print_message('')
154
+
155
+ ok = migrate_inventory && migrate_config && migrate_modules
156
+
157
+ if ok
158
+ @outputter.print_message("Project successfully migrated")
159
+ else
160
+ @outputter.print_error("Project could not be migrated completely")
161
+ end
162
+
163
+ ok ? 0 : 1
164
+ end
165
+
166
+ # Migrates the project-level configuration file to the latest version.
167
+ #
168
+ private def migrate_config
169
+ migrator = ConfigMigrator.new(@outputter)
170
+
171
+ migrator.migrate(
172
+ @config.project.config_file,
173
+ @config.project.project_file,
174
+ @config.inventoryfile || @config.project.inventory_file,
175
+ @config.project.backup_dir
176
+ )
177
+ end
178
+
179
+ # Migrates the inventory file to the latest version.
180
+ #
181
+ private def migrate_inventory
182
+ migrator = InventoryMigrator.new(@outputter)
183
+
184
+ migrator.migrate(
185
+ @config.inventoryfile || @config.project.inventory_file,
186
+ @config.project.backup_dir
187
+ )
188
+ end
189
+
190
+ # Migrates the project's modules to use current best practices.
191
+ #
192
+ private def migrate_modules
193
+ migrator = ModuleMigrator.new(@outputter)
194
+
195
+ migrator.migrate(
196
+ @config.project,
197
+ @config.modulepath
198
+ )
199
+ end
200
+ end
201
+ 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,50 @@ 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
+ # Keys to update. The first element is the old key, while the second is
76
+ # the key update it to.
77
+ to_update = [
78
+ %w[apply_settings apply-settings],
79
+ %w[puppetfile module-install],
80
+ %w[plugin_hooks plugin-hooks]
81
+ ]
82
+
83
+ to_update.each do |old, new|
84
+ next unless data.key?(old)
85
+
86
+ if data.key?(new)
87
+ @outputter.print_action_step("Removing deprecated option '#{old}'")
88
+ data.delete(old)
89
+ else
90
+ @outputter.print_action_step("Updating deprecated option '#{old}' to '#{new}'")
91
+ data[new] = data.delete(old)
92
+ end
93
+
94
+ modified = true
95
+ end
96
+
97
+ if modified
98
+ begin
99
+ File.write(project_file, data.to_yaml)
100
+ rescue StandardError => e
101
+ raise Bolt::FileError.new("#{e.message}; unable to write config.", project_file)
102
+ end
103
+
104
+ @outputter.print_action_step("Successfully updated project configuration #{project_file}")
105
+ else
106
+ @outputter.print_action_step("Project configuration is up to date, nothing to do.")
107
+ end
108
+
109
+ true
110
+ end
66
111
  end
67
112
  end
68
113
  end