bolt 2.27.0 → 2.32.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  5. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  6. data/guides/module.txt +19 -0
  7. data/guides/modulepath.txt +25 -0
  8. data/lib/bolt/applicator.rb +14 -14
  9. data/lib/bolt/bolt_option_parser.rb +74 -22
  10. data/lib/bolt/catalog.rb +1 -1
  11. data/lib/bolt/cli.rb +178 -127
  12. data/lib/bolt/config.rb +13 -1
  13. data/lib/bolt/config/modulepath.rb +30 -0
  14. data/lib/bolt/config/options.rb +38 -9
  15. data/lib/bolt/config/transport/options.rb +1 -1
  16. data/lib/bolt/executor.rb +1 -1
  17. data/lib/bolt/inventory.rb +11 -10
  18. data/lib/bolt/logger.rb +26 -19
  19. data/lib/bolt/module_installer.rb +197 -0
  20. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  21. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  22. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  23. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  24. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  25. data/lib/bolt/module_installer/resolver.rb +76 -0
  26. data/lib/bolt/module_installer/specs.rb +93 -0
  27. data/lib/bolt/module_installer/specs/forge_spec.rb +84 -0
  28. data/lib/bolt/module_installer/specs/git_spec.rb +178 -0
  29. data/lib/bolt/outputter.rb +2 -45
  30. data/lib/bolt/outputter/human.rb +78 -18
  31. data/lib/bolt/outputter/json.rb +22 -7
  32. data/lib/bolt/outputter/logger.rb +2 -2
  33. data/lib/bolt/pal.rb +29 -25
  34. data/lib/bolt/plugin.rb +1 -1
  35. data/lib/bolt/plugin/module.rb +1 -1
  36. data/lib/bolt/project.rb +32 -22
  37. data/lib/bolt/project_migrator.rb +80 -0
  38. data/lib/bolt/project_migrator/base.rb +39 -0
  39. data/lib/bolt/project_migrator/config.rb +67 -0
  40. data/lib/bolt/project_migrator/inventory.rb +67 -0
  41. data/lib/bolt/project_migrator/modules.rb +200 -0
  42. data/lib/bolt/shell/bash.rb +4 -3
  43. data/lib/bolt/transport/base.rb +4 -4
  44. data/lib/bolt/transport/ssh/connection.rb +1 -1
  45. data/lib/bolt/util.rb +51 -10
  46. data/lib/bolt/version.rb +1 -1
  47. data/lib/bolt_server/acl.rb +2 -2
  48. data/lib/bolt_server/base_config.rb +3 -3
  49. data/lib/bolt_server/file_cache.rb +11 -11
  50. data/lib/bolt_server/schemas/partials/task.json +17 -2
  51. data/lib/bolt_server/transport_app.rb +93 -13
  52. data/lib/bolt_spec/bolt_context.rb +8 -6
  53. data/lib/bolt_spec/plans.rb +1 -1
  54. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  55. data/lib/bolt_spec/run.rb +1 -1
  56. metadata +30 -11
  57. data/lib/bolt/project_migrate.rb +0 -138
  58. data/lib/bolt/puppetfile.rb +0 -160
  59. data/lib/bolt/puppetfile/module.rb +0 -66
  60. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/project_migrator/base'
