pdk 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  require 'bundler'
2
+ require 'digest'
2
3
  require 'fileutils'
3
4
  require 'pdk/util'
4
5
  require 'pdk/cli/exec'
@@ -8,11 +9,14 @@ module PDK
8
9
  module Bundler
9
10
  class BundleHelper; end
10
11
 
11
- def self.ensure_bundle!
12
+ def self.ensure_bundle!(gem_overrides = nil)
12
13
  bundle = BundleHelper.new
13
14
 
14
- if already_bundled?(bundle.gemfile)
15
- PDK.logger.debug(_('Bundle has already been installed. Skipping run.'))
15
+ # This will default ensure_bundle! to re-resolving everything to latest
16
+ gem_overrides ||= { puppet: nil, hiera: nil, facter: nil }
17
+
18
+ if already_bundled?(bundle.gemfile, gem_overrides)
19
+ PDK.logger.debug(_('Bundler managed gems already up to date.'))
16
20
  return
17
21
  end
18
22
 
@@ -22,64 +26,83 @@ module PDK
22
26
  end
23
27
 
24
28
  unless bundle.locked?
25
- if PDK::Util.package_install?
26
- # In packaged installs, try to use vendored Gemfile.lock as a starting point.
27
- # The 'bundle install' below will pick up any new dependencies.
28
- vendored_gemfile_lock = File.join(PDK::Util.package_cachedir, 'Gemfile.lock')
29
+ # Generate initial default Gemfile.lock, either from package cache or
30
+ # by invoking `bundle lock`
31
+ bundle.lock!
32
+ end
29
33
 
30
- if File.exist?(vendored_gemfile_lock)
31
- PDK.logger.debug(_("No Gemfile.lock found in module. Using vendored Gemfile.lock from '%{source}'.") % { source: vendored_gemfile_lock })
32
- FileUtils.cp(vendored_gemfile_lock, File.join(PDK::Util.module_root, 'Gemfile.lock'))
33
- end
34
- else
35
- # In non-packaged installs, just let bundler resolve deps as normal.
36
- unless bundle.lock!
37
- raise PDK::CLI::FatalError, _('Unable to resolve Gemfile dependencies.')
38
- end
39
- end
34
+ # Check if all dependencies will be available once we update the lockfile.
35
+ begin
36
+ original_lockfile = bundle.gemfile_lock
37
+ temp_lockfile = "#{original_lockfile}.tmp"
38
+
39
+ FileUtils.mv(original_lockfile, temp_lockfile)
40
+
41
+ all_deps_available = bundle.installed?(gem_overrides)
42
+ ensure
43
+ FileUtils.mv(temp_lockfile, original_lockfile, force: true)
40
44
  end
41
45
 
46
+ bundle.update_lock!(with: gem_overrides, local: all_deps_available)
47
+
48
+ # If there are missing dependencies after updating the lockfile, let `bundle install`
49
+ # go out and get them.
42
50
  unless bundle.installed?
43
- unless bundle.install!
44
- raise PDK::CLI::FatalError, _('Unable to install missing Gemfile dependencies.')
45
- end
51
+ bundle.install!(gem_overrides)
46
52
  end
47
53
 
48
- mark_as_bundled!(bundle.gemfile)
54
+ mark_as_bundled!(bundle.gemfile, gem_overrides)
49
55
  end
50
56
 
51
- def self.already_bundled?(gemfile)
52
- !(@bundled ||= {})[gemfile].nil?
57
+ def self.ensure_binstubs!(*gems)
58
+ bundle = BundleHelper.new
59
+
60
+ bundle.binstubs!(gems)
53
61
  end
54
62
 
55
- def self.mark_as_bundled!(gemfile)
56
- (@bundled ||= {})[gemfile] = true
63
+ def self.already_bundled?(gemfile, gem_overrides)
64
+ !(@bundled ||= {})[bundle_cache_key(gemfile, gem_overrides)].nil?
57
65
  end
58
66
 
59
- def self.ensure_binstubs!(*gems)
60
- bundle = BundleHelper.new
67
+ def self.mark_as_bundled!(gemfile, gem_overrides)
68
+ (@bundled ||= {})[bundle_cache_key(gemfile, gem_overrides)] = true
69
+ end
61
70
 
62
- unless bundle.binstubs!(gems) # rubocop:disable Style/GuardClause
63
- raise PDK::CLI::FatalError, _('Unable to install requested binstubs.')
64
- end
71
+ def self.bundle_cache_key(gemfile, gem_overrides)
72
+ override_sig = (gem_overrides || {}).sort_by { |gem, _| gem.to_s }.to_s
73
+ Digest::MD5.hexdigest(gemfile.to_s + override_sig)
65
74
  end
