bolt 2.31.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.

@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'set'
5
+
6
+ require 'bolt/error'
7
+
8
+ # This class represents a Git module specification.
9
+ #
10
+ module Bolt
11
+ class ModuleInstaller
12
+ class Specs
13
+ class GitSpec
14
+ NAME_REGEX = %r{\A(?:[a-z][a-z0-9_]*[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
15
+ REQUIRED_KEYS = Set.new(%w[git ref]).freeze
16
+
17
+ attr_reader :git, :ref, :type
18
+
19
+ def initialize(init_hash)
20
+ @name = parse_name(init_hash['name'])
21
+ @git, @repo = parse_git(init_hash['git'])
22
+ @ref = init_hash['ref']
23
+ @type = :git
24
+ end
25
+
26
+ def self.implements?(hash)
27
+ REQUIRED_KEYS == hash.keys.to_set
28
+ end
29
+
30
+ # Parses the name into owner and name segments, and formats the full
31
+ # name.
32
+ #
33
+ private def parse_name(name)
34
+ return unless name
35
+
36
+ unless (match = name.match(NAME_REGEX))
37
+ raise Bolt::ValidationError,
38
+ "Invalid name for Git module specification: #{name}. Name must match "\
39
+ "'name' or 'owner/name', must start with a lowercase letter, and may "\
40
+ "only include lowercase letters, digits, and underscores."
41
+ end
42
+
43
+ match[:name]
44
+ end
45
+
46
+ # Gets the repo from the git URL.
47
+ #
48
+ private def parse_git(git)
49
+ repo = if git.start_with?('git@github.com:')
50
+ git.split('git@github.com:').last.split('.git').first
51
+ elsif git.start_with?('https://github.com')
52
+ git.split('https://github.com/').last.split('.git').first
53
+ else
54
+ raise Bolt::ValidationError,
55
+ "Invalid git source: #{git}. Only GitHub modules are supported."
56
+ end
57
+
58
+ [git, repo]
59
+ end
60
+
61
+ # Returns true if the specification is satisfied by the module.
62
+ #
63
+ def satisfied_by?(mod)
64
+ @type == mod.type && @git == mod.git
65
+ end
66
+
67
+ # Returns a hash matching the module spec in bolt-project.yaml
68
+ #
69
+ def to_hash
70
+ {
71
+ 'git' => @git,
72
+ 'ref' => @ref
73
+ }
74
+ end
75
+
76
+ # Returns a PuppetfileResolver::Model::GitModule object for resolving.
77
+ #
78
+ def to_resolver_module
79
+ require 'puppetfile-resolver'
80
+
81
+ PuppetfileResolver::Puppetfile::GitModule.new(name).tap do |mod|
82
+ mod.remote = @git
83
+ mod.ref = sha
84
+ end
85
+ end
86
+
87
+ # Resolves the module's title from the module metadata. This is lazily
88
+ # resolved since Bolt does not always need to know a Git module's name.
89
+ #
90
+ def name
91
+ @name ||= begin
92
+ url = "https://raw.githubusercontent.com/#{@repo}/#{sha}/metadata.json"
93
+ response = make_request(:Get, url)
94
+
95
+ case response
96
+ when Net::HTTPOK
97
+ body = JSON.parse(response.body)
98
+
99
+ unless body.key?('name')
100
+ raise Bolt::Error.new(
101
+ "Missing name in metadata.json at #{git}. This is not a valid module.",
102
+ "bolt/missing-module-name-error"
103
+ )
104
+ end
105
+
106
+ parse_name(body['name'])
107
+ else
108
+ raise Bolt::Error.new(
109
+ "Missing metadata.json at #{git}. This is not a valid module.",
110
+ "bolt/missing-module-metadata-error"
111
+ )
112
+ end
113
+ end
114
+ end
115
+
116
+ # Resolves the SHA for the specified ref. This is lazily resolved since
117
+ # Bolt does not always need to know a Git module's SHA.
118
+ #
119
+ def sha
120
+ @sha ||= begin
121
+ url = "https://api.github.com/repos/#{@repo}/commits/#{ref}"
122
+ headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
123
+ response = make_request(:Get, url, headers)
124
+
125
+ case response
126
+ when Net::HTTPOK
127
+ body = JSON.parse(response.body)
128
+ body['sha']
129
+ when Net::HTTPUnauthorized
130
+ raise Bolt::Error.new(
131
+ "Invalid token at GITHUB_TOKEN, unable to resolve git modules.",
132
+ "bolt/invalid-git-token-error"
133
+ )
134
+ when Net::HTTPForbidden
135
+ message = "GitHub API rate limit exceeded, unable to resolve git modules. "
136
+
137
+ unless ENV['GITHUB_TOKEN']
138
+ message += "To increase your rate limit, set the GITHUB_TOKEN environment "\
139
+ "variable with a GitHub personal access token."
140
+ end
141
+
142
+ raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
143
+ when Net::HTTPNotFound
144
+ raise Bolt::Error.new(
145
+ "#{git} is not a git repository.",
146
+ "bolt/missing-git-repository-error"
147
+ )
148
+ else
149
+ raise Bolt::Error.new(
150
+ "Ref #{ref} at #{git} is not a commit, tag, or branch.",
151
+ "bolt/invalid-git-ref-error"
152
+ )
153
+ end
154
+ end
155
+ end
156
+
157
+ # Makes a generic HTTP request.
158
+ #
159
+ private def make_request(verb, url, headers = {})
160
+ require 'net/http'
161
+
162
+ uri = URI.parse(url)
163
+ opts = { use_ssl: uri.scheme == 'https' }
164
+
165
+ Net::HTTP.start(uri.host, uri.port, opts) do |client|
166
+ request = Net::HTTP.const_get(verb).new(uri, headers)
167
+ client.request(request)
168
+ end
169
+ rescue StandardError => e
170
+ raise Bolt::Error.new(
171
+ "Failed to connect to #{uri}: #{e.message}",
172
+ "bolt/http-connect-error"
173
+ )
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -27,10 +27,6 @@ module Bolt
27
27
  string.gsub(/^/, indent.to_s)
28
28
  end
29
29
 
30
- def print_message_event(event)
31
- print_message(stringify(event[:message]))
32
- end
33
-
34
30
  def print_message
35
31
  raise NotImplementedError, "print_message() must be implemented by the outputter class"
36
32
  end
@@ -38,49 +34,6 @@ module Bolt
38
34
  def print_error
39
35
  raise NotImplementedError, "print_error() must be implemented by the outputter class"
40
36
  end
41
-
42
- def stringify(message)
43
- formatted = format_message(message)
44
- if formatted.is_a?(Hash) || formatted.is_a?(Array)
45
- ::JSON.pretty_generate(formatted)
46
- else
47
- formatted
48
- end
49
- end
50
-
51
- def format_message(message)
52
- case message
53
- when Array
54
- message.map { |item| format_message(item) }
55
- when Bolt::ApplyResult
56
- format_apply_result(message)
57
- when Bolt::Result, Bolt::ResultSet
58
- # This is equivalent to to_s, but formattable
59
- message.to_data
60
- when Bolt::RunFailure
61
- formatted_resultset = message.result_set.to_data
62
- message.to_h.merge('result_set' => formatted_resultset)
63
- when Hash
64
- message.each_with_object({}) do |(k, v), h|
65
- h[format_message(k)] = format_message(v)
66
- end
67
- when Integer, Float, NilClass
68
- message
69
- else
70
- message.to_s
71
- end
72
- end
73
-
74
- def format_apply_result(result)
75
- logs = result.resource_logs&.map do |log|
76
- # Omit low-level info/debug messages
77
- next if %w[info debug].include?(log['level'])
78
- indent(2, format_log(log))
79
- end
80
- hash = result.to_data
81
- hash['logs'] = logs unless logs.empty?
82
- hash
83
- end
84
37
  end
85
38
  end
86
39
 
@@ -45,7 +45,7 @@ module Bolt
45
45
  when :disable_default_output
46
46
  @disable_depth += 1
47
47
  when :message
48
- print_message_event(event)
48
+ print_message(event[:message])
49
49
  end
50
50
 
51
51
  if enabled?
@@ -344,7 +344,7 @@ module Bolt
344
344
  end
345
345
 
346
346
  @stream.puts "INVENTORY FILE:"
347
- if inventoryfile.exist?
347
+ if File.exist?(inventoryfile)
348
348
  @stream.puts inventoryfile
349
349
  else
350
350
  @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
@@ -22,7 +22,7 @@ module Bolt
22
22
  when :node_result
23
23
  print_result(event[:result])
24
24
  when :message
25
- print_message_event(event)
25
+ print_message(event[:message])
26
26
  end
27
27
  end
28
28
 
@@ -210,14 +210,9 @@ module Bolt
210
210
  raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
211
211
  end
212
212
 
213
- @data['modules'].each do |mod|
214
- next if (mod.is_a?(Hash) && mod.key?('name')) || mod.is_a?(String)
215
- raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash with a name key"
216
- end
217
-
218
- unknown_keys = modules.flat_map(&:keys).uniq - %w[name version_requirement]
219
- if unknown_keys.any?
220
- @logs << { warn: "Ignoring unknown keys in module declarations: #{unknown_keys.join(', ')}." }
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"
221
216
  end
222
217
  end
223
218
  end
@@ -42,12 +42,14 @@ module Bolt
42
42
  # to the new moduledir.
43
43
  #
44
44
  private def migrate_modules_from_puppetfile(config, puppetfile_path, managed_moduledir, modulepath)
45
- require 'bolt/puppetfile'
46
- require 'bolt/puppetfile/installer'
45
+ require 'bolt/module_installer/installer'
46
+ require 'bolt/module_installer/puppetfile'
47
+ require 'bolt/module_installer/resolver'
48
+ require 'bolt/module_installer/specs'
47
49
 
48
50
  begin
49
51
  @outputter.print_action_step("Parsing Puppetfile at #{puppetfile_path}")
50
- puppetfile = Bolt::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
52
+ puppetfile = Bolt::ModuleInstaller::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true)
51
53
  rescue Bolt::Error => e
52
54
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
53
55
  return false
@@ -56,14 +58,14 @@ module Bolt
56
58
  # Prompt for direct dependencies
57
59
  modules = select_modules(puppetfile.modules)
58
60
 
59
- # Create new Puppetfile object
60
- puppetfile = Bolt::Puppetfile.new(modules)
61
+ # Create specs to resolve from
62
+ specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
61
63
 
62
64
  # Attempt to resolve dependencies
63
65
  begin
64
66
  @outputter.print_message('')
65
67
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
66
- puppetfile.resolve
68
+ puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs)
67
69
  rescue Bolt::Error => e
68
70
  @outputter.print_action_error("#{e.message}\nSkipping module migration.")
69
71
  return false
@@ -98,7 +100,7 @@ module Bolt
98
100
 
99
101
  # Install Puppetfile
100
102
  @outputter.print_action_step("Syncing modules from #{puppetfile_path} to #{managed_moduledir}")
101
- Bolt::Puppetfile::Installer.new({}).install(puppetfile_path, managed_moduledir)
103
+ Bolt::ModuleInstaller::Installer.new.install(puppetfile_path, managed_moduledir)
102
104
  else
103
105
  @outputter.print_action_step(
104
106
  "Project does not include any managed modules, deleting Puppetfile "\
@@ -123,7 +125,7 @@ module Bolt
123
125
  return modules if all
124
126
 
125
127
  modules.select do |mod|
126
- Bolt::Util.prompt_yes_no("Select #{mod.title}?", @outputter)
128
+ Bolt::Util.prompt_yes_no("Select #{mod.full_name}?", @outputter)
127
129
  end
128
130
  end
129
131
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.31.0'
4
+ VERSION = '2.32.0'
5
5
  end
@@ -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
  }
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.31.0
4
+ version: 2.32.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-10-19 00:00:00.000000000 Z
11
+ date: 2020-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -182,16 +182,22 @@ dependencies:
182
182
  name: puppet
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - '='
185
+ - - ">="
186
186
  - !ruby/object:Gem::Version
187
187
  version: 6.18.0
188
+ - - "<="
189
+ - !ruby/object:Gem::Version
190
+ version: '6.19'
188
191
  type: :runtime
189
192
  prerelease: false
190
193
  version_requirements: !ruby/object:Gem::Requirement
191
194
  requirements:
192
- - - '='
195
+ - - ">="
193
196
  - !ruby/object:Gem::Version
194
197
  version: 6.18.0
198
+ - - "<="
199
+ - !ruby/object:Gem::Version
200
+ version: '6.19'
195
201
  - !ruby/object:Gem::Dependency
196
202
  name: puppetfile-resolver
197
203
  requirement: !ruby/object:Gem::Requirement
@@ -432,6 +438,8 @@ files:
432
438
  - bolt-modules/system/lib/puppet/functions/system/env.rb
433
439
  - exe/bolt
434
440
  - guides/inventory.txt
441
+ - guides/module.txt
442
+ - guides/modulepath.txt
435
443
  - guides/project.txt
436
444
  - lib/bolt.rb
437
445
  - lib/bolt/analytics.rb
@@ -463,6 +471,15 @@ files:
463
471
  - lib/bolt/logger.rb
464
472
  - lib/bolt/module.rb
465
473
  - lib/bolt/module_installer.rb
474
+ - lib/bolt/module_installer/installer.rb
475
+ - lib/bolt/module_installer/puppetfile.rb
476
+ - lib/bolt/module_installer/puppetfile/forge_module.rb
477
+ - lib/bolt/module_installer/puppetfile/git_module.rb
478
+ - lib/bolt/module_installer/puppetfile/module.rb
479
+ - lib/bolt/module_installer/resolver.rb
480
+ - lib/bolt/module_installer/specs.rb
481
+ - lib/bolt/module_installer/specs/forge_spec.rb
482
+ - lib/bolt/module_installer/specs/git_spec.rb
466
483
  - lib/bolt/node/errors.rb
467
484
  - lib/bolt/node/output.rb
468
485
  - lib/bolt/outputter.rb
@@ -504,9 +521,6 @@ files:
504
521
  - lib/bolt/puppetdb.rb
505
522
  - lib/bolt/puppetdb/client.rb
506
523
  - lib/bolt/puppetdb/config.rb
507
- - lib/bolt/puppetfile.rb
508
- - lib/bolt/puppetfile/installer.rb
509
- - lib/bolt/puppetfile/module.rb
510
524
  - lib/bolt/r10k_log_proxy.rb
511
525
  - lib/bolt/rerun.rb
512
526
  - lib/bolt/resource_instance.rb