dependabot-opentofu 0.385.0 → 0.387.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bb80d0b504e093127b1fc9a74f482b8a8b73f99698c968bfd0e8b1c198dd026
4
- data.tar.gz: fa2816fbf5b4108eb4bbe9e3acbe13efb6a7dee9aafc385af73ecb404233ed76
3
+ metadata.gz: 2bc33535e90295dc9a6701b45db149a1d6b91c5771a1c6905a76be6568324c7f
4
+ data.tar.gz: e9dbb471d8b1f931cc073167deef98a03edc4be1f12aebb71cbcec511fff895c
5
5
  SHA512:
6
- metadata.gz: bb7b69e38138d4ea57b94df897078e63dbc9bab27ac79d8e6dd5c63528ddc4c1a890afeb18c97688e77d9bd4976304ba13b7cfffd4522c1c2c6b4b489f9a0b28
7
- data.tar.gz: 8fa4575d341d81acbd5a242e800590f6b8326d84cb55c58bcf4b8d84a81c74d53c59425f7676ba2dd4a7c29dfbc7640ad99cbf9520a892b1ffbdf23940a76475
6
+ metadata.gz: 22cef6dcf34daa9849d66cd54fbf87c6fc1c046f58396ce0ec308515776ca5478c9c7b8066f69c10fcee428bebe17cff9d61a41fc9f16c090fa9bc564717619f
7
+ data.tar.gz: a2290ea710a8bf2fbbb0ed18ff55c7b4c9fc56adaea9a6e7f89485b8b16c7f598e1be4684ca8625b9a34faa58b3c2c58f6ba2199c1203f7b8ab9ec6a547066b0
@@ -52,14 +52,14 @@ module Dependabot
52
52
 
53
53
  sig do
54
54
  params(
55
- modules: T::Hash[String, T::Array[T::Hash[String, T.untyped]]],
55
+ modules: T::Hash[String, T::Array[T::Hash[String, Object]]],
56
56
  base_modules: T::Hash[String,
57
57
  T::Array[T::Hash[String,
58
- T.untyped]]]
58
+ Object]]]
59
59
  )
60
60
  .returns(T::Hash[String,
61
61
  T::Array[T::Hash[String,
62
- T.untyped]]])
62
+ Object]]])
63
63
  end
64
64
  def merge_modules(modules, base_modules)
65
65
  merged_modules = base_modules.dup
@@ -7,11 +7,12 @@ require "dependabot/file_updaters"
7
7
  require "dependabot/file_updaters/base"
8
8
  require "dependabot/errors"
9
9
  require "dependabot/opentofu/file_selector"
10
+ require "dependabot/opentofu/registry_client"
10
11
  require "dependabot/shared_helpers"
11
12
 
12
13
  module Dependabot
13
14
  module Opentofu
14
- class FileUpdater < Dependabot::FileUpdaters::Base
15
+ class FileUpdater < Dependabot::FileUpdaters::Base # rubocop:disable Metrics/ClassLength
15
16
  extend T::Sig
16
17
 
17
18
  include FileSelector
@@ -234,17 +235,52 @@ module Dependabot
234
235
  end
235
236
 
236
237
  sig { returns(T.nilable(T::Array[Symbol])) }
237
- def lookup_hash_architecture # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
238
+ def lookup_hash_architecture
238
239
  new_req = T.must(dependency.requirements.first)
239
240
 
240
241
  # NOTE: Only providers are included in the lockfile, modules are not
241
242
  return unless new_req[:source][:type] == "provider"
242
243
 