75
+ private_class_method :bundle_cache_key
66
76
 
67
77
  class BundleHelper
78
+ def gemfile
79
+ @gemfile ||= PDK::Util.find_upwards('Gemfile')
80
+ end
81
+
82
+ def gemfile_lock
83
+ return nil if gemfile.nil?
84
+ @gemfile_lock ||= File.join(File.dirname(gemfile), 'Gemfile.lock')
85
+ end
86
+
68
87
  def gemfile?
69
88
  !gemfile.nil?
70
89
  end
71
90
 
72
91
  def locked?
73
- !gemfile_lock.nil?
92
+ !gemfile_lock.nil? && File.file?(gemfile_lock)
74
93
  end
75
94
 
76
- def installed?
95
+ def installed?(gem_overrides = {})
77
96
  PDK.logger.debug(_('Checking for missing Gemfile dependencies.'))
78
97
 
79
- argv = ['check', "--gemfile=#{gemfile}"]
98
+ argv = ['check', "--gemfile=#{gemfile}", '--dry-run']
80
99
  argv << "--path=#{bundle_cachedir}" unless PDK::Util.package_install?
81
100
 
82
- result = bundle_command(*argv).execute!
101
+ cmd = bundle_command(*argv).tap do |c|
102
+ c.update_environment(gemfile_env(gem_overrides)) unless gem_overrides.empty?
103
+ end
104
+
105
+ result = cmd.execute!
83
106
 
84
107
  unless result[:exit_code].zero?
85
108
  PDK.logger.debug(result.values_at(:stdout, :stderr).join("\n"))
@@ -89,67 +112,141 @@ module PDK
89
112
  end
90
113
 
91
114
  def lock!
92
- command = bundle_command('lock').tap do |c|
93
- c.add_spinner(_('Resolving Gemfile dependencies.'))
115
+ if PDK::Util.package_install?
116
+ # In packaged installs, use vendored Gemfile.lock as a starting point.
117
+ # Subsequent 'bundle install' will still pick up any new dependencies.
118
+ vendored_lockfiles = [
119
+ File.join(PDK::Util.package_cachedir, "Gemfile-#{PDK::Util::RubyVersion.active_ruby_version}.lock"),
120
+ File.join(PDK::Util.package_cachedir, 'Gemfile.lock'),
121
+ ]
122
+
123
+ vendored_gemfile_lock = vendored_lockfiles.find { |lockfile| File.exist?(lockfile) }
124
+
125
+ unless vendored_gemfile_lock
126
+ raise PDK::CLI::FatalError, _('Vendored Gemfile.lock (%{source}) not found.') % {
127
+ source: vendored_gemfile_lock,
128
+ }
129
+ end
130
+
131
+ PDK.logger.debug(_('Using vendored Gemfile.lock from %{source}.') % { source: vendored_gemfile_lock })
132
+ FileUtils.cp(vendored_gemfile_lock, File.join(PDK::Util.module_root, 'Gemfile.lock'))
133
+ else
134
+ argv = ['lock']
135
+
136
+ cmd = bundle_command(*argv).tap do |c|
137
+ c.add_spinner(_('Resolving default Gemfile dependencies.'))
138
+ end
139
+
140
+ result = cmd.execute!
141
+
142
+ unless result[:exit_code].zero?
143
+ PDK.logger.fatal(result.values_at(:stdout, :stderr).join("\n"))
144
+ raise PDK::CLI::FatalError, _('Unable to resolve default Gemfile dependencies.')
145
+ end
146
+
147
+ # After initial lockfile generation, re-resolve json gem to built-in
148
+ # version to avoid unncessary native compilation attempts. For packaged
149
+ # installs this is done during the generation of the vendored Gemfile.lock
150
+ update_lock!(only: { json: nil }, local: true)
151
+ end
152
+
153
+ true
154
+ end
155
+
156
+ def update_lock!(options = {})
157
+ PDK.logger.debug(_('Updating Gemfile dependencies.'))
158
+
159
+ argv = ['lock', '--update']
160
+
161
+ overrides = nil
162
+
163
+ if options && options[:only]
164
+ update_gems = options[:only].keys.map(&:to_s)
165
+ argv << update_gems
166
+ argv.flatten!
167
+
168
+ overrides = options[:only]
169
+ elsif options && options[:with]
170
+ overrides = options[:with]
171
+ end
172
+
173
+ argv << '--local' if options && options[:local]
174
+ argv << '--conservative' if options && options[:conservative]
175
+
176
+ cmd = bundle_command(*argv).tap do |c|
177
+ c.update_environment(gemfile_env(overrides)) if overrides
94
178
  end