4
+
5
+ module Bolt
6
+ class ProjectMigrator
7
+ class Modules < Base
8
+ def migrate(project, configured_modulepath)
9
+ return true unless project.modules.nil?
10
+
11
+ @outputter.print_message "Migrating project modules\n\n"
12
+
13
+ config = project.project_file
14
+ puppetfile = project.puppetfile
15
+ managed_moduledir = project.managed_moduledir
16
+ modulepath = [(project.path + 'modules').to_s,
17
+ (project.path + 'site-modules').to_s,
18
+ (project.path + 'site').to_s]
19
+
20
+ # Notify user to manually migrate modules if using non-default modulepath
21
+ if configured_modulepath != modulepath
22
+ @outputter.print_action_step(
23
+ "Project has a non-default configured modulepath, unable to automatically "\
24
+ "migrate project modules. To migrate project modules manually, see "\
25
+ "http://pup.pt/bolt-modules"
26
+ )
27
+ true
28
+ # Migrate modules from Puppetfile
29
+ elsif File.exist?(puppetfile)
30
+ migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, modulepath)
31
+ # Migrate modules to updated modulepath
32
+ else
33
+ consolidate_modules(modulepath)
34
+ update_project_config([], config)
35
+ end
36
+ end
37
+
38
+ # Migrates modules by reading a Puppetfile and prompting the user for
39
+ # which ones are direct dependencies for the project. Once the user has
40
+ # selected the direct dependencies, this will resolve the modules, write a
41
+ # new Puppetfile, install the modules, and then move any remaining modules
42
+ # to the new moduledir.
43
+ #
44
+ private def migrate_modules_from_puppetfile(config, puppetfile_path, managed_moduledir, modulepath)
45
+ require 'bolt/module_installer/installer'
46
+ require 'bolt/module_installer/puppetfile'
47
+ require 'bolt/module_installer/resolver'
48
+ require 'bolt/module_installer/specs'
49
+
50
+ begin
51
+ @outputter.print_action_step("Parsing Puppetfile at #{puppetfile_path}")
52
+ puppetfile = Bolt::ModuleInstaller::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
53
+ rescue Bolt::Error => e
54
+ @outputter.print_action_error("#{e.message}\nSkipping module migration.")
55
+ return false
56
+ end
57
+
58
+ # Prompt for direct dependencies
59
+ modules = select_modules(puppetfile.modules)
60
+
61
+ # Create specs to resolve from
62
+ specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
63
+
64
+ # Attempt to resolve dependencies
65
+ begin
66
+ @outputter.print_message('')
67
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
68
+ puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
69
+ rescue Bolt::Error => e
70
+ @outputter.print_action_error("#{e.message}\nSkipping module migration.")
71
+ return false
72
+ end
73
+
74
+ migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
75
+
76
+ # Move remaining modules to 'modules'
77
+ consolidate_modules(modulepath)
78
+
79
+ # Delete old modules that are now managed
80
+ delete_modules(modulepath.first, puppetfile.modules)
81
+
82
+ # Add modules to project
83
+ update_project_config(modules.map(&:to_hash), config)
84
+ end
85
+
86
+ # Migrates the managed modules. If modules were selected to be managed,
87
+ # the Puppetfile is rewritten and modules are installed. If no modules
88
+ # were selected, the Puppetfile is deleted.
89
+ #
90
+ private def migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
91
+ if puppetfile.modules.any?
92
+ # Show the new Puppetfile content
93
+ message = "Generated new Puppetfile content:\n\n"
94
+ message += puppetfile.modules.map(&:to_spec).join("\n").to_s
95
+ @outputter.print_action_step(message)
96
+
97
+ # Write Puppetfile
98
+ @outputter.print_action_step("Updating Puppetfile at #{puppetfile_path}")
99
+ puppetfile.write(puppetfile_path, managed_moduledir)
100
+
101
+ # Install Puppetfile
102
+ @outputter.print_action_step("Syncing modules from #{puppetfile_path} to #{managed_moduledir}")
103
+ Bolt::ModuleInstaller::Installer.new.install(puppetfile_path, managed_moduledir)
104
+ else
105
+ @outputter.print_action_step(
106
+ "Project does not include any managed modules, deleting Puppetfile "\
107
+ "at #{puppetfile_path}"
108
+ )
109
+ FileUtils.rm(puppetfile_path)
110
+ end
111
+ end
112
+
113
+ # Prompts the user to select modules, returning a list of
114
+ # the selected modules.
115
+ #
116
+ private def select_modules(modules)
117
+ @outputter.print_action_step(
118
+ "Select modules that are direct dependencies of your project. Bolt will "\
119
+ "automatically manage dependencies for each module selected, so do not "\
120
+ "select a module's dependencies unless you use content from it directly "\
121
+ "in your project."
122
+ )
123
+
124
+ all = Bolt::Util.prompt_yes_no("Select all modules?", @outputter)
125
+ return modules if all
126
+
127
+ modules.select do |mod|
128
+ Bolt::Util.prompt_yes_no("Select #{mod.full_name}?", @outputter)
129
+ end
130
+ end
131
+
132
+ # Consolidates all modules on the modulepath to 'modules'.
133
+ #
134
+ private def consolidate_modules(modulepath)
135
+ moduledir, *sources = modulepath
136
+
137
+ sources.select! { |source| Dir.exist?(source) }
138
+
139
+ if sources.any?
140
+ @outputter.print_action_step(
141
+ "Moving modules from #{sources.join(', ')} to #{moduledir}"
142
+ )
143
+
144
+ FileUtils.mkdir_p(moduledir)
145
+ move_modules(moduledir, sources)
146
+ end
147
+ end
148
+
149
+ # Moves modules from a list of source directories to the specified
150
+ # moduledir, deleting the source directory after it's done.
151
+ #
152
+ private def move_modules(moduledir, sources)
153
+ moduledir = Pathname.new(moduledir)
154
+
155
+ sources.each do |source|
156
+ source = Pathname.new(source)
157
+
158
+ source.each_child do |mod|
159
+ next unless mod.directory?
160
+ next if (moduledir + mod.basename).directory?
161
+ FileUtils.mv(mod, moduledir)
162
+ end
163
+
164
+ FileUtils.rm_r(source)
165
+ end
166
+ end
167
+
168
+ # Deletes modules from a specified directory.
169
+ #
170
+ private def delete_modules(moduledir, modules)
171
+ @outputter.print_action_step("Cleaning up #{moduledir}")
172
+ moduledir = Pathname.new(moduledir)
173
+
174
+ modules.each do |mod|
175
+ path = moduledir + mod.name
176
+ FileUtils.rm_r(path) if path.directory?
177
+ end
178
+ end
179
+
180
+ # Adds a list of modules to the project configuration file.
181
+ #
182
+ private def update_project_config(modules, config_file)
183
+ @outputter.print_action_step("Updating project configuration at #{config_file}")
184
+ data = Bolt::Util.read_optional_yaml_hash(config_file, 'project')
185
+ data.merge!('modules' => modules)
186
+ data.delete('modulepath')
187
+
188
+ begin
189
+ File.write(config_file, data.to_yaml)
190
+ true
191
+ rescue StandardError => e
192
+ raise Bolt::FileError.new(
193
+ "Unable to write to #{config_file}: #{e.message}",
194
+ config_file
195
+ )
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -143,7 +143,7 @@ module Bolt
143
143
 
