dependabot-opentofu 0.348.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d100ee54c94dcd801baea1e63e4a3bdc79a81744757674885810aff4863f4b65
4
+ data.tar.gz: c9ad465d66e7943109c75de15eadc0e751e6cda4c20448208c0e23451f72eba6
5
+ SHA512:
6
+ metadata.gz: 5373dce4cf40f3ac59d92e11b95191d6576375e658f09add64d38b06ccdb84cd8b4fd259deb2986b46f088ccfaeb81f173550428818b0d8d21d0b9bdef489105
7
+ data.tar.gz: 1059dd9d73a32bfd5c93cdd7d272b3137e230738f4cceed2976c3433a929c0260f385cab22d6ee3a0c47d853702387c1c975921b5c0a2de655328f8b6729116d
data/helpers/build ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ if [ -z "$DEPENDABOT_NATIVE_HELPERS_PATH" ]; then
6
+ echo "Unable to build, DEPENDABOT_NATIVE_HELPERS_PATH is not set"
7
+ exit 1
8
+ fi
9
+
10
+ install_dir="$DEPENDABOT_NATIVE_HELPERS_PATH/opentofu"
11
+
12
+ if [ ! -d "$install_dir/bin" ]; then
13
+ mkdir -p "$install_dir/bin"
14
+ fi
15
+
16
+ os="$(uname -s | tr '[:upper:]' '[:lower:]')"
17
+
18
+ hcl2json_checksum="8da5a86b3caff977067c62dd190bfdf296842191b0282c7e3a7019d6cf0f6657"
19
+ hcl2json_url="https://github.com/tmccombs/hcl2json/releases/download/v0.6.4/hcl2json_${os}_amd64"
20
+ hcl2json_path="$install_dir/bin/hcl2json"
21
+ curl -sSLfo "$hcl2json_path" "$hcl2json_url"
22
+ echo "$hcl2json_checksum $hcl2json_path" | sha256sum -c
23
+ chmod +x "$install_dir/bin/hcl2json"
@@ -0,0 +1,130 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/file_fetchers"
7
+ require "dependabot/file_fetchers/base"
8
+ require "dependabot/opentofu/file_selector"
9
+ require "dependabot/file_filtering"
10
+
11
+ module Dependabot
12
+ module Opentofu
13
+ class FileFetcher < Dependabot::FileFetchers::Base
14
+ extend T::Sig
15
+ extend T::Helpers
16
+
17
+ include FileFilter
18
+
19
+ # https://opentofu.org/docs/language/modules/sources/#local-paths
20
+ LOCAL_PATH_SOURCE = %r{source\s*=\s*['"](?<path>..?\/[^'"]+)}
21
+
22
+ sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
23
+ def self.required_files_in?(filenames)
24
+ filenames.any? { |f| f.end_with?(".tf", ".tofu", ".hcl") }
25
+ end
26
+
27
+ sig { override.returns(String) }
28
+ def self.required_files_message
29
+ "Repo must contain a OpenTofu configuration file."
30
+ end
31
+
32
+ sig { override.returns(T::Array[DependencyFile]) }
33
+ def fetch_files
34
+ unless allow_beta_ecosystems?
35
+ raise Dependabot::DependencyFileNotFound.new(
36
+ nil,
37
+ "OpenTofu support is currently in beta. Set ALLOW_BETA_ECOSYSTEMS=true to enable it."
38
+ )
39
+ end
40
+ fetched_files = []
41
+ fetched_files += opentofu_files
42
+ fetched_files += terragrunt_files
43
+ fetched_files += local_path_module_files(opentofu_files)
44
+ fetched_files += [lockfile] if lockfile
45
+
46
+ filtered_files = fetched_files.compact.reject do |file|
47
+ Dependabot::FileFiltering.should_exclude_path?(file.name, "file from final collection", @exclude_paths)
48
+ end
49
+
50
+ filtered_files
51
+ end
52
+
53
+ private
54
+
55
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
56
+ def opentofu_files
57
+ @opentofu_files ||= T.let(
58
+ repo_contents(raise_errors: false)
59
+ .select { |f| f.type == "file" && f.name.end_with?(".tf", ".tofu") }
60
+ .map { |f| fetch_file_from_host(f.name) },
61
+ T.nilable(T::Array[Dependabot::DependencyFile])
62
+ )
63
+ end
64
+
65
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
66
+ def terragrunt_files
67
+ @terragrunt_files ||= T.let(
68
+ repo_contents(raise_errors: false)
69
+ .select { |f| f.type == "file" && terragrunt_file?(f.name) }
70
+ .map { |f| fetch_file_from_host(f.name) },
71
+ T.nilable(T::Array[Dependabot::DependencyFile])
72
+ )
73
+ end
74
+
75
+ sig do
76
+ params(
77
+ files: T::Array[Dependabot::DependencyFile],
78
+ dir: String
79
+ )
80
+ .returns(T::Array[Dependabot::DependencyFile])
81
+ end
82
+ def local_path_module_files(files, dir: ".")
83
+ opentofu_files = T.let([], T::Array[Dependabot::DependencyFile])
84
+
85
+ files.each do |file|
86
+ opentofu_file_local_module_details(file).each do |path|
87
+ base_path = Pathname.new(File.join(dir, path)).cleanpath.to_path
88
+
89
+ # Skip excluded local module paths
90
+ if Dependabot::FileFiltering.should_exclude_path?(base_path, "local path module directory", @exclude_paths)
91
+ next
92
+ end
93
+
94
+ nested_opentofu_files =
95
+ repo_contents(dir: base_path)
96
+ .select { |f| f.type == "file" && f.name.end_with?(".tf", ".tofu") }
97
+ .map { |f| fetch_file_from_host(File.join(base_path, f.name)) }
98
+ opentofu_files += nested_opentofu_files
99
+ opentofu_files += local_path_module_files(nested_opentofu_files, dir: path)
100
+ end
101
+ end
102
+
103
+ # NOTE: The `support_file` attribute is not used but we set this to
104
+ # match what we do in other ecosystems
105
+ opentofu_files.tap { |fs| fs.each { |f| f.support_file = true } }
106
+ end
107
+
108
+ sig { params(file: Dependabot::DependencyFile).returns(T::Array[String]) }
109
+ def opentofu_file_local_module_details(file)
110
+ return [] unless file.name.end_with?(".tf", ".tofu")
111
+ return [] unless file.content&.match?(LOCAL_PATH_SOURCE)
112
+
113
+ T.must(file.content).scan(LOCAL_PATH_SOURCE).flatten.map do |path|
114
+ Pathname.new(path).cleanpath.to_path
115
+ end
116
+ end
117
+
118
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
119
+ def lockfile
120
+ @lockfile ||= T.let(
121
+ fetch_file_if_present(".terraform.lock.hcl"),
122
+ T.nilable(Dependabot::DependencyFile)
123
+ )
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ Dependabot::FileFetchers
130
+ .register("opentofu", Dependabot::Opentofu::FileFetcher)
@@ -0,0 +1,24 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Opentofu
8
+ module FileFilter
9
+ extend T::Sig
10
+
11
+ private
12
+
13
+ sig { params(file_name: String).returns(T::Boolean) }
14
+ def terragrunt_file?(file_name)
15
+ !lockfile?(file_name) && file_name.end_with?(".hcl")
16
+ end
17
+
18
+ sig { params(filename: String).returns(T::Boolean) }
19
+ def lockfile?(filename)
20
+ filename == ".terraform.lock.hcl"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,483 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "cgi"
5
+ require "excon"
6
+ require "nokogiri"
7
+ require "open3"
8
+ require "digest"
9
+ require "sorbet-runtime"
10
+ require "dependabot/dependency"
11
+ require "dependabot/file_parsers"
12
+ require "dependabot/file_parsers/base"
13
+ require "dependabot/git_commit_checker"
14
+ require "dependabot/shared_helpers"
15
+ require "dependabot/errors"
16
+ require "dependabot/opentofu/file_selector"
17
+ require "dependabot/opentofu/registry_client"
18
+ require "dependabot/opentofu/package_manager"
19
+
20
+ module Dependabot
21
+ module Opentofu
22
+ class FileParser < Dependabot::FileParsers::Base
23
+ extend T::Sig
24
+
25
+ require "dependabot/file_parsers/base/dependency_set"
26
+
27
+ include FileSelector
28
+
29
+ DEFAULT_REGISTRY = "registry.opentofu.org"
30
+ DEFAULT_NAMESPACE = "hashicorp"
31
+ # https://opentofu.org/docs/language/providers/requirements/#source-addresses
32
+ PROVIDER_SOURCE_ADDRESS = %r{\A((?<hostname>.+)/)?(?<namespace>.+)/(?<name>.+)\z}
33
+
34
+ sig { override.returns(T::Array[Dependabot::Dependency]) }
35
+ def parse
36
+ dependency_set = DependencySet.new
37
+
38
+ parse_opentofu_files(dependency_set)
39
+
40
+ parse_terragrunt_files(dependency_set)
41
+
42
+ dependency_set.dependencies.sort_by(&:name)
43
+ end
44
+
45
+ sig { returns(Ecosystem) }
46
+ def ecosystem
47
+ @ecosystem ||= T.let(
48
+ begin
49
+ Ecosystem.new(
50
+ name: ECOSYSTEM,
51
+ package_manager: package_manager
52
+ )
53
+ end,
54
+ T.nilable(Dependabot::Ecosystem)
55
+ )
56
+ end
57
+
58
+ private
59
+
60
+ # rubocop:disable Metrics/PerceivedComplexity
61
+ sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).void }
62
+ def parse_opentofu_files(dependency_set)
63
+ opentofu_files.each do |file|
64
+ next if file.support_file?
65
+
66
+ modules = parsed_file(file).fetch("module", {})
67
+ # If override.tf files are present, we need to merge the modules
68
+ if override_opentofu_files.any?
69
+ override_opentofu_files.each do |override_file|
70
+ override_modules = parsed_file(override_file).fetch("module", {})
71
+ modules = merge_modules(override_modules, modules)
72
+ end
73
+ end
74
+
75
+ modules.each do |name, details|
76
+ details = details.first
77
+
78
+ source = source_from(details)
79
+ # Cannot update local path modules, skip
80
+ next if source && source[:type] == "path"
81
+
82
+ # Cannot update modules using early evaluation yet
83
+ if T.must(source)[:type] == "interpolation"
84
+ Dependabot.logger.warn(
85
+ "Cannot parse module source name with early evaluation for #{name} in #{file.name}."
86
+ )
87
+ next
88
+ end
89
+
90
+ dependency_set << build_opentofu_dependency(file, name, T.must(source), details)
91
+ end
92
+
93
+ parsed_file(file).fetch("terraform", []).each do |opentofu|
94
+ required_providers = opentofu.fetch("required_providers", {})
95
+ required_providers.each do |provider|
96
+ provider.each do |name, details|
97
+ dependency_set << build_provider_dependency(file, name, details)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).void }
105
+ def parse_terragrunt_files(dependency_set)
106
+ terragrunt_files.each do |file|
107
+ modules = parsed_file(file).fetch("terraform", [])
108
+ modules.each do |details|
109
+ next unless details["source"]
110
+
111
+ source = source_from(details)
112
+ # Cannot update nil (interpolation sources) or local path modules, skip
113
+ next if source.nil? || source[:type] == "path"
114
+
115
+ dependency_set << build_terragrunt_dependency(file, source)
116
+ end
117
+ end
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ file: Dependabot::DependencyFile,
123
+ name: String,
124
+ source: T::Hash[Symbol, T.untyped],
125
+ details: T.untyped
126
+ )
127
+ .returns(Dependabot::Dependency)
128
+ end
129
+ def build_opentofu_dependency(file, name, source, details)
130
+ # dep_name should be unique for a source, using the info derived from
131
+ # the source or the source name provides this uniqueness
132
+ dep_name = case source[:type]
133
+ when "registry" then source[:module_identifier]
134
+ when "provider" then details["source"]
135
+ when "git" then git_dependency_name(name, source)
136
+ else name
137
+ end
138
+ version_req = details["version"]&.strip
139
+ version =
140
+ if source[:type] == "git" then version_from_ref(source[:ref])
141
+ elsif version_req&.match?(/^\d/) then version_req
142
+ end
143
+
144
+ Dependency.new(
145
+ name: dep_name,
146
+ version: version,
147
+ package_manager: "opentofu",
148
+ requirements: [
149
+ requirement: version_req,
150
+ groups: [],
151
+ file: file.name,
152
+ source: source
153
+ ]
154
+ )
155
+ end
156
+
157
+ sig do
158
+ params(
159
+ file: Dependabot::DependencyFile,
160
+ name: String,
161
+ details: T.any(String, T::Hash[String, T.untyped])
162
+ )
163
+ .returns(Dependabot::Dependency)
164
+ end
165
+ def build_provider_dependency(file, name, details = {})
166
+ deprecated_provider_error(file) if deprecated_provider?(details)
167
+
168
+ source_address = T.cast(details, T::Hash[String, T.untyped]).fetch("source", nil)
169
+ version_req = details["version"]&.strip
170
+ hostname, namespace, name = provider_source_from(source_address, name)
171
+ dependency_name = source_address ? "#{namespace}/#{name}" : name
172
+
173
+ Dependency.new(
174
+ name: T.must(dependency_name),
175
+ version: determine_version_for(T.must(hostname), T.must(namespace), T.must(name), version_req),
176
+ package_manager: "opentofu",
177
+ requirements: [
178
+ requirement: version_req,
179
+ groups: [],
180
+ file: file.name,
181
+ source: {
182
+ type: "provider",
183
+ registry_hostname: hostname,
184
+ module_identifier: "#{namespace}/#{name}"
185
+ }
186
+ ]
187
+ )
188
+ end
189
+
190
+ sig { params(file: Dependabot::DependencyFile).returns(T.noreturn) }
191
+ def deprecated_provider_error(file)
192
+ raise Dependabot::DependencyFileNotParseable.new(
193
+ file.path,
194
+ "This provider syntax is now deprecated.\n" \
195
+ "See https://opentofu.org/docs/language/providers/requirements/" \
196
+ "for the supported provider syntax."
197
+ )
198
+ end
199
+
200
+ sig { params(details: Object).returns(T::Boolean) }
201
+ def deprecated_provider?(details)
202
+ # The old syntax for terraform providers v0.12- looked like
203
+ # "tls ~> 2.1" which gets parsed as a string instead of a hash
204
+ details.is_a?(String)
205
+ end
206
+
207
+ sig { params(file: Dependabot::DependencyFile, source: T::Hash[Symbol, String]).returns(Dependabot::Dependency) }
208
+ def build_terragrunt_dependency(file, source)
209
+ dep_name = Source.from_url(source[:url]) ? T.must(Source.from_url(source[:url])).repo : source[:url]
210
+ version = version_from_ref(source[:ref])
211
+
212
+ Dependency.new(
213
+ name: T.must(dep_name),
214
+ version: version,
215
+ package_manager: "opentofu",
216
+ requirements: [
217
+ requirement: nil,
218
+ groups: [],
219
+ file: file.name,
220
+ source: source
221
+ ]
222
+ )
223
+ end
224
+
225
+ # Full docs at https://opentofu.org/docs/language/modules/sources/
226
+ sig { params(details_hash: T::Hash[String, String]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
227
+ def source_from(details_hash)
228
+ raw_source = details_hash.fetch("source")
229
+ bare_source = RegistryClient.get_proxied_source(raw_source)
230
+ source_type = source_type(bare_source)
231
+
232
+ source_details =
233
+ case source_type
234
+ # TODO: add support for OCI Registries https://opentofu.org/docs/cli/oci_registries/
235
+ when :http_archive, :path, :mercurial, :s3
236
+ { type: source_type.to_s, url: bare_source }
237
+ when :github, :bitbucket, :git
238
+ git_source_details_from(bare_source)
239
+ when :registry
240
+ registry_source_details_from(bare_source)
241
+ when :interpolation
242
+ { type: source_type.to_s, name: bare_source }
243
+ end
244
+
245
+ T.must(source_details)[:proxy_url] = raw_source if raw_source != bare_source
246
+ source_details
247
+ end
248
+
249
+ sig { params(source_address: T.nilable(String), name: String).returns(T::Array[String]) }
250
+ def provider_source_from(source_address, name)
251
+ matches = source_address&.match(PROVIDER_SOURCE_ADDRESS)
252
+ matches = {} if matches.nil?
253
+
254
+ [
255
+ matches[:hostname] || DEFAULT_REGISTRY,
256
+ matches[:namespace] || DEFAULT_NAMESPACE,
257
+ matches[:name] || name
258
+ ]
259
+ end
260
+
261
+ sig { params(source_string: T.untyped).returns(T::Hash[Symbol, String]) }
262
+ def registry_source_details_from(source_string)
263
+ parts = source_string.split("//").first.split("/")
264
+
265
+ if parts.count == 3
266
+ {
267
+ type: "registry",
268
+ registry_hostname: "registry.opentofu.org",
269
+ module_identifier: source_string.split("//").first
270
+ }
271
+ elsif parts.count == 4
272
+ {
273
+ type: "registry",
274
+ registry_hostname: parts.first,
275
+ module_identifier: parts[1..3].join("/")
276
+ }
277
+ else
278
+ msg = "Invalid registry source specified: '#{source_string}'"
279
+ raise DependencyFileNotEvaluatable, msg
280
+ end
281
+ end
282
+
283
+ sig { params(name: String, source: T::Hash[Symbol, T.untyped]).returns(String) }
284
+ def git_dependency_name(name, source)
285
+ git_source = Source.from_url(source[:url])
286
+ if git_source && source[:ref]
287
+ name + "::" + git_source.provider + "::" + git_source.repo + "::" + source[:ref]
288
+ elsif git_source
289
+ name + "::" + git_source.provider + "::" + git_source.repo
290
+ elsif source[:ref]
291
+ name + "::git_provider::repo_name/git_repo(" \
292
+ + Digest::SHA1.hexdigest(source[:url]) + ")::" + source[:ref]
293
+ else
294
+ name + "::git_provider::repo_name/git_repo(" + Digest::SHA1.hexdigest(source[:url]) + ")"
295
+ end
296
+ end
297
+
298
+ sig { params(source_string: String).returns(T::Hash[Symbol, T.nilable(String)]) }
299
+ def git_source_details_from(source_string)
300
+ git_url = source_string.strip.gsub(/^git::/, "")
301
+ git_url = "https://" + git_url unless git_url.start_with?("git@") || git_url.include?("://")
302
+
303
+ bare_uri =
304
+ if git_url.include?("git@")
305
+ T.must(git_url.split("git@").last).sub(":", "/")
306
+ else
307
+ git_url.sub(%r{(?:\w{3,5})?://}, "")
308
+ end
309
+
310
+ querystr = URI.parse("https://" + bare_uri).query
311
+ git_url = git_url.gsub("?#{querystr}", "").split(%r{(?<!:)//}).first
312
+
313
+ {
314
+ type: "git",
315
+ url: git_url,
316
+ branch: nil,
317
+ ref: CGI.parse(querystr.to_s)["ref"].first&.split(%r{(?<!:)//})&.first
318
+ }
319
+ end
320
+
321
+ sig { params(ref: T.nilable(String)).returns(T.nilable(String)) }
322
+ def version_from_ref(ref)
323
+ version_regex = GitCommitChecker::VERSION_REGEX
324
+ return unless ref&.match?(version_regex)
325
+
326
+ ref.match(version_regex)&.named_captures&.fetch("version")
327
+ end
328
+
329
+ # rubocop:disable Metrics/CyclomaticComplexity
330
+ sig { params(source_string: String).returns(Symbol) }
331
+ def source_type(source_string)
332
+ # TODO: add support for OCI Registries https://opentofu.org/docs/cli/oci_registries/
333
+ return :interpolation if source_string.include?("${")
334
+ return :path if source_string.start_with?(".")
335
+ return :github if source_string.start_with?("github.com/")
336
+ return :bitbucket if source_string.start_with?("bitbucket.org/")
337
+ return :git if source_string.start_with?("git::", "git@")
338
+ return :mercurial if source_string.start_with?("hg::")
339
+ return :s3 if source_string.start_with?("s3::")
340
+
341
+ raise "Unknown src: #{source_string}" if source_string.split("/").first&.include?("::")
342
+
343
+ return :registry unless source_string.start_with?("http")
344
+
345
+ path_uri = URI.parse(T.must(source_string.split(%r{(?<!:)//}).first))
346
+ query_uri = URI.parse(source_string)
347
+ return :http_archive if RegistryClient::ARCHIVE_EXTENSIONS.any? { |ext| path_uri.path&.end_with?(ext) }
348
+ return :http_archive if query_uri.query&.include?("archive=")
349
+
350
+ raise "HTTP source, but not an archive!"
351
+ end
352
+ # rubocop:enable Metrics/CyclomaticComplexity
353
+ # rubocop:enable Metrics/PerceivedComplexity
354
+
355
+ # == Returns:
356
+ # A Hash representing each module found in the specified file
357
+ #
358
+ # E.g.
359
+ # {
360
+ # "module" => {
361
+ # {
362
+ # "consul" => [
363
+ # {
364
+ # "source"=>"consul/aws",
365
+ # "version"=>"0.1.0"
366
+ # }
367
+ # ]
368
+ # }
369
+ # },
370
+ # "terragrunt"=>[
371
+ # {
372
+ # "include"=>[{ "path"=>"${find_in_parent_folders()}" }],
373
+ # "terraform"=>[{ "source" => "git::git@github.com:gruntwork-io/modules-example.git//consul?ref=v0.0.2" }]
374
+ # }
375
+ # ],
376
+ # }
377
+ sig { params(file: Dependabot::DependencyFile).returns(T::Hash[String, T.untyped]) }
378
+ def parsed_file(file)
379
+ @parsed_buildfile ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
380
+ @parsed_buildfile[file.name] ||= SharedHelpers.in_a_temporary_directory do
381
+ File.write("tmp.tf", file.content)
382
+
383
+ command = "#{opentofu_hcl2_parser_path} < tmp.tf"
384
+ start = Time.now
385
+ stdout, stderr, process = Open3.capture3(command)
386
+ time_taken = Time.now - start
387
+
388
+ unless process.success?
389
+ raise SharedHelpers::HelperSubprocessFailed.new(
390
+ message: stderr,
391
+ error_context: {
392
+ command: command,
393
+ time_taken: time_taken,
394
+ process_exit_value: process.to_s
395
+ }
396
+ )
397
+ end
398
+
399
+ JSON.parse(stdout)
400
+ end
401
+ rescue SharedHelpers::HelperSubprocessFailed => e
402
+ msg = e.message.strip
403
+ raise Dependabot::DependencyFileNotParseable.new(file.path, msg)
404
+ end
405
+
406
+ sig { returns(String) }
407
+ def opentofu_parser_path
408
+ helper_bin_dir = File.join(native_helpers_root, "opentofu/bin")
409
+ Pathname.new(File.join(helper_bin_dir, "json2hcl")).cleanpath.to_path
410
+ end
411
+
412
+ sig { returns(String) }
413
+ def opentofu_hcl2_parser_path
414
+ helper_bin_dir = File.join(native_helpers_root, "opentofu/bin")
415
+ Pathname.new(File.join(helper_bin_dir, "hcl2json")).cleanpath.to_path
416
+ end
417
+
418
+ sig { returns(String) }
419
+ def native_helpers_root
420
+ default_path = File.join(__dir__, "../../../helpers/install-dir")
421
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
422
+ end
423
+
424
+ sig { override.void }
425
+ def check_required_files
426
+ return if [*opentofu_files, *terragrunt_files].any?
427
+
428
+ raise "No OpenTofu configuration file!"
429
+ end
430
+
431
+ sig do
432
+ params(
433
+ hostname: String,
434
+ namespace: String,
435
+ name: String,
436
+ constraint: T.nilable(String)
437
+ )
438
+ .returns(T.nilable(String))
439
+ end
440
+ def determine_version_for(hostname, namespace, name, constraint)
441
+ return constraint if constraint&.match?(/\A\d/)
442
+
443
+ lockfile_content
444
+ .dig("provider", "#{hostname}/#{namespace}/#{name}", 0, "version")
445
+ end
446
+
447
+ sig { returns(T::Hash[String, T.untyped]) }
448
+ def lockfile_content
449
+ @lockfile_content ||= T.let(
450
+ begin
451
+ lockfile = dependency_files.find do |file|
452
+ file.name == ".terraform.lock.hcl"
453
+ end
454
+ lockfile ? parsed_file(lockfile) : {}
455
+ end,
456
+ T.nilable(T::Hash[String, T.untyped])
457
+ )
458
+ end
459
+
460
+ sig { returns(Ecosystem::VersionManager) }
461
+ def package_manager
462
+ @package_manager ||= T.let(
463
+ PackageManager.new(T.must(opentofu_version)),
464
+ T.nilable(Dependabot::Opentofu::PackageManager)
465
+ )
466
+ end
467
+
468
+ sig { returns(T.nilable(String)) }
469
+ def opentofu_version
470
+ @opentofu_version ||= T.let(
471
+ begin
472
+ version = SharedHelpers.run_shell_command("tofu --version")
473
+ version.match(Dependabot::Ecosystem::VersionManager::DEFAULT_VERSION_PATTERN)&.captures&.first
474
+ end,
475
+ T.nilable(String)
476
+ )
477
+ end
478
+ end
479
+ end
480
+ end
481
+
482
+ Dependabot::FileParsers
483
+ .register("opentofu", Dependabot::Opentofu::FileParser)