95
179
 
96
- result = command.execute!
180
+ result = cmd.execute!
97
181
 
98
182
  unless result[:exit_code].zero?
99
183
  PDK.logger.fatal(result.values_at(:stdout, :stderr).join("\n"))
184
+ raise PDK::CLI::FatalError, _('Unable to resolve Gemfile dependencies.')
100
185
  end
101
186
 
102
- result[:exit_code].zero?
187
+ true
103
188
  end
104
189
 
105
- def install!
190
+ def install!(gem_overrides = {})
106
191
  argv = ['install', "--gemfile=#{gemfile}", '-j4']
107
192
  argv << "--path=#{bundle_cachedir}" unless PDK::Util.package_install?
108
193
 
109
- command = bundle_command(*argv).tap do |c|
194
+ cmd = bundle_command(*argv).tap do |c|
110
195
  c.add_spinner(_('Installing missing Gemfile dependencies.'))
196
+ c.update_environment(gemfile_env(gem_overrides)) unless gem_overrides.empty?
111
197
  end
112
198
 
113
- result = command.execute!
199
+ result = cmd.execute!
114
200
 
115
201
  unless result[:exit_code].zero?
116
202
  PDK.logger.fatal(result.values_at(:stdout, :stderr).join("\n"))
203
+ raise PDK::CLI::FatalError, _('Unable to install missing Gemfile dependencies.')
117
204
  end
118
205
 
119
- result[:exit_code].zero?
206
+ true
120
207
  end
121
208
 
122
209
  def binstubs!(gems)
123
210
  binstub_dir = File.join(File.dirname(gemfile), 'bin')
124
211
  return true if gems.all? { |gem| File.file?(File.join(binstub_dir, gem)) }
125
212
 
126
- command = bundle_command('binstubs', *gems, '--force')
127
-
128
- result = command.execute!
213
+ cmd = bundle_command('binstubs', *gems, '--force')
214
+ result = cmd.execute!
129
215
 
130
216
  unless result[:exit_code].zero?
131
217
  PDK.logger.fatal(_("Failed to generate binstubs for '%{gems}':\n%{output}") % { gems: gems.join(' '), output: result.values_at(:stdout, :stderr).join("\n") })
218
+ raise PDK::CLI::FatalError, _('Unable to install requested binstubs.')
132
219
  end
133
220
 
134
- result[:exit_code].zero?
221
+ true
135
222
  end
136
223
 
137
- def gemfile
138
- @gemfile ||= PDK::Util.find_upwards('Gemfile')
224
+ def self.gemfile_env(gem_overrides)
225
+ gemfile_env = {}
226
+
227
+ return gemfile_env unless gem_overrides.respond_to?(:each)
228
+
229
+ gem_overrides.each do |gem, version|
230
+ gemfile_env['PUPPET_GEM_VERSION'] = version if gem.respond_to?(:to_s) && gem.to_s == 'puppet' && !version.nil?
231
+ gemfile_env['FACTER_GEM_VERSION'] = version if gem.respond_to?(:to_s) && gem.to_s == 'facter' && !version.nil?
232
+ gemfile_env['HIERA_GEM_VERSION'] = version if gem.respond_to?(:to_s) && gem.to_s == 'hiera' && !version.nil?
233
+ end
234
+
235
+ gemfile_env
139
236
  end
140
237
 
141
238
  private
142
239
 
240
+ def gemfile_env(gem_overrides)
241
+ self.class.gemfile_env(gem_overrides)
242
+ end
243
+
143
244
  def bundle_command(*args)
144
245
  PDK::CLI::Exec::Command.new(PDK::CLI::Exec.bundle_bin, *args).tap do |c|
145
246
  c.context = :module
146
247
  end
147
248
  end
148
249
 
149
- def gemfile_lock
150
- @gemfile_lock ||= PDK::Util.find_upwards('Gemfile.lock')
151
- end
152
-
153
250
  def bundle_cachedir
154
251
  @bundle_cachedir ||= PDK::Util.package_install? ? PDK::Util.package_cachedir : File.join(PDK::Util.cachedir)
155
252
  end
