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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require "cgi"
|
|
7
|
+
require "excon"
|
|
8
|
+
require "sorbet-runtime"
|
|
9
|
+
require "dependabot/swift"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Opentofu
|
|
13
|
+
module Package
|
|
14
|
+
class PackageDetailsFetcher
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
RELEASES_URL_GIT = "https://api.github.com/repos/"
|
|
18
|
+
RELEASE_URL_FOR_PROVIDER = "https://api.opentofu.org/registry/docs/providers/"
|
|
19
|
+
RELEASE_URL_FOR_MODULE = "https://api.opentofu.org/registry/docs/modules/"
|
|
20
|
+
APPLICATION_JSON = "JSON"
|
|
21
|
+
# https://api.opentofu.org/registry/docs/providers/hashicorp/aws/index.json
|
|
22
|
+
# https://api.opentofu.org/registry/docs/modules/hashicorp/consul/aws/index.json
|
|
23
|
+
|
|
24
|
+
ELIGIBLE_SOURCE_TYPES = T.let(
|
|
25
|
+
%w(git provider registry).freeze,
|
|
26
|
+
T::Array[String]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sig do
|
|
30
|
+
params(
|
|
31
|
+
dependency: Dependency,
|
|
32
|
+
credentials: T::Array[Dependabot::Credential],
|
|
33
|
+
git_commit_checker: Dependabot::GitCommitChecker
|
|
34
|
+
).void
|
|
35
|
+
end
|
|
36
|
+
def initialize(dependency:, credentials:, git_commit_checker:)
|
|
37
|
+
@dependency = dependency
|
|
38
|
+
@credentials = credentials
|
|
39
|
+
@git_commit_checker = git_commit_checker
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(Dependabot::GitCommitChecker) }
|
|
43
|
+
attr_reader :git_commit_checker
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
46
|
+
attr_reader :credentials
|
|
47
|
+
|
|
48
|
+
sig { returns(T::Array[GitTagWithDetail]) }
|
|
49
|
+
def fetch_tag_and_release_date
|
|
50
|
+
truncate_github_url = @dependency.name.gsub("github.com/", "")
|
|
51
|
+
url = RELEASES_URL_GIT + "#{truncate_github_url}/releases"
|
|
52
|
+
result_lines = T.let([], T::Array[GitTagWithDetail])
|
|
53
|
+
# Fetch the releases from the GitHub API
|
|
54
|
+
response = Excon.get(
|
|
55
|
+
url,
|
|
56
|
+
headers: { "User-Agent" => "Dependabot (dependabot.com)",
|
|
57
|
+
"Accept" => "application/vnd.github.v3+json" }
|
|
58
|
+
)
|
|
59
|
+
Dependabot.logger.error("Failed call details: #{response.body}") unless response.status == 200
|
|
60
|
+
return result_lines unless response.status == 200
|
|
61
|
+
|
|
62
|
+
# Parse the JSON response
|
|
63
|
+
releases = JSON.parse(response.body)
|
|
64
|
+
|
|
65
|
+
# Extract version names and release dates into a hash
|
|
66
|
+
releases.map do |release|
|
|
67
|
+
result_lines << GitTagWithDetail.new(
|
|
68
|
+
tag: release["tag_name"],
|
|
69
|
+
release_date: release["published_at"]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# sort the result lines by tag in descending order
|
|
74
|
+
result_lines = result_lines.sort_by(&:tag).reverse
|
|
75
|
+
# Log the extracted details for debugging
|
|
76
|
+
Dependabot.logger.info("Extracted release details: #{result_lines}")
|
|
77
|
+
result_lines
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { returns(T::Array[GitTagWithDetail]) }
|
|
81
|
+
def fetch_tag_and_release_date_from_provider
|
|
82
|
+
return [] unless dependency_source_details
|
|
83
|
+
|
|
84
|
+
url = RELEASE_URL_FOR_PROVIDER + dependency_source_details&.fetch(:module_identifier) + "/index.json"
|
|
85
|
+
Dependabot.logger.info("Fetching provider release details from URL: #{url}")
|
|
86
|
+
result_lines = T.let([], T::Array[GitTagWithDetail])
|
|
87
|
+
# Fetch the releases from the provider API
|
|
88
|
+
response = Excon.get(url, headers: { "Accept" => "application/vnd.github.v3+json" })
|
|
89
|
+
Dependabot.logger.error("Failed call details: #{response.body}") unless response.status == 200
|
|
90
|
+
return result_lines unless response.status == 200
|
|
91
|
+
|
|
92
|
+
# Parse the JSON response
|
|
93
|
+
releases = JSON.parse(response.body).fetch("versions", [])
|
|
94
|
+
# Check if releases is an array and not empty
|
|
95
|
+
return result_lines unless releases.is_a?(Array) && !releases.empty?
|
|
96
|
+
|
|
97
|
+
# Extract version names and release dates into result_lines
|
|
98
|
+
releases.each do |release|
|
|
99
|
+
result_lines << GitTagWithDetail.new(
|
|
100
|
+
tag: release["id"],
|
|
101
|
+
release_date: release["published"]
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
# Sort the result lines by tag in descending order
|
|
105
|
+
result_lines.sort_by(&:tag).reverse
|
|
106
|
+
end
|
|
107
|
+
# RuboCop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
108
|
+
|
|
109
|
+
sig { returns(T::Array[GitTagWithDetail]) }
|
|
110
|
+
def fetch_tag_and_release_date_from_module
|
|
111
|
+
return [] unless dependency_source_details
|
|
112
|
+
|
|
113
|
+
url = RELEASE_URL_FOR_MODULE + dependency_source_details&.fetch(:module_identifier) + "/index.json"
|
|
114
|
+
Dependabot.logger.info("Fetching provider release details from URL: #{url}")
|
|
115
|
+
result_lines = T.let([], T::Array[GitTagWithDetail])
|
|
116
|
+
# Fetch the releases from the provider API
|
|
117
|
+
response = Excon.get(url, headers: { "Accept" => "application/vnd.github.v3+json" })
|
|
118
|
+
Dependabot.logger.error("Failed call details: #{response.body}") unless response.status == 200
|
|
119
|
+
return result_lines unless response.status == 200
|
|
120
|
+
|
|
121
|
+
# Parse the JSON response
|
|
122
|
+
releases = JSON.parse(response.body).fetch("versions", [])
|
|
123
|
+
|
|
124
|
+
# Extract version names and release dates into result_lines
|
|
125
|
+
releases.each do |release|
|
|
126
|
+
result_lines << GitTagWithDetail.new(
|
|
127
|
+
tag: release["id"],
|
|
128
|
+
release_date: release["published"]
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
# Sort the result lines by tag in descending order
|
|
132
|
+
result_lines.sort_by(&:tag).reverse
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
|
|
136
|
+
def dependency_source_details
|
|
137
|
+
return nil unless @dependency.source_details
|
|
138
|
+
|
|
139
|
+
@dependency.source_details(allowed_types: ELIGIBLE_SOURCE_TYPES)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/ecosystem"
|
|
6
|
+
require "dependabot/opentofu/version"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Opentofu
|
|
10
|
+
ECOSYSTEM = "opentofu"
|
|
11
|
+
PACKAGE_MANAGER = "opentofu"
|
|
12
|
+
SUPPORTED_OPENTOFU_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
|
13
|
+
|
|
14
|
+
# When a version is going to be unsupported, it will be added here
|
|
15
|
+
DEPRECATED_OPENTOFU_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
|
16
|
+
|
|
17
|
+
class PackageManager < Dependabot::Ecosystem::VersionManager
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
sig { params(raw_version: String).void }
|
|
21
|
+
def initialize(raw_version)
|
|
22
|
+
super(
|
|
23
|
+
name: PACKAGE_MANAGER,
|
|
24
|
+
version: Version.new(raw_version),
|
|
25
|
+
deprecated_versions: DEPRECATED_OPENTOFU_VERSIONS,
|
|
26
|
+
supported_versions: SUPPORTED_OPENTOFU_VERSIONS
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(T::Boolean) }
|
|
31
|
+
def deprecated?
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(T::Boolean) }
|
|
36
|
+
def unsupported?
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/dependency"
|
|
5
|
+
require "dependabot/errors"
|
|
6
|
+
require "dependabot/registry_client"
|
|
7
|
+
require "dependabot/source"
|
|
8
|
+
require "dependabot/opentofu/version"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Opentofu
|
|
12
|
+
# Opentofu::RegistryClient is a basic API client to interact with a
|
|
13
|
+
# OpenTofu registry: https://api.opentofu.org/
|
|
14
|
+
class RegistryClient
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
# Archive extensions supported by OpenTofu for HTTP URLs
|
|
18
|
+
# https://opentofu.org/docs/language/modules/sources/#http-urls
|
|
19
|
+
ARCHIVE_EXTENSIONS = T.let(
|
|
20
|
+
%w(.zip .bz2 .tar.bz2 .tar.tbz2 .tbz2 .gz .tar.gz .tgz .xz .tar.xz .txz).freeze,
|
|
21
|
+
T::Array[String]
|
|
22
|
+
)
|
|
23
|
+
PUBLIC_HOSTNAME = "registry.opentofu.org"
|
|
24
|
+
API_BASE_URL = "api.opentofu.org"
|
|
25
|
+
|
|
26
|
+
sig { params(hostname: String, credentials: T::Array[Dependabot::Credential]).void }
|
|
27
|
+
def initialize(hostname: PUBLIC_HOSTNAME, credentials: [])
|
|
28
|
+
@hostname = hostname
|
|
29
|
+
@api_base_url = T.let(API_BASE_URL, String)
|
|
30
|
+
@tokens = T.let(
|
|
31
|
+
credentials.each_with_object({}) do |item, memo|
|
|
32
|
+
memo[item["host"]] = item["token"] if item["type"] == "opentofu_registry"
|
|
33
|
+
end,
|
|
34
|
+
T::Hash[String, String]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
39
|
+
# rubocop:disable Metrics/AbcSize
|
|
40
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
41
|
+
# See https://opentofu.org/docs/language/modules/sources/#http-urls for
|
|
42
|
+
# details of how OpenTofu handle HTTP(S) sources for modules
|
|
43
|
+
sig { params(raw_source: String).returns(String) }
|
|
44
|
+
def self.get_proxied_source(raw_source)
|
|
45
|
+
return raw_source unless raw_source.start_with?("http")
|
|
46
|
+
|
|
47
|
+
uri = URI.parse(T.must(raw_source.split(%r{(?<!:)//}).first))
|
|
48
|
+
return raw_source if ARCHIVE_EXTENSIONS.any? { |ext| uri.path&.end_with?(ext) }
|
|
49
|
+
return raw_source if URI.parse(raw_source).query&.include?("archive=")
|
|
50
|
+
|
|
51
|
+
url = T.must(raw_source.split(%r{(?<!:)//}).first) + "?opentofu-get=1"
|
|
52
|
+
host = URI.parse(raw_source).host
|
|
53
|
+
|
|
54
|
+
response = Dependabot::RegistryClient.get(url: url)
|
|
55
|
+
raise PrivateSourceAuthenticationFailure, host if response.status == 401
|
|
56
|
+
|
|
57
|
+
return T.must(response.headers["X-OpenTofu-Get"]) if response.headers["X-OpenTofu-Get"]
|
|
58
|
+
|
|
59
|
+
doc = Nokogiri::XML(response.body)
|
|
60
|
+
doc.css("meta").find do |tag|
|
|
61
|
+
tag.attributes&.fetch("name", nil)&.value == "opentofu-get"
|
|
62
|
+
end&.attributes&.fetch("content", nil)&.value
|
|
63
|
+
rescue Excon::Error::Socket, Excon::Error::Timeout => e
|
|
64
|
+
raise PrivateSourceAuthenticationFailure, host if e.message.include?("no address for")
|
|
65
|
+
|
|
66
|
+
raw_source
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
69
|
+
# rubocop:enable Metrics/AbcSize
|
|
70
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
71
|
+
|
|
72
|
+
# Fetch all the versions of a provider, and return a Version
|
|
73
|
+
# representation of them.
|
|
74
|
+
#
|
|
75
|
+
# @param identifier [String] the identifier for the dependency, i.e:
|
|
76
|
+
# "hashicorp/aws"
|
|
77
|
+
# @return [Array<Dependabot::Opentofu::Version>]
|
|
78
|
+
# @raise [Dependabot::DependabotError] when the versions cannot be retrieved
|
|
79
|
+
sig { params(identifier: String).returns(T::Array[Dependabot::Opentofu::Version]) }
|
|
80
|
+
def all_provider_versions(identifier:)
|
|
81
|
+
base_url = service_url_for_registry("providers.v1")
|
|
82
|
+
response = http_get!(URI.join(base_url, "#{identifier}/versions"))
|
|
83
|
+
|
|
84
|
+
JSON.parse(response.body)
|
|
85
|
+
.fetch("versions")
|
|
86
|
+
.map { |release| version_class.new(release.fetch("version")) }
|
|
87
|
+
rescue Excon::Error
|
|
88
|
+
raise error("Could not fetch provider versions")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fetch all the versions of a module, and return a Version
|
|
92
|
+
# representation of them.
|
|
93
|
+
#
|
|
94
|
+
# @param identifier [String] the identifier for the dependency, i.e:
|
|
95
|
+
# "hashicorp/consul/aws"
|
|
96
|
+
# @return [Array<Dependabot::Opentofu::Version>]
|
|
97
|
+
# @raise [Dependabot::DependabotError] when the versions cannot be retrieved
|
|
98
|
+
sig { params(identifier: String).returns(T::Array[Dependabot::Opentofu::Version]) }
|
|
99
|
+
def all_module_versions(identifier:)
|
|
100
|
+
base_url = service_url_for_registry("modules.v1")
|
|
101
|
+
response = http_get!(URI.join(base_url, "#{identifier}/versions"))
|
|
102
|
+
|
|
103
|
+
JSON.parse(response.body)
|
|
104
|
+
.fetch("modules").first.fetch("versions")
|
|
105
|
+
.map { |release| version_class.new(release.fetch("version")) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Fetch the "source" for a module or provider. We use the API to fetch
|
|
109
|
+
# the source for a dependency, this typically points to a source code
|
|
110
|
+
# repository, and then instantiate a Dependabot::Source object that we
|
|
111
|
+
# can use to fetch Metadata about a specific version of the dependency.
|
|
112
|
+
#
|
|
113
|
+
# @param dependency [Dependabot::Dependency] the dependency who's source
|
|
114
|
+
# we're attempting to find
|
|
115
|
+
# @return [nil, Dependabot::Source]
|
|
116
|
+
sig { params(dependency: Dependabot::Dependency).returns(T.nilable(Dependabot::Source)) }
|
|
117
|
+
def source(dependency:)
|
|
118
|
+
type = T.must(dependency.requirements.first)[:source][:type]
|
|
119
|
+
base_url = url_for_api("/registry/docs/")
|
|
120
|
+
case type
|
|
121
|
+
when "module", "modules", "registry"
|
|
122
|
+
download_url = URI.join(base_url, "modules/#{dependency.name}/#{dependency.version}/download")
|
|
123
|
+
response = http_get(download_url)
|
|
124
|
+
return nil unless response.status == 204
|
|
125
|
+
|
|
126
|
+
source_url = response.headers.fetch("X-OpenTofu-Get")
|
|
127
|
+
source_url = URI.join(download_url, source_url) if
|
|
128
|
+
source_url.start_with?("/", "./", "../")
|
|
129
|
+
source_url = RegistryClient.get_proxied_source(source_url) if source_url
|
|
130
|
+
when "provider", "providers"
|
|
131
|
+
url = URI.join(base_url, "providers/#{dependency.name}/v#{dependency.version}/index.json")
|
|
132
|
+
response = http_get(url)
|
|
133
|
+
return nil unless response.status == 200
|
|
134
|
+
|
|
135
|
+
source_url = JSON.parse(response.body).dig("docs", "index", "edit_link")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Source.from_url(source_url) if source_url
|
|
139
|
+
rescue JSON::ParserError, Excon::Error::Timeout
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Perform service discovery and return the absolute URL for
|
|
144
|
+
# the requested service.
|
|
145
|
+
#
|
|
146
|
+
# @param service_key [String] the service type
|
|
147
|
+
# @param return String
|
|
148
|
+
# @raise [Dependabot::PrivateSourceAuthenticationFailure] when the service is not available
|
|
149
|
+
sig { params(service_key: String).returns(String) }
|
|
150
|
+
def service_url_for_registry(service_key)
|
|
151
|
+
url_for_registry(services.fetch(service_key))
|
|
152
|
+
rescue KeyError
|
|
153
|
+
raise Dependabot::PrivateSourceAuthenticationFailure, "Host does not support required OpenTofu-native service"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
sig { returns(String) }
|
|
159
|
+
attr_reader :hostname, :api_base_url
|
|
160
|
+
|
|
161
|
+
sig { returns(T::Hash[String, String]) }
|
|
162
|
+
attr_reader :tokens
|
|
163
|
+
|
|
164
|
+
sig { returns(T.class_of(Dependabot::Opentofu::Version)) }
|
|
165
|
+
def version_class
|
|
166
|
+
Version
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { params(hostname: String).returns(T::Hash[String, String]) }
|
|
170
|
+
def headers_for(hostname)
|
|
171
|
+
token = tokens[hostname]
|
|
172
|
+
token ? { "Authorization" => "Bearer #{token}" } : {}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
sig { returns(T::Hash[String, String]) }
|
|
176
|
+
def services
|
|
177
|
+
@services ||= T.let(
|
|
178
|
+
begin
|
|
179
|
+
response = http_get(url_for_registry("/.well-known/terraform.json"))
|
|
180
|
+
response.status == 200 ? JSON.parse(response.body) : {}
|
|
181
|
+
end,
|
|
182
|
+
T.nilable(T::Hash[String, String])
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sig { params(type: String).returns(String) }
|
|
187
|
+
def service_key_for(type)
|
|
188
|
+
case type
|
|
189
|
+
when "module", "modules", "registry"
|
|
190
|
+
"modules.v1"
|
|
191
|
+
when "provider", "providers"
|
|
192
|
+
"providers.v1"
|
|
193
|
+
else
|
|
194
|
+
raise error("Invalid source type")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
sig { params(url: T.any(String, URI::Generic)).returns(Excon::Response) }
|
|
199
|
+
def http_get(url)
|
|
200
|
+
Dependabot::RegistryClient.get(
|
|
201
|
+
url: url.to_s,
|
|
202
|
+
headers: headers_for(hostname)
|
|
203
|
+
)
|
|
204
|
+
rescue Excon::Error::Socket, Excon::Error::Timeout
|
|
205
|
+
raise PrivateSourceBadResponse, hostname
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
sig { params(url: URI::Generic).returns(Excon::Response) }
|
|
209
|
+
def http_get!(url)
|
|
210
|
+
response = http_get(url)
|
|
211
|
+
|
|
212
|
+
raise Dependabot::PrivateSourceAuthenticationFailure, hostname if response.status == 401
|
|
213
|
+
raise error("Response from registry was #{response.status}") unless response.status == 200
|
|
214
|
+
|
|
215
|
+
response
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
sig { params(path: String).returns(String) }
|
|
219
|
+
def url_for_registry(path)
|
|
220
|
+
uri = URI.parse(path)
|
|
221
|
+
return uri.to_s if uri.scheme == "https"
|
|
222
|
+
raise error("Unsupported scheme provided") if uri.host && uri.scheme
|
|
223
|
+
|
|
224
|
+
uri.host = hostname
|
|
225
|
+
uri.scheme = "https"
|
|
226
|
+
uri.to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
sig { params(path: String).returns(String) }
|
|
230
|
+
def url_for_api(path)
|
|
231
|
+
uri = URI.parse(path)
|
|
232
|
+
return uri.to_s if uri.scheme == "https"
|
|
233
|
+
raise error("Unsupported scheme provided") if uri.host && uri.scheme
|
|
234
|
+
|
|
235
|
+
uri.host = api_base_url
|
|
236
|
+
uri.scheme = "https"
|
|
237
|
+
uri.to_s
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
sig { params(message: String).returns(Dependabot::DependabotError) }
|
|
241
|
+
def error(message)
|
|
242
|
+
Dependabot::DependabotError.new(message)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/requirement"
|
|
7
|
+
require "dependabot/utils"
|
|
8
|
+
require "dependabot/opentofu/version"
|
|
9
|
+
|
|
10
|
+
# Just ensures that OpenTofu requirements use OpenTofu versions
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Opentofu
|
|
13
|
+
class Requirement < Dependabot::Requirement
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
# Override regex PATTERN from Gem::Requirement to add support for the
|
|
17
|
+
# optional 'v' prefix to release tag names, which OpenTofu supports.
|
|
18
|
+
OPERATORS = T.let(OPS.keys.map { |key| Regexp.quote(key) }.join("|").freeze, String)
|
|
19
|
+
PATTERN_RAW = T.let("\\s*(#{OPERATORS})?\\s*v?(#{Gem::Version::VERSION_PATTERN})\\s*".freeze, String)
|
|
20
|
+
PATTERN = /\A#{PATTERN_RAW}\z/
|
|
21
|
+
|
|
22
|
+
sig { params(obj: T.any(String, Gem::Version)).returns(T::Array[T.any(String, Version)]) }
|
|
23
|
+
def self.parse(obj)
|
|
24
|
+
return ["=", Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
|
|
25
|
+
|
|
26
|
+
unless (matches = PATTERN.match(obj.to_s))
|
|
27
|
+
msg = "Illformed requirement [#{obj.inspect}]"
|
|
28
|
+
raise BadRequirementError, msg
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
|
|
32
|
+
|
|
33
|
+
[matches[1] || "=", Opentofu::Version.new(matches[2])]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# For consistency with other languages, we define a requirements array.
|
|
37
|
+
# OpenTofu doesn't have an `OR` separator for requirements, so it
|
|
38
|
+
# always contains a single element.
|
|
39
|
+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
|
|
40
|
+
def self.requirements_array(requirement_string)
|
|
41
|
+
[new(requirement_string.to_s)]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Patches Gem::Requirement to make it accept requirement strings like
|
|
45
|
+
# "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
|
|
46
|
+
sig { params(requirements: T.any(String, T::Array[String])).void }
|
|
47
|
+
def initialize(*requirements)
|
|
48
|
+
requirements = requirements.flatten.flat_map do |req_string|
|
|
49
|
+
req_string.split(",").map(&:strip)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
super(requirements)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Dependabot::Utils
|
|
59
|
+
.register_requirement_class("opentofu", Dependabot::Opentofu::Requirement)
|