144
144
  execute_options[:stdin] = stdin
145
145
  execute_options[:sudoable] = true if run_as
146
- output = execute(remote_task_path, execute_options)
146
+ output = execute(remote_task_path, **execute_options)
147
147
  end
148
148
  Bolt::Result.for_task(target, output.stdout.string,
149
149
  output.stderr.string,
@@ -202,13 +202,14 @@ module Bolt
202
202
  end
203
203
 
204
204
  def handle_sudo_errors(err)
205
- if err =~ /^#{conn.user} is not in the sudoers file\./
205
+ case err
206
+ when /^#{conn.user} is not in the sudoers file\./
206
207
  @logger.trace { err }
207
208
  raise Bolt::Node::EscalateError.new(
208
209
  "User #{conn.user} does not have sudo permission on #{target}",
209
210
  'SUDO_DENIED'
210
211
  )
211
- elsif err =~ /^Sorry, try again\./
212
+ when /^Sorry, try again\./
212
213
  @logger.trace { err }
213
214
  raise Bolt::Node::EscalateError.new(
214
215
  "Sudo password for user #{conn.user} not recognized on #{target}",
@@ -47,10 +47,10 @@ module Bolt
47
47
  callback&.call(type: :node_start, target: target)
48
48
 
49
49
  result = begin
50
- yield
51
- rescue StandardError, NotImplementedError => e
52
- Bolt::Result.from_exception(target, e, action: action)
53
- end
50
+ yield
51
+ rescue StandardError, NotImplementedError => e
52
+ Bolt::Result.from_exception(target, e, action: action)
53
+ end
54
54
 
55
55
  callback&.call(type: :node_result, result: result)
56
56
  result
@@ -30,7 +30,7 @@ module Bolt
30
30
  @transport_logger = transport_logger
31
31
  @logger.trace("Initializing ssh connection to #{@target.safe_name}")
32
32
 
33
- if target.options['private-key']&.instance_of?(String)
33
+ if target.options['private-key'].instance_of?(String)
34
34
  begin
35
35
  Bolt::Util.validate_file('ssh key', target.options['private-key'])
36
36
  rescue Bolt::FileError => e
@@ -3,6 +3,25 @@
3
3
  module Bolt
4
4
  module Util
5
5
  class << self
6
+ # Gets input for an argument.
7
+ def get_arg_input(value)
8
+ if value.start_with?('@')
9
+ file = value.sub(/^@/, '')
10
+ read_arg_file(file)
11
+ elsif value == '-'
12
+ $stdin.read
13
+ else
14
+ value
15
+ end
16
+ end
17
+
18
+ # Reads a file passed as an argument to a command.
19
+ def read_arg_file(file)
20
+ File.read(File.expand_path(file))
21
+ rescue StandardError => e
22
+ raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
23
+ end
24
+
6
25
  def read_yaml_hash(path, file_name)
7
26
  require 'yaml'
8
27
 
@@ -179,16 +198,16 @@ module Bolt
179
198
  # object was frozen
180
199
  frozen = obj.frozen?
181
200
  cl = begin
182
- obj.clone(freeze: false)
183
- # Some datatypes, such as FalseClass, can't be unfrozen. These
184
- # aren't the types we recurse on, so we can leave them frozen
185
- rescue ArgumentError => e
186
- if e.message =~ /can't unfreeze/
187
- obj.clone
188
- else
189
- raise e
190
- end
191
- end
201
+ obj.clone(freeze: false)
202
+ # Some datatypes, such as FalseClass, can't be unfrozen. These
203
+ # aren't the types we recurse on, so we can leave them frozen
204
+ rescue ArgumentError => e
205
+ if e.message =~ /can't unfreeze/
206
+ obj.clone
207
+ else
208
+ raise e
209
+ end
210
+ end
192
211
  rescue *error_types
193
212
  cloned[obj.object_id] = obj
194
213
  obj
@@ -280,6 +299,28 @@ module Bolt
280
299
  raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
281
300
  path.split(%r{[/\\]}).last
282
301
  end
302
+
303
+ # Prompts yes or no, returning true for yes and false for no.
304
+ #
305
+ def prompt_yes_no(prompt, outputter)
306
+ choices = {
307
+ 'y' => true,
308
+ 'yes' => true,
309
+ 'n' => false,
310
+ 'no' => false
311
+ }
312
+
313
+ loop do
314
+ outputter.print_prompt("#{prompt} ([y]es/[n]o) ")
315
+ response = $stdin.gets.to_s.downcase.chomp
316
+
317
+ if choices.key?(response)
318
+ return choices[response]
319
+ else
320
+ outputter.print_prompt_error("Invalid response, must pick [y]es or [n]o")
321
+ end
322
+ end
323
+ end
283
324
  end
284
325
  end
285
326
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.27.0'
4
+ VERSION = '2.32.0'
5
5
  end
@@ -16,9 +16,9 @@ module BoltServer
16
16
  end
17
17
  end
18
18
 
19
- def initialize(app, whitelist)
19
+ def initialize(app, allowlist)
20
20
  acls = []
21
- whitelist.each do |entry|
21
+ allowlist.each do |entry|
22
22
  acls << {
23
23
  'resources' => [
24
24
  {
@@ -7,7 +7,7 @@ module BoltServer
7
7
  class BaseConfig
8
8
  def config_keys
9
9
  %w[host port ssl-cert ssl-key ssl-ca-cert
10
- ssl-cipher-suites loglevel logfile whitelist projects-dir]
10
+ ssl-cipher-suites loglevel logfile allowlist projects-dir]
11
11
  end
12
12
 
13
13
  def env_keys
@@ -98,8 +98,8 @@ module BoltServer
98
98
  raise Bolt::ValidationError, "Configured 'ssl-cipher-suites' must be an array of cipher suite names"
99
99
  end
100
100
 
101
- unless @data['whitelist'].nil? || @data['whitelist'].is_a?(Array)
102
- raise Bolt::ValidationError, "Configured 'whitelist' must be an array of names"
101
+ unless @data['allowlist'].nil? || @data['allowlist'].is_a?(Array)
102
+ raise Bolt::ValidationError, "Configured 'allowlist' must be an array of names"
103
103
  end
104
104
  end
105
105
 
@@ -64,17 +64,17 @@ module BoltServer
64
64
 
65
65
  def client
66
66
  @client ||= begin
67
- uri = URI(@config['file-server-uri'])
68
- https = Net::HTTP.new(uri.host, uri.port)
69
- https.use_ssl = true
70
- https.ssl_version = :TLSv1_2
71
- https.ca_file = @config['ssl-ca-cert']
72
- https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
73
- https.key = OpenSSL::PKey::RSA.new(ssl_key)
74
- https.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
- https.open_timeout = @config['file-server-conn-timeout']
76
- https
77
- end
67
+ uri = URI(@config['file-server-uri'])
68
+ https = Net::HTTP.new(uri.host, uri.port)
69
+ https.use_ssl = true
70
+ https.ssl_version = :TLSv1_2
71
+ https.ca_file = @config['ssl-ca-cert']
72
+ https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
73
+ https.key = OpenSSL::PKey::RSA.new(ssl_key)
74
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
+ https.open_timeout = @config['file-server-conn-timeout']
76
+ https
77
+ end
78
78
  end
79
79
 
80
80
  def request_file(path, params, file)
@@ -63,9 +63,24 @@
63
63
  "environment": {
64
64
  "description": "Environment the task is in",
65
65
  "type": "string"
66
+ },
67
+ "project": {
68
+ "description": "Project the task is in",
69
+ "type": "string"
66
70
  }
67
71
  },
68
- "required": ["environment"],
72
+ "oneOf": [
73
+ {
74
+ "required": [
75
+ "environment"
76
+ ]
77
+ },
78
+ {
79
+ "required": [
80
+ "project"
81
+ ]
82
+ }
83
+ ],
69
84
  "additionalProperties": true
70
85
  }
71
86
  },
@@ -90,5 +105,5 @@
90
105
  }