@@ -32,10 +32,27 @@ module PDK
32
32
  PDK::CLI::Exec.execute(git_bin, *args)
33
33
  end
34
34
 
35
- def self.repo_exists?(repo, ref = nil)
36
- args = ['ls-remote', '--exit-code', repo, ref].compact
35
+ def self.git_with_env(env, *args)
36
+ PDK::CLI::Exec.ensure_bin_present!(git_bin, 'git')
37
+
38
+ PDK::CLI::Exec.execute_with_env(env, git_bin, *args)
39
+ end
40
+
41
+ def self.repo?(maybe_repo)
42
+ return bare_repo?(maybe_repo) if File.directory?(maybe_repo)
43
+
44
+ remote_repo?(maybe_repo)
45
+ end
46
+
47
+ def self.bare_repo?(maybe_repo)
48
+ env = { 'GIT_DIR' => maybe_repo }
49
+ rev_parse = git_with_env(env, 'rev-parse', '--is-bare-repository')
50
+
51
+ rev_parse[:exit_code].zero? && rev_parse[:stdout].strip == 'true'
52
+ end
37
53
 
38
- git(*args)[:exit_code].zero?
54
+ def self.remote_repo?(maybe_repo)
55
+ git('ls-remote', '--exit-code', maybe_repo)[:exit_code].zero?
39
56
  end
40
57
 
41
58
  def self.ls_remote(repo, ref)
