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 +7 -0
- data/helpers/build +23 -0
- data/lib/dependabot/opentofu/file_fetcher.rb +130 -0
- data/lib/dependabot/opentofu/file_filter.rb +24 -0
- data/lib/dependabot/opentofu/file_parser.rb +483 -0
- data/lib/dependabot/opentofu/file_selector.rb +82 -0
- data/lib/dependabot/opentofu/file_updater.rb +461 -0
- data/lib/dependabot/opentofu/metadata_finder.rb +55 -0
- data/lib/dependabot/opentofu/package/package_details_fetcher.rb +144 -0
- data/lib/dependabot/opentofu/package_manager.rb +41 -0
- data/lib/dependabot/opentofu/registry_client.rb +246 -0
- data/lib/dependabot/opentofu/requirement.rb +59 -0
- data/lib/dependabot/opentofu/requirements_updater.rb +223 -0
- data/lib/dependabot/opentofu/update_checker/latest_version_resolver.rb +217 -0
- data/lib/dependabot/opentofu/update_checker.rb +264 -0
- data/lib/dependabot/opentofu/version.rb +61 -0
- data/lib/dependabot/opentofu.rb +31 -0
- metadata +283 -0
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)
|