244
+ result = lookup_hash_architecture_from_registry(new_req)
245
+ return result if result
246
+
247
+ lookup_hash_architecture_from_cli(new_req)
248
+ end
249
+
250
+ sig { params(new_req: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[Symbol])) }
251
+ def lookup_hash_architecture_from_registry(new_req) # rubocop:disable Metrics/PerceivedComplexity
252
+ content, _provider_source, declaration_regex = lockfile_details(new_req)
253
+ existing_h1 = extract_provider_h1_hashes(content, declaration_regex)
254
+ return nil if existing_h1.empty?
255
+
256
+ identifier = new_req[:source][:module_identifier]
257
+ old_version = dependency.previous_version
258
+ return nil unless old_version
259
+
260
+ hostname = new_req[:source][:registry_hostname] || RegistryClient::PUBLIC_HOSTNAME
261
+ client = RegistryClient.new(hostname: hostname, credentials: credentials)
262
+ packages = client.all_provider_package_hashes(identifier: identifier, version: old_version)
263
+ return nil unless packages
264
+
265
+ h1_to_platform = {}
266
+ packages.each do |platform, hashes|
267
+ hashes.each do |h|
268
+ h1_to_platform[h] = platform if h.start_with?("h1:")
269
+ end
270
+ end
271
+
272
+ architectures = existing_h1.filter_map { |h| h1_to_platform[h]&.to_sym }
273
+ architectures.empty? ? nil : architectures.uniq
274
+ rescue Dependabot::DependabotError
275
+ nil
276
+ end
277
+
278
+ sig { params(new_req: T::Hash[Symbol, T.untyped]).returns(T::Array[Symbol]) }
279
+ def lookup_hash_architecture_from_cli(new_req) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
243
280
  architectures = []
244
281
  content, provider_source, declaration_regex = lockfile_details(new_req)
245
282
  hashes = extract_provider_h1_hashes(content, declaration_regex)
246
283
 