91
106
  },
92
107
  "required": ["name", "files"],
93
- "additionalProperties": false
108
+ "additionalProperties": true
94
109
  }
@@ -14,7 +14,9 @@ require 'json-schema'
14
14
 
15
15
  # These are only needed for the `/plans` endpoint.
16
16
  require 'puppet'
17
- require 'bolt_server/pe/pal'
17
+
18
+ # Needed by the `/project_file_metadatas` endpoint
19
+ require 'puppet/file_serving/fileset'
18
20
 
19
21
  module BoltServer
20
22
  class TransportApp < Sinatra::Base
@@ -35,6 +37,17 @@ module BoltServer
35
37
  transport-winrm
36
38
  ].freeze
37
39
 
40
+ # PE_BOLTLIB_PATH is intended to function exactly like the BOLTLIB_PATH used
41
+ # in Bolt::PAL. Paths and variable names are similar to what exists in
42
+ # Bolt::PAL, but with a 'PE' prefix.
43
+ PE_BOLTLIB_PATH = '/opt/puppetlabs/server/apps/bolt-server/pe-bolt-modules'
44
+
45
+ # For now at least, we maintain an entirely separate codedir from
46
+ # puppetserver by default, so that filesync can work properly. If filesync
47
+ # is not used, this can instead match the usual puppetserver codedir.
48
+ # See the `orchestrator.bolt.codedir` tk config setting.
49
+ DEFAULT_BOLT_CODEDIR = '/opt/puppetlabs/server/data/orchestration-services/code'
50
+
38
51
  def initialize(config)