@@ -0,0 +1,182 @@
1
+ require 'pdk/util'
2
+
3
+ module PDK
4
+ module Util
5
+ class PuppetVersion
6
+ class << self
7
+ extend Forwardable
8
+
9
+ def_delegators :instance, :find_gem_for, :from_pe_version, :from_module_metadata, :latest_available
10
+
11
+ attr_writer :instance
12
+
13
+ def instance
14
+ @instance ||= new
15
+ end
16
+ end
17
+
18
+ PE_VERSIONS_URL = 'https://forgeapi.puppet.com/private/versions/pe'.freeze
19
+
20
+ def latest_available
21
+ latest = find_gem(Gem::Requirement.create('>= 0'))
22
+
23
+ if latest.nil?
24
+ raise ArgumentError, _('Unable to find a Puppet gem in current Ruby environment or from Rubygems.org.')
25
+ end
26
+
27
+ latest
28
+ end
29
+
30
+ def find_gem_for(version_str)
31
+ version = parse_specified_version(version_str)
32
+
33
+ # Look for a gem matching exactly the version passed in.
34
+ if version.segments.length == 3
35
+ exact_match_gem = find_gem(Gem::Requirement.create(version))
36
+ return exact_match_gem unless exact_match_gem.nil?
37
+ end
38
+
39
+ # Construct a pessimistic version constraint to find the latest
40
+ # available gem matching the level of specificity of version_str.
41
+ requirement_string = version.approximate_recommendation
42
+ requirement_string += '.0' unless version.segments.length == 1
43
+ latest_requirement = Gem::Requirement.create(requirement_string)
44
+
45
+ latest_available_gem = find_gem(latest_requirement)
46
+
47
+ if latest_available_gem.nil?
48
+ raise ArgumentError, _('Unable to find a Puppet gem matching %{requirement}.') % {
49
+ requirement: latest_requirement,
50
+ }
51
+ end
52
+
53
+ # Only issue this warning if they requested an exact version that isn't available.
54
+ if version.segments.length == 3
55
+ PDK.logger.warn(_('Puppet %{requested_version} is not available, activating %{found_version} instead.') % {
56
+ requested_version: version_str,
57
+ found_version: latest_available_gem[:gem_version].version,
58
+ })
59
+ end
60
+
61
+ latest_available_gem
62
+ end
63
+
64
+ def from_pe_version(version_str)
65
+ version = parse_specified_version(version_str)
66
+
67
+ gem_version = pe_version_map.find do |version_map|
68
+ version_map[:requirement].satisfied_by?(version)
69
+ end
70
+
71
+ if gem_version.nil?
72
+ raise ArgumentError, _('Unable to map Puppet Enterprise version %{pe_version} to a Puppet version.') % {
73
+ pe_version: version_str,
74
+ }
75
+ end
76
+
77
+ PDK.logger.info _('Puppet Enterprise %{pe_version} maps to Puppet %{puppet_version}.') % {
78
+ pe_version: version_str,
79
+ puppet_version: gem_version[:gem_version],
80
+ }
81
+
82
+ find_gem_for(gem_version[:gem_version])
83
+ end
84
+
85
+ def from_module_metadata(metadata = nil)
86
+ metadata ||= PDK::Module::Metadata.from_file(PDK::Util.find_upwards('metadata.json'))
87
+ metadata.validate_puppet_version_requirement!
88
+ metadata_requirement = metadata.puppet_requirement
89
+
90
+ # Split combined requirements like ">= 4.7.0 < 6.0.0" into their
91
+ # component requirements [">= 4.7.0", "< 6.0.0"]
92
+ pattern = %r{#{Gem::Requirement::PATTERN_RAW}}
93
+ requirement_strings = metadata_requirement['version_requirement'].scan(pattern).map do |req|
94
+ req.compact.join(' ')
95
+ end
96
+
97
+ gem_requirement = Gem::Requirement.create(requirement_strings)
98
+ find_gem(gem_requirement)
99
+ end
100
+
101
+ private
102
+
103
+ def parse_specified_version(version_str)
104
+ Gem::Version.new(version_str)
105
+ rescue ArgumentError
106
+ raise ArgumentError, _('%{version} is not a valid version number.') % {
107
+ version: version_str,
108
+ }
109
+ end
110
+
111
+ def pe_version_map
112
+ @pe_version_map ||= fetch_pe_version_map.map { |version_map|
113
+ maps = version_map['versions'].map do |pe_release|
114
+ requirements = ["= #{pe_release['version']}"]
115
+
116
+ # Some PE release have a .0 Z release, which causes problems when
117
+ # the user specifies "X.Y" expecting to get the latest Z and
118
+ # instead getting the oldest.
119
+ requirements << "!= #{pe_release['version'].gsub(%r{\.\d+\Z}, '')}" if pe_release['version'].end_with?('.0')
120
+ {
121
+ requirement: Gem::Requirement.create(requirements),
122
+ gem_version: pe_release['puppet'],
123
+ }
124
+ end
125
+
126
+ maps << {
127
+ requirement: requirement_from_forge_range(version_map['release']),
128
+ gem_version: version_map['versions'].find { |r| r['version'] == version_map['latest'] }['puppet'],
129
+ }
130
+ }.flatten
131
+ end
132
+
133
+ def fetch_pe_version_map
134
+ map = PDK::Util::VendoredFile.new('pe_versions.json', PE_VERSIONS_URL).read
135
+
136
+ JSON.parse(map)
137
+ rescue PDK::Util::VendoredFile::DownloadError => e
138
+ raise PDK::CLI::FatalError, e.message
139
+ rescue JSON::ParserError
140
+ raise PDK::CLI::FatalError, _('Failed to parse Puppet Enterprise version map file.')
141
+ end
142
+
143
+ def requirement_from_forge_range(range_str)
144
+ Gem::Requirement.create("~> #{range_str.gsub(%r{\.x\Z}, '.0')}")
145
+ end
146
+
147
+ def rubygems_puppet_versions
148
+ return @rubygems_puppet_versions unless @rubygems_puppet_versions.nil?
149
+
150
+ fetcher = Gem::SpecFetcher.fetcher
151
+ puppet_tuples = fetcher.detect(:released) do |spec_tuple|
152
+ spec_tuple.name == 'puppet' && Gem::Platform.match(spec_tuple.platform)
153
+ end
154
+ puppet_versions = puppet_tuples.map { |name, _| name.version }.uniq
155
+ @rubygems_puppet_versions = puppet_versions.sort { |a, b| b <=> a }
156
+ end
157
+
158
+ def find_gem(requirement)
159
+ if PDK::Util.package_install?
160
+ find_in_package_cache(requirement)
161
+ else
162
+ find_in_rubygems(requirement)
163
+ end
164
+ end
165
+
166
+ def find_in_rubygems(requirement)
167
+ version = rubygems_puppet_versions.find { |r| requirement.satisfied_by?(r) }
168
+ version.nil? ? nil : { gem_version: version, ruby_version: PDK::Util::RubyVersion.default_ruby_version }
169
+ end
170
+
171
+ def find_in_package_cache(requirement)
172
+ PDK::Util::RubyVersion.versions.each do |ruby_version, _|
173
+ PDK::Util::RubyVersion.use(ruby_version)
174
+ version = PDK::Util::RubyVersion.available_puppet_versions.find { |r| requirement.satisfied_by?(r) }
175
+ return { gem_version: version, ruby_version: ruby_version } unless version.nil?
176
+ end
177
+
178
+ nil
179
+ end
180
+ end
181
+ end
182
+ end