pdk 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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