247
- # These are ordered in assumed popularity
248
284
  possible_architectures = %w(
249
285
  linux_amd64
250
286
  darwin_amd64
@@ -256,15 +292,10 @@ module Dependabot
256
292
  base_dir = T.must(dependency_files.first).directory
257
293
  lockfile_hash_removed = remove_provider_h1_hashes(content, declaration_regex)
258
294
 
259
- # This runs in the same directory as the actual lockfile update so
260
- # the platform must be determined before the updated manifest files
261
- # are written to disk
262
295
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
263
296
  possible_architectures.each do |arch|
264
- # Exit early if we have detected all of the architectures present
265
297
  break if architectures.count == hashes.count
266
298
 
267
- # OpenTofu will update the lockfile in place so we use a fresh lockfile for each lookup
268
299
  File.write(".terraform.lock.hcl", lockfile_hash_removed)
269
300
 
270
301
  SharedHelpers.run_shell_command(
@@ -276,7 +307,6 @@ module Dependabot
276
307
  updated_hashes = extract_provider_h1_hashes(updated_lockfile, declaration_regex)
277
308
  next if updated_hashes.nil?
278
309
 
279
- # Check if the architecture is present in the original lockfile
280
310
  hashes.each do |hash|
281
311
  updated_hashes.select { |h| h.match?(/^h1:/) }.each do |updated_hash|
282
312
  architectures.append(arch.to_sym) if hash == updated_hash
@@ -292,7 +322,6 @@ module Dependabot
292
322
  end
293
323
  raise if @retrying_lock || !e.message.include?("tofu init")
294
324
 
295
- # NOTE: Modules need to be installed before OpenTofu can update the lockfile
296
325
  @retrying_lock = true
297
326
  run_opentofu_init
298
327
  retry
@@ -304,32 +333,117 @@ module Dependabot
304
333
  sig { returns(T::Array[Symbol]) }
305
334
  def architecture_type
306
335
  @architecture_type ||= T.let(
307
- if lookup_hash_architecture.nil? || lookup_hash_architecture&.empty?
308
- [:linux_amd64]
309
- else
310
- T.must(lookup_hash_architecture)
336
+ begin
337
+ detected = lookup_hash_architecture
338
+ detected.nil? || detected.empty? ? [:linux_amd64] : detected
311
339
  end,
312
340
  T.nilable(T::Array[Symbol])
313
341
  )
314
342
  end
315
343
 
316
344
  sig { params(updated_manifest_files: T::Array[Dependabot::DependencyFile]).returns(T.nilable(String)) }
317
- def update_lockfile_declaration(updated_manifest_files) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
345
+ def update_lockfile_declaration(updated_manifest_files)
318
346
  return if lockfile.nil?
319
347
 
320
348
  new_req = T.must(dependency.requirements.first)
321
- # NOTE: Only providers are included in the lockfile, modules are not
322
349
  return unless new_req[:source][:type] == "provider"
323
350
 
351
+ result = update_lockfile_from_registry(new_req)
352
+ return result if result
353
+
354
+ update_lockfile_from_cli(new_req, updated_manifest_files)
355
+ end
356
+
357
+ sig { params(new_req: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
358
+ def update_lockfile_from_registry(new_req) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
359
+ content, provider_source, declaration_regex = lockfile_details(new_req)
360
+ platforms = architecture_type
361
+ return nil if platforms.empty?
362
+
363
+ identifier = new_req[:source][:module_identifier]
364
+ new_version = dependency.version
365
+ return nil unless new_version
366
+
367
+ hostname = new_req[:source][:registry_hostname] || RegistryClient::PUBLIC_HOSTNAME
368
+ client = RegistryClient.new(hostname: hostname, credentials: credentials)
369
+ packages = client.all_provider_package_hashes(identifier: identifier, version: new_version)
370
+ return nil unless packages
371
+
372
+ platform_strings = platforms.map(&:to_s)
373
+ return nil unless platform_strings.all? { |p| packages.key?(p) }
374
+
375
+ h1_hashes = platform_strings.flat_map { |p| packages.fetch(p).select { |h| h.start_with?("h1:") } }.uniq.sort
376
+ zh_hashes = packages.values.flat_map { |hs| hs.select { |h| h.start_with?("zh:") } }.uniq.sort
377
+ all_hashes = h1_hashes + zh_hashes
378
+ return nil if all_hashes.empty?
379
+
380
+ new_declaration = build_lockfile_declaration(
381
+ provider_source: provider_source,
382
+ version: new_version,
383
+ constraints: extract_constraints(content, declaration_regex),
384
+ hashes: all_hashes
385
+ )
386
+
387
+ replace_lockfile_provider_block(content, declaration_regex, new_declaration)
388
+ rescue Dependabot::DependabotError
389
+ nil
390
+ end
391
+
392
+ sig do
393
+ params(
394
+ provider_source: String,
395
+ version: String,
396
+ constraints: T.nilable(String),
397
+ hashes: T::Array[String]
398
+ ).returns(String)
399
+ end
400
+ def build_lockfile_declaration(provider_source:, version:, constraints:, hashes:)
401
+ lines = []
402
+ lines << "provider \"#{provider_source}\" {"
403
+ if constraints
404
+ lines << " version = \"#{version}\""
405
+ lines << " constraints = \"#{constraints}\""
406
+ else
407
+ lines << " version = \"#{version}\""
408
+ end
409
+ lines << " hashes = ["
410
+ hashes.each { |h| lines << " \"#{h}\"," }
411
+ lines << " ]"
412
+ lines << "}"
413
+ lines.join("\n")
414
+ end
415
+
416
+ sig { params(content: String, declaration_regex: Regexp, new_declaration: String).returns(String) }
417
+ def replace_lockfile_provider_block(content, declaration_regex, new_declaration)
418
+ provider_block_regex = /provider\s*["'][^"']+["']\s*\{[^}]*\}/m
419
+ content.sub(declaration_regex) do |match|
420
+ match.sub(provider_block_regex, new_declaration)
421
+ end
422
+ end
423
+
424
+ sig { params(content: String, declaration_regex: Regexp).returns(T.nilable(String)) }
425
+ def extract_constraints(content, declaration_regex)
426
+ match = content.match(declaration_regex)
427
+ return nil unless match
428
+
429
+ constraint_match = match.to_s.match(/^\s*constraints\s*=\s*"([^"]*)"/)
430
+ constraint_match ? constraint_match[1] : nil
431
+ end
432
+
433
+ sig do
434
+ params(
435
+ new_req: T::Hash[Symbol, T.untyped],
436
+ updated_manifest_files: T::Array[Dependabot::DependencyFile]
437
+ ).returns(T.nilable(String))
438
+ end
439
+ def update_lockfile_from_cli(new_req, updated_manifest_files) # rubocop:disable Metrics/AbcSize
324
440
  content, provider_source, declaration_regex = lockfile_details(new_req)
325
441
  lockfile_dependency_removed = content.sub(declaration_regex, "")
326
442
 
327
443
  base_dir = T.must(dependency_files.first).directory
328
444
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
329
- # Determine the provider using the original manifest files
330
445
  platforms = architecture_type.map { |arch| "-platform=#{arch}" }.join(" ")
331
446
 
332
- # Update the provider requirements in case the previous requirement doesn't allow the new version
333
447
  updated_manifest_files.each { |f| File.write(f.name, f.content) }
334
448
 
335
449
  File.write(".terraform.lock.hcl", lockfile_dependency_removed)
@@ -342,8 +456,6 @@ module Dependabot
342
456
  updated_lockfile = File.read(".terraform.lock.hcl")
343
457
  updated_dependency = T.cast(updated_lockfile.scan(declaration_regex).first, String)
344
458
 
345
- # OpenTofu will occasionally update h1 hashes without updating the version of the dependency
346
- # Here we make sure the dependency's version actually changes in the lockfile
347
459
  unless T.cast(updated_dependency.scan(declaration_regex).first, String).scan(/^\s*version\s*=.*/) ==
348
460
  T.cast(content.scan(declaration_regex).first, String).scan(/^\s*version\s*=.*/)
349
461
  content.sub!(declaration_regex, updated_dependency)
@@ -358,7 +470,6 @@ module Dependabot
358
470
  end
359
471
  raise if @retrying_lock || !e.message.include?("tofu init")
360
472
 
361
- # NOTE: Modules need to be installed before OpenTofu can update the lockfile
362
473
  @retrying_lock = T.let(true, T.nilable(T::Boolean))
363
474
  run_opentofu_init
364
475
  retry
@@ -130,6 +130,39 @@ module Dependabot
130
130
  raise Dependabot::PrivateSourceBadResponse, host
131
131
  end
132
132
 
133
+ # Fetch the package hashes for every platform of a given provider version.
134
+ # Returns a Hash mapping "<os>_<arch>" strings to arrays of hash strings
135
+ # (e.g. {"linux_amd64" => ["h1:...", "zh:..."], "darwin_arm64" => [...]}).
136
+ # Returns nil when the registry does not include the `packages` field
137
+ # (e.g. the Terraform registry).
138
+ sig do
139
+ params(identifier: String, version: String)
140
+ .returns(T.nilable(T::Hash[String, T::Array[String]]))
141
+ end
142
+ def all_provider_package_hashes(identifier:, version:)
143
+ base_url = service_url_for_registry("providers.v1")
144
+ response = http_get!(URI.join(base_url, "#{identifier}/#{version}/download/linux/amd64"))
145
+ body = JSON.parse(response.body)
146
+
147
+ packages = body["packages"]
148
+ return nil unless packages.is_a?(Hash)
149
+
150
+ packages.each_with_object({}) do |(platform, info), result|
151
+ next unless info.is_a?(Hash)
152
+
153
+ hashes = Array(info["hashes"])
154
+ if hashes.empty?
155
+ Dependabot.logger.debug(
156
+ "No package hashes for #{identifier} v#{version} on platform #{platform}; " \
157
+ "registry returned hashes=#{info['hashes'].inspect}"
158
+ )
159
+ end
160
+ result[platform] = hashes
161
+ end
162
+ rescue Excon::Error, JSON::ParserError
163
+ raise error("Could not fetch provider package hashes for #{identifier} v#{version}")
164
+ end
165
+
133
166
  # Fetch all the versions of a provider, and return a Version
134
167
  # representation of them.
135
168
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-opentofu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.385.0
4
+ version: 0.387.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.385.0
18
+ version: 0.387.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.385.0
25
+ version: 0.387.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -262,7 +262,7 @@ licenses:
262
262
  - MIT
263
263
  metadata:
264
264
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
265
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.385.0
265
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0
266
266
  rdoc_options: []
267
267
  require_paths:
268
268
  - lib