39
52
  @config = config
40
53
  @schemas = Hash[REQUEST_SCHEMAS.map do |basename|
@@ -191,10 +204,52 @@ module BoltServer
191
204
  [@executor.run_script(target, file_location, body['arguments'])]
192
205
  end
193
206
 
207
+ # This function is nearly identical to Bolt::Pal's `with_puppet_settings` with the
208
+ # one difference that we set the codedir to point to actual code, rather than the
209
+ # tmpdir. We only use this funtion inside the Modulepath initializer so that Puppet
210
+ # is correctly configured to pull environment configuration correctly. If we don't
211
+ # set codedir in this way: when we try to load and interpolate the modulepath it
212
+ # won't correctly load.
213
+ #
214
+ # WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
215
+ def with_pe_pal_init_settings(codedir, environmentpath, basemodulepath)
216
+ Dir.mktmpdir('pe-bolt') do |dir|
217
+ cli = []
218
+ Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
219
+ dir = setting == :codedir ? codedir : dir
220
+ cli << "--#{setting}" << dir
221
+ end
222
+ cli << "--environmentpath" << environmentpath
223
+ cli << "--basemodulepath" << basemodulepath
224
+ Puppet.settings.send(:clear_everything_for_tests)
225
+ Puppet.initialize_settings(cli)
226
+ yield
227
+ end
228
+ end
229
+
230
+ # Use puppet to identify the modulepath from an environment.
231
+ #
232
+ # WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
233
+ def modulepath_from_environment(environment_name)
234
+ codedir = DEFAULT_BOLT_CODEDIR
235
+ environmentpath = "#{codedir}/environments"
236
+ basemodulepath = "#{codedir}/modules:/opt/puppetlabs/puppet/modules"
237
+ modulepath_dirs = nil
238
+ with_pe_pal_init_settings(codedir, environmentpath, basemodulepath) do
239
+ environment = Puppet.lookup(:environments).get!(environment_name)
240
+ modulepath_dirs = environment.modulepath
241
+ end
242
+ modulepath_dirs
243
+ end
244
+
194
245
  def in_pe_pal_env(environment)
195
246
  return [400, '`environment` is a required argument'] if environment.nil?
196
247
  @pal_mutex.synchronize do
197
- pal = BoltServer::PE::PAL.new({}, environment)
248
+ modulepath_obj = Bolt::Config::Modulepath.new(
249
+ modulepath_from_environment(environment),
250
+ boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
251
+ )
252
+ pal = Bolt::PAL.new(modulepath_obj, nil, nil)
198
253
  yield pal
199
254
  rescue Puppet::Environments::EnvironmentNotFound
200
255
  [400, {
@@ -212,17 +267,12 @@ module BoltServer
212
267
  return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
213
268
  @pal_mutex.synchronize do
214
269
  project = Bolt::Project.create_project(project_dir)
215
- bolt_config = Bolt::Config.from_project(project, {})
216
- pal = Bolt::PAL.new(bolt_config.modulepath, nil, nil, nil, nil, nil, bolt_config.project)
217
- module_path = [
218
- BoltServer::PE::PAL::PE_BOLTLIB_PATH,
219
- Bolt::PAL::BOLTLIB_PATH,
220
- *bolt_config.modulepath,
221
- Bolt::PAL::MODULES_PATH
222
- ]
223
- # CODEREVIEW: I *think* this is the only thing we need to make different between bolt's PAL. Is it acceptable
224
- # to hack this? Modulepath is currently a readable attribute, could we make it writeable?
225
- pal.instance_variable_set(:@modulepath, module_path)
270
+ bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
271
+ modulepath_object = Bolt::Config::Modulepath.new(
272
+ bolt_config.modulepath,
273
+ boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
274
+ )
275
+ pal = Bolt::PAL.new(modulepath_object, nil, nil, nil, nil, nil, bolt_config.project)
226
276
  context = {
227
277
  pal: pal,
228
278
  config: bolt_config
@@ -307,6 +357,23 @@ module BoltServer
307
357
  plans.map { |plan_name| { 'name' => plan_name } }
308
358
  end
309
359
 
360
+ def file_metadatas(pal, module_name, file)
361
+ pal.in_bolt_compiler do
362
+ mod = Puppet.lookup(:current_environment).module(module_name)
363
+ raise ArgumentError, "`module_name`: #{module_name} does not exist" unless mod
364
+ abs_file_path = mod.file(file)
365
+ raise ArgumentError, "`file`: #{file} does not exist inside the module's 'files' directory" unless abs_file_path
366
+ fileset = Puppet::FileServing::Fileset.new(abs_file_path, 'recurse' => 'yes')
367
+ Puppet::FileServing::Fileset.merge(fileset).collect do |relative_file_path, base_path|
368
+ metadata = Puppet::FileServing::Metadata.new(base_path, relative_path: relative_file_path)
369
+ metadata.checksum_type = 'sha256'
370
+ metadata.links = 'follow'
371
+ metadata.collect
372
+ metadata.to_data_hash
373
+ end
374
+ end
375
+ end
376
+
310
377
  get '/' do
311
378
  200
312
379
  end
@@ -550,6 +617,19 @@ module BoltServer
550
617
  end
551
618
  end
552
619
 
620
+ # Implements puppetserver's file_metadatas endpoint for projects.
621
+ #
622
+ # @param project_ref [String] the project_ref to fetch the file metadatas from
623
+ get '/project_file_metadatas/:module_name/*' do
624
+ in_bolt_project(params['project_ref']) do |context|
625
+ file = params[:splat].first
626
+ metadatas = file_metadatas(context[:pal], params[:module_name], file)
627
+ [200, metadatas.to_json]
628
+ end
629
+ rescue ArgumentError => e
630
+ [400, e.message]
631
+ end
632
+
553
633
  error 404 do
554
634
  err = Bolt::Error.new("Could not find route #{request.path}",
555
635
  'boltserver/not-found')