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,223 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
####################################################################
|
|
5
|
+
# For more details on OpenTofu version constraints, see: #
|
|
6
|
+
# https://opentofu.org/docs/language/modules/#published-modules #
|
|
7
|
+
####################################################################
|
|
8
|
+
|
|
9
|
+
require "sorbet-runtime"
|
|
10
|
+
|
|
11
|
+
require "dependabot/opentofu/version"
|
|
12
|
+
require "dependabot/opentofu/requirement"
|
|
13
|
+
|
|
14
|
+
module Dependabot
|
|
15
|
+
module Opentofu
|
|
16
|
+
# Takes an array of `requirements` hashes for a dependency at the old
|
|
17
|
+
# version and a new version, and generates a set of new `requirements`
|
|
18
|
+
# hashes at the new version.
|
|
19
|
+
#
|
|
20
|
+
# A requirements hash is a basic description of a dependency at a certain
|
|
21
|
+
# version constraint, and it includes the data that is needed to update the
|
|
22
|
+
# manifest (i.e. the `.tf` file) with the new version.
|
|
23
|
+
#
|
|
24
|
+
# A requirements hash looks like this for a registry hosted requirement:
|
|
25
|
+
# ```ruby
|
|
26
|
+
# {
|
|
27
|
+
# requirement: "~> 0.2.1",
|
|
28
|
+
# groups: [],
|
|
29
|
+
# file: "main.tf",
|
|
30
|
+
# source: {
|
|
31
|
+
# type: "registry",
|
|
32
|
+
# registry_hostname: "registry.opentofu.org",
|
|
33
|
+
# module_identifier: "hashicorp/consul/aws"
|
|
34
|
+
# }
|
|
35
|
+
# }
|
|
36
|
+
#
|
|
37
|
+
# And like this for a git requirement:
|
|
38
|
+
# ```ruby
|
|
39
|
+
# {
|
|
40
|
+
# requirement: nil,
|
|
41
|
+
# groups: [],
|
|
42
|
+
# file: "main.tf",
|
|
43
|
+
# source: {
|
|
44
|
+
# type: "git",
|
|
45
|
+
# url: "https://github.com/cloudposse/terraform-null-label.git",
|
|
46
|
+
# branch: nil,
|
|
47
|
+
# ref: nil
|
|
48
|
+
# }
|
|
49
|
+
# }
|
|
50
|
+
class RequirementsUpdater
|
|
51
|
+
extend T::Sig
|
|
52
|
+
|
|
53
|
+
# @param requirements [Hash{Symbol => String, Array, Hash}]
|
|
54
|
+
# @param latest_version [Dependabot::Opentofu::Version]
|
|
55
|
+
# @param tag_for_latest_version [String, NilClass]
|
|
56
|
+
sig do
|
|
57
|
+
params(
|
|
58
|
+
requirements: T::Array[T::Hash[Symbol, T.untyped]],
|
|
59
|
+
latest_version: T.nilable(Dependabot::Version::VersionParameter),
|
|
60
|
+
tag_for_latest_version: T.nilable(String)
|
|
61
|
+
).void
|
|
62
|
+
end
|
|
63
|
+
def initialize(requirements:, latest_version:, tag_for_latest_version:)
|
|
64
|
+
@requirements = requirements
|
|
65
|
+
@tag_for_latest_version = tag_for_latest_version
|
|
66
|
+
|
|
67
|
+
return unless latest_version
|
|
68
|
+
return unless version_class.correct?(latest_version)
|
|
69
|
+
|
|
70
|
+
@latest_version = T.let(version_class.new(latest_version), Dependabot::Opentofu::Version)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return requirements [Hash{Symbol => String, Array, Hash}]
|
|
74
|
+
# * requirement [String, NilClass] the updated version constraint
|
|
75
|
+
# * groups [Array] no-op for OpenTofu
|
|
76
|
+
# * file [String] the file that specified this dependency
|
|
77
|
+
# * source [Hash{Symbol => String}] The updated git or registry source details
|
|
78
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
79
|
+
def updated_requirements
|
|
80
|
+
# NOTE: Order is important here. The FileUpdater needs the updated
|
|
81
|
+
# requirement at index `i` to correspond to the previous requirement
|
|
82
|
+
# at the same index.
|
|
83
|
+
requirements.map do |req|
|
|
84
|
+
case req.dig(:source, :type)
|
|
85
|
+
when "git" then update_git_requirement(req)
|
|
86
|
+
when "registry", "provider" then update_registry_requirement(req)
|
|
87
|
+
else req
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
95
|
+
attr_reader :requirements
|
|
96
|
+
|
|
97
|
+
sig { returns(Dependabot::Opentofu::Version) }
|
|
98
|
+
attr_reader :latest_version
|
|
99
|
+
|
|
100
|
+
sig { returns(T.nilable(String)) }
|
|
101
|
+
attr_reader :tag_for_latest_version
|
|
102
|
+
|
|
103
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
104
|
+
def update_git_requirement(req)
|
|
105
|
+
return req unless req.dig(:source, :ref)
|
|
106
|
+
return req unless tag_for_latest_version
|
|
107
|
+
|
|
108
|
+
req.merge(source: req[:source].merge(ref: tag_for_latest_version))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
112
|
+
def update_registry_requirement(req)
|
|
113
|
+
return req if req.fetch(:requirement).nil?
|
|
114
|
+
|
|
115
|
+
string_req = req.fetch(:requirement).strip
|
|
116
|
+
ruby_req = requirement_class.new(string_req)
|
|
117
|
+
return req if ruby_req.satisfied_by?(latest_version)
|
|
118
|
+
|
|
119
|
+
new_req =
|
|
120
|
+
if ruby_req.exact? then latest_version.to_s
|
|
121
|
+
elsif string_req.start_with?("~>")
|
|
122
|
+
update_twiddle_version(string_req).to_s
|
|
123
|
+
else
|
|
124
|
+
update_range(string_req).map(&:to_s).join(", ")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
req.merge(requirement: new_req)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Updates the version in a "~>" constraint to allow the given version
|
|
131
|
+
sig { params(req_string: String).returns(String) }
|
|
132
|
+
def update_twiddle_version(req_string)
|
|
133
|
+
old_version = requirement_class.new(req_string)
|
|
134
|
+
.requirements.first.last
|
|
135
|
+
updated_version = at_same_precision(latest_version, old_version)
|
|
136
|
+
req_string.sub(old_version.to_s, updated_version)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { params(req_string: String).returns(T::Array[Dependabot::Opentofu::Requirement]) }
|
|
140
|
+
def update_range(req_string)
|
|
141
|
+
requirement_class.new(req_string).requirements.flat_map do |r|
|
|
142
|
+
ruby_req = requirement_class.new(r.join(" "))
|
|
143
|
+
next ruby_req if ruby_req.satisfied_by?(latest_version)
|
|
144
|
+
|
|
145
|
+
case op = ruby_req.requirements.first.first
|
|
146
|
+
when "<", "<=" then [update_greatest_version(ruby_req, latest_version)]
|
|
147
|
+
when "!=" then []
|
|
148
|
+
else raise "Unexpected operation for unsatisfied req: #{op}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig do
|
|
154
|
+
params(
|
|
155
|
+
new_version: Dependabot::Opentofu::Version,
|
|
156
|
+
old_version: Dependabot::Opentofu::Version
|
|
157
|
+
)
|
|
158
|
+
.returns(String)
|
|
159
|
+
end
|
|
160
|
+
def at_same_precision(new_version, old_version)
|
|
161
|
+
release_precision =
|
|
162
|
+
old_version.to_s.split(".").count { |i| i.match?(/^\d+$/) }
|
|
163
|
+
prerelease_precision =
|
|
164
|
+
old_version.to_s.split(".").count - release_precision
|
|
165
|
+
|
|
166
|
+
new_release =
|
|
167
|
+
new_version.to_s.split(".").first(release_precision)
|
|
168
|
+
new_prerelease =
|
|
169
|
+
new_version.to_s.split(".")
|
|
170
|
+
.drop_while { |i| i.match?(/^\d+$/) }
|
|
171
|
+
.first([prerelease_precision, 1].max)
|
|
172
|
+
|
|
173
|
+
[*new_release, *new_prerelease].join(".")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Updates the version in a "<" or "<=" constraint to allow the given
|
|
177
|
+
# version
|
|
178
|
+
sig do
|
|
179
|
+
params(
|
|
180
|
+
requirement: Dependabot::Requirement,
|
|
181
|
+
version_to_be_permitted: T.any(String, Dependabot::Opentofu::Version)
|
|
182
|
+
)
|
|
183
|
+
.returns(Dependabot::Opentofu::Requirement)
|
|
184
|
+
end
|
|
185
|
+
def update_greatest_version(requirement, version_to_be_permitted)
|
|
186
|
+
if version_to_be_permitted.is_a?(String)
|
|
187
|
+
version_to_be_permitted =
|
|
188
|
+
version_class.new(version_to_be_permitted)
|
|
189
|
+
end
|
|
190
|
+
op, version = requirement.requirements.first
|
|
191
|
+
version = version.release if version.prerelease?
|
|
192
|
+
|
|
193
|
+
# When 'less than'/'<',
|
|
194
|
+
# increment the last available segment only so that the new version is within the constraint
|
|
195
|
+
if op == "<"
|
|
196
|
+
new_segments = version.segments.map.with_index do |_, index|
|
|
197
|
+
version_to_be_permitted.segments[index]
|
|
198
|
+
end
|
|
199
|
+
new_segments[-1] += 1
|
|
200
|
+
# When 'less-than/equal'/'<=', use the new version as-is even when previously set as a non-semver version
|
|
201
|
+
# OpenTofu treats shortened versions the same as a version with any remaining segments as 0
|
|
202
|
+
# Example: '0.2' is treated as '0.2.0' | '1' is treated as '1.0.0'
|
|
203
|
+
elsif op == "<="
|
|
204
|
+
new_segments = version_to_be_permitted.segments
|
|
205
|
+
else
|
|
206
|
+
raise "Unexpected operation: #{op}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
requirement_class.new("#{op} #{new_segments.join('.')}")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { returns(T.class_of(Dependabot::Opentofu::Version)) }
|
|
213
|
+
def version_class
|
|
214
|
+
Version
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
sig { returns(T.class_of(Dependabot::Opentofu::Requirement)) }
|
|
218
|
+
def requirement_class
|
|
219
|
+
Requirement
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/update_checkers/base"
|
|
5
|
+
require "dependabot/opentofu/package/package_details_fetcher"
|
|
6
|
+
require "sorbet-runtime"
|
|
7
|
+
require "dependabot/git_commit_checker"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module Opentofu
|
|
11
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
12
|
+
class LatestVersionResolver
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
DAY_IN_SECONDS = T.let(24 * 60 * 60, Integer)
|
|
16
|
+
|
|
17
|
+
sig do
|
|
18
|
+
params(
|
|
19
|
+
dependency: Dependabot::Dependency,
|
|
20
|
+
credentials: T::Array[Dependabot::Credential],
|
|
21
|
+
cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions),
|
|
22
|
+
git_commit_checker: Dependabot::GitCommitChecker
|
|
23
|
+
).void
|
|
24
|
+
end
|
|
25
|
+
def initialize(dependency:, credentials:, cooldown_options:, git_commit_checker:)
|
|
26
|
+
@dependency = dependency
|
|
27
|
+
@credentials = credentials
|
|
28
|
+
@cooldown_options = cooldown_options
|
|
29
|
+
@git_commit_checker = T.let(
|
|
30
|
+
git_commit_checker,
|
|
31
|
+
Dependabot::GitCommitChecker
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(Dependabot::Dependency) }
|
|
36
|
+
attr_reader :dependency
|
|
37
|
+
|
|
38
|
+
# Return latest version tag for the dependency, it removes tags that are in cooldown period
|
|
39
|
+
# and returns the latest version tag that is not in cooldown period. If exception occurs
|
|
40
|
+
# it will return the latest version tag from the git_commit_checker. as it was before
|
|
41
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
42
|
+
def latest_version_tag
|
|
43
|
+
# step one fetch allowed version tags and
|
|
44
|
+
allowed_version_tags = git_commit_checker.allowed_version_tags
|
|
45
|
+
begin
|
|
46
|
+
if cooldown_enabled?
|
|
47
|
+
# sort the allowed version tags by name in descending order
|
|
48
|
+
select_version_tags_in_cooldown_period&.each do |tag_name|
|
|
49
|
+
# filter out if name is not in cooldown period
|
|
50
|
+
allowed_version_tags.reject! do |gitref_filtered|
|
|
51
|
+
true if gitref_filtered.name == tag_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
Dependabot.logger.info(
|
|
56
|
+
"Allowed version tags after filtering versions in cooldown:
|
|
57
|
+
#{allowed_version_tags.map(&:name).join(', ')}"
|
|
58
|
+
)
|
|
59
|
+
git_commit_checker.max_local_tag(allowed_version_tags)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Dependabot.logger.error("Error fetching latest version tag: #{e.message}")
|
|
62
|
+
git_commit_checker.local_tag_for_latest_version
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# To filter versions in cooldown period based on version tags from registry call
|
|
67
|
+
sig do
|
|
68
|
+
params(versions: T::Array[Dependabot::Opentofu::Version])
|
|
69
|
+
.returns(T::Array[Dependabot::Opentofu::Version])
|
|
70
|
+
end
|
|
71
|
+
def filter_versions_in_cooldown_period_from_provider(versions)
|
|
72
|
+
# to make call for registry to get the versions
|
|
73
|
+
# step one fetch allowed version tags and
|
|
74
|
+
|
|
75
|
+
# sort the allowed version tags by name in descending order
|
|
76
|
+
select_tags_which_in_cooldown_from_provider&.each do |tag_name|
|
|
77
|
+
# Iterate through versions and filter out those matching the tag_name
|
|
78
|
+
versions.reject! do |version|
|
|
79
|
+
version.to_s == tag_name
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
Dependabot.logger.info(
|
|
83
|
+
"Allowed version tags after filtering versions in cooldown:
|
|
84
|
+
#{versions.map(&:to_s).join(', ')}"
|
|
85
|
+
)
|
|
86
|
+
versions
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
Dependabot.logger.error("Error filter_versions_in_cooldown_period_from_provider(versions): #{e.message}")
|
|
89
|
+
versions
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# To filter versions in cooldown period based on version tags from registry call
|
|
93
|
+
sig do
|
|
94
|
+
params(versions: T::Array[Dependabot::Opentofu::Version])
|
|
95
|
+
.returns(T::Array[Dependabot::Opentofu::Version])
|
|
96
|
+
end
|
|
97
|
+
def filter_versions_in_cooldown_period_from_module(versions)
|
|
98
|
+
# to make call for registry to get the versions
|
|
99
|
+
# step one fetch allowed version tags and
|
|
100
|
+
|
|
101
|
+
# sort the allowed version tags by name in descending order
|
|
102
|
+
select_tags_which_in_cooldown_from_module&.each do |tag_name|
|
|
103
|
+
# Iterate through versions and filter out those matching the tag_name
|
|
104
|
+
versions.reject! do |version|
|
|
105
|
+
version.to_s == tag_name
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
Dependabot.logger.info(
|
|
109
|
+
"filter_versions_in_cooldown_period_from_module::
|
|
110
|
+
Allowed version tags after filtering versions in cooldown:#{versions.map(&:to_s).join(', ')}"
|
|
111
|
+
)
|
|
112
|
+
versions
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
Dependabot.logger.error("Error fetching latest version tag: #{e.message}")
|
|
115
|
+
versions
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { returns(T.nilable(T::Array[String])) }
|
|
119
|
+
def select_version_tags_in_cooldown_period
|
|
120
|
+
version_tags_in_cooldown_period = T.let([], T::Array[String])
|
|
121
|
+
|
|
122
|
+
package_details_fetcher.fetch_tag_and_release_date.each do |git_tag_with_detail|
|
|
123
|
+
if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
|
|
124
|
+
version_tags_in_cooldown_period << git_tag_with_detail.tag
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
version_tags_in_cooldown_period
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
|
|
130
|
+
version_tags_in_cooldown_period
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig { params(release_date: String).returns(T::Boolean) }
|
|
134
|
+
def check_if_version_in_cooldown_period?(release_date)
|
|
135
|
+
return false unless release_date.length.positive?
|
|
136
|
+
|
|
137
|
+
cooldown = @cooldown_options
|
|
138
|
+
return false unless cooldown
|
|
139
|
+
|
|
140
|
+
return false if cooldown.nil?
|
|
141
|
+
|
|
142
|
+
# Calculate the number of seconds passed since the release
|
|
143
|
+
passed_seconds = Time.now.to_i - release_date_to_seconds(release_date)
|
|
144
|
+
# Check if the release is within the cooldown period
|
|
145
|
+
passed_seconds < cooldown.default_days * DAY_IN_SECONDS
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { params(release_date: String).returns(Integer) }
|
|
149
|
+
def release_date_to_seconds(release_date)
|
|
150
|
+
Time.parse(release_date).to_i
|
|
151
|
+
rescue ArgumentError => e
|
|
152
|
+
Dependabot.logger.error("Invalid release date format: #{release_date} and error: #{e.message}")
|
|
153
|
+
0 # Default to 360 days in seconds if parsing fails, so that it will not be in cooldown
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sig { returns(T.nilable(T::Array[String])) }
|
|
157
|
+
def select_tags_which_in_cooldown_from_provider
|
|
158
|
+
version_tags_in_cooldown_from_provider = T.let([], T::Array[String])
|
|
159
|
+
|
|
160
|
+
package_details_fetcher.fetch_tag_and_release_date_from_provider.each do |git_tag_with_detail|
|
|
161
|
+
if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
|
|
162
|
+
version_tags_in_cooldown_from_provider << git_tag_with_detail.tag
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
version_tags_in_cooldown_from_provider
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
|
|
168
|
+
version_tags_in_cooldown_from_provider
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
sig { returns(T.nilable(T::Array[String])) }
|
|
172
|
+
def select_tags_which_in_cooldown_from_module
|
|
173
|
+
version_tags_in_cooldown_from_module = T.let([], T::Array[String])
|
|
174
|
+
|
|
175
|
+
package_details_fetcher.fetch_tag_and_release_date_from_module.each do |git_tag_with_detail|
|
|
176
|
+
if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
|
|
177
|
+
version_tags_in_cooldown_from_module << git_tag_with_detail.tag
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
version_tags_in_cooldown_from_module
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
|
|
183
|
+
version_tags_in_cooldown_from_module
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sig { returns(Package::PackageDetailsFetcher) }
|
|
187
|
+
def package_details_fetcher
|
|
188
|
+
@package_details_fetcher ||= T.let(
|
|
189
|
+
Package::PackageDetailsFetcher.new(
|
|
190
|
+
dependency: dependency,
|
|
191
|
+
credentials: credentials,
|
|
192
|
+
git_commit_checker: git_commit_checker
|
|
193
|
+
),
|
|
194
|
+
T.nilable(Package::PackageDetailsFetcher)
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
sig { returns(T::Boolean) }
|
|
199
|
+
def cooldown_enabled?
|
|
200
|
+
# This is a simple check to see if user has put cooldown days.
|
|
201
|
+
# If not set, then we aassume user does not want cooldown.
|
|
202
|
+
# Since OpenTofu does not support Semver versioning, So option left
|
|
203
|
+
# for the user is to set cooldown default days.
|
|
204
|
+
return false if @cooldown_options.nil?
|
|
205
|
+
|
|
206
|
+
@cooldown_options.default_days.positive?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
sig { returns(Dependabot::GitCommitChecker) }
|
|
210
|
+
attr_reader :git_commit_checker
|
|
211
|
+
|
|
212
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
213
|
+
attr_reader :credentials
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/update_checkers"
|
|
7
|
+
require "dependabot/update_checkers/base"
|
|
8
|
+
require "dependabot/git_commit_checker"
|
|
9
|
+
require "dependabot/opentofu/requirements_updater"
|
|
10
|
+
require "dependabot/opentofu/requirement"
|
|
11
|
+
require "dependabot/opentofu/version"
|
|
12
|
+
require "dependabot/opentofu/registry_client"
|
|
13
|
+
|
|
14
|
+
module Dependabot
|
|
15
|
+
module Opentofu
|
|
16
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
require_relative "update_checker/latest_version_resolver"
|
|
20
|
+
|
|
21
|
+
ELIGIBLE_SOURCE_TYPES = T.let(
|
|
22
|
+
%w(git provider registry).freeze,
|
|
23
|
+
T::Array[String]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
27
|
+
def latest_version
|
|
28
|
+
return latest_version_for_git_dependency if git_dependency?
|
|
29
|
+
return latest_version_for_registry_dependency if registry_dependency?
|
|
30
|
+
|
|
31
|
+
latest_version_for_provider_dependency if provider_dependency?
|
|
32
|
+
# Other sources (mercurial, path dependencies) just return `nil`
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
36
|
+
def latest_resolvable_version
|
|
37
|
+
# No concept of resolvability for terraform modules (that we're aware
|
|
38
|
+
# of - there may be in future).
|
|
39
|
+
latest_version
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
|
|
43
|
+
def latest_resolvable_version_with_no_unlock
|
|
44
|
+
# TODO: Update later to use lock files
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
49
|
+
def updated_requirements
|
|
50
|
+
RequirementsUpdater.new(
|
|
51
|
+
requirements: dependency.requirements,
|
|
52
|
+
latest_version: latest_version&.to_s,
|
|
53
|
+
tag_for_latest_version: tag_for_latest_version
|
|
54
|
+
).updated_requirements
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { returns(T::Boolean) }
|
|
58
|
+
def requirements_unlocked_or_can_be?
|
|
59
|
+
# If the requirement comes from a proxy URL then there's no way for
|
|
60
|
+
# us to update it
|
|
61
|
+
!proxy_requirement?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
sig { override.returns(T::Boolean) }
|
|
67
|
+
def latest_version_resolvable_with_full_unlock?
|
|
68
|
+
# Full unlock checks aren't relevant for OpenTofu files
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
73
|
+
def updated_dependencies_after_full_unlock
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { returns(T.nilable(Dependabot::Opentofu::Version)) }
|
|
78
|
+
def latest_version_for_registry_dependency
|
|
79
|
+
return unless registry_dependency?
|
|
80
|
+
|
|
81
|
+
return @latest_version_for_registry_dependency if @latest_version_for_registry_dependency
|
|
82
|
+
|
|
83
|
+
versions = all_module_versions
|
|
84
|
+
# Filter versions which are in cooldown period
|
|
85
|
+
if cooldown_enabled? # rubocop:disable Style/IfUnlessModifier
|
|
86
|
+
versions = latest_version_resolver.filter_versions_in_cooldown_period_from_module(versions)
|
|
87
|
+
end
|
|
88
|
+
versions.reject!(&:prerelease?) unless wants_prerelease?
|
|
89
|
+
versions.reject! { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
|
|
90
|
+
@latest_version_for_registry_dependency = T.let(
|
|
91
|
+
versions.max,
|
|
92
|
+
T.nilable(Dependabot::Opentofu::Version)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { returns(T::Array[Dependabot::Opentofu::Version]) }
|
|
97
|
+
def all_module_versions
|
|
98
|
+
identifier = dependency_source_details&.fetch(:module_identifier)
|
|
99
|
+
registry_client.all_module_versions(identifier: identifier)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { returns(T::Array[Dependabot::Opentofu::Version]) }
|
|
103
|
+
def all_provider_versions
|
|
104
|
+
identifier = dependency_source_details&.fetch(:module_identifier)
|
|
105
|
+
registry_client.all_provider_versions(identifier: identifier)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { returns(Dependabot::Opentofu::RegistryClient) }
|
|
109
|
+
def registry_client
|
|
110
|
+
@registry_client ||= T.let(
|
|
111
|
+
begin
|
|
112
|
+
hostname = dependency_source_details&.fetch(:registry_hostname)
|
|
113
|
+
RegistryClient.new(hostname: hostname, credentials: credentials)
|
|
114
|
+
end,
|
|
115
|
+
T.nilable(Dependabot::Opentofu::RegistryClient)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { returns(T.nilable(Dependabot::Opentofu::Version)) }
|
|
120
|
+
def latest_version_for_provider_dependency
|
|
121
|
+
return unless provider_dependency?
|
|
122
|
+
|
|
123
|
+
return @latest_version_for_provider_dependency if @latest_version_for_provider_dependency
|
|
124
|
+
|
|
125
|
+
versions = all_provider_versions
|
|
126
|
+
# Filter versions which are in cooldown period
|
|
127
|
+
if cooldown_enabled?
|
|
128
|
+
versions = latest_version_resolver.filter_versions_in_cooldown_period_from_provider(versions)
|
|
129
|
+
end
|
|
130
|
+
versions.reject!(&:prerelease?) unless wants_prerelease?
|
|
131
|
+
versions.reject! { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
|
|
132
|
+
|
|
133
|
+
@latest_version_for_provider_dependency = T.let(
|
|
134
|
+
versions.max,
|
|
135
|
+
T.nilable(Dependabot::Opentofu::Version)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { returns(T::Boolean) }
|
|
140
|
+
def wants_prerelease?
|
|
141
|
+
current_version = dependency.version
|
|
142
|
+
if current_version &&
|
|
143
|
+
version_class.correct?(current_version) &&
|
|
144
|
+
version_class.new(current_version).prerelease?
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
dependency.requirements.any? do |req|
|
|
149
|
+
req[:requirement]&.match?(/\d-[A-Za-z0-9]/)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { returns(T.nilable(T.any(Dependabot::Version, String))) }
|
|
154
|
+
def latest_version_for_git_dependency
|
|
155
|
+
# If the module isn't pinned then there's nothing for us to update
|
|
156
|
+
# (since there's no lockfile to update the version in). We still
|
|
157
|
+
# return the latest commit for the given branch, in order to keep
|
|
158
|
+
# this method consistent
|
|
159
|
+
return git_commit_checker.head_commit_for_current_branch unless git_commit_checker.pinned?
|
|
160
|
+
|
|
161
|
+
# If the dependency is pinned to a tag that looks like a version then
|
|
162
|
+
# we want to update that tag. Because we don't have a lockfile, the
|
|
163
|
+
# latest version is the tag itself.
|
|
164
|
+
if git_commit_checker.pinned_ref_looks_like_version?
|
|
165
|
+
# Filter version tags that are in cooldown period
|
|
166
|
+
latest_tag = latest_version_resolver.latest_version_tag&.fetch(:tag)
|
|
167
|
+
version_rgx = GitCommitChecker::VERSION_REGEX
|
|
168
|
+
return unless latest_tag.match(version_rgx)
|
|
169
|
+
|
|
170
|
+
version = latest_tag.match(version_rgx)
|
|
171
|
+
.named_captures.fetch("version")
|
|
172
|
+
return version_class.new(version)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# If the dependency is pinned to a tag that doesn't look like a
|
|
176
|
+
# version then there's nothing we can do.
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
sig { returns(T.nilable(String)) }
|
|
181
|
+
def tag_for_latest_version
|
|
182
|
+
return unless git_commit_checker.git_dependency?
|
|
183
|
+
return unless git_commit_checker.pinned?
|
|
184
|
+
return unless git_commit_checker.pinned_ref_looks_like_version?
|
|
185
|
+
|
|
186
|
+
latest_tag = git_commit_checker.local_tag_for_latest_version
|
|
187
|
+
&.fetch(:tag)
|
|
188
|
+
|
|
189
|
+
version_rgx = GitCommitChecker::VERSION_REGEX
|
|
190
|
+
return unless latest_tag.match(version_rgx)
|
|
191
|
+
|
|
192
|
+
latest_tag
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { returns(T::Boolean) }
|
|
196
|
+
def proxy_requirement?
|
|
197
|
+
dependency.requirements.any? do |req|
|
|
198
|
+
req.fetch(:source)&.fetch(:proxy_url, nil)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
sig { returns(T::Boolean) }
|
|
203
|
+
def registry_dependency?
|
|
204
|
+
return false if dependency_source_details.nil?
|
|
205
|
+
|
|
206
|
+
dependency_source_details&.fetch(:type) == "registry"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
sig { returns(T::Boolean) }
|
|
210
|
+
def provider_dependency?
|
|
211
|
+
return false if dependency_source_details.nil?
|
|
212
|
+
|
|
213
|
+
dependency_source_details&.fetch(:type) == "provider"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
sig { returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
|
|
217
|
+
def dependency_source_details
|
|
218
|
+
dependency.source_details(allowed_types: ELIGIBLE_SOURCE_TYPES)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
sig { returns(T::Boolean) }
|
|
222
|
+
def git_dependency?
|
|
223
|
+
git_commit_checker.git_dependency?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
sig { returns(LatestVersionResolver) }
|
|
227
|
+
def latest_version_resolver
|
|
228
|
+
LatestVersionResolver.new(
|
|
229
|
+
dependency: dependency,
|
|
230
|
+
credentials: credentials,
|
|
231
|
+
cooldown_options: update_cooldown,
|
|
232
|
+
git_commit_checker: git_commit_checker
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
sig { returns(Dependabot::GitCommitChecker) }
|
|
237
|
+
def git_commit_checker
|
|
238
|
+
@git_commit_checker ||= T.let(
|
|
239
|
+
GitCommitChecker.new(
|
|
240
|
+
dependency: dependency,
|
|
241
|
+
credentials: credentials,
|
|
242
|
+
ignored_versions: ignored_versions,
|
|
243
|
+
raise_on_ignored: raise_on_ignored
|
|
244
|
+
),
|
|
245
|
+
T.nilable(Dependabot::GitCommitChecker)
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { returns(T::Boolean) }
|
|
250
|
+
def cooldown_enabled?
|
|
251
|
+
# This is a simple check to see if user has put cooldown days.
|
|
252
|
+
# If not set, then we aassume user does not want cooldown.
|
|
253
|
+
# Since OpenTofu does not support Semver versioning, So option left
|
|
254
|
+
# for the user is to set cooldown default days.
|
|
255
|
+
return false if update_cooldown.nil?
|
|
256
|
+
|
|
257
|
+
T.must(update_cooldown&.default_days).positive?
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
Dependabot::UpdateCheckers
|
|
264
|
+
.register("opentofu", Dependabot::Opentofu::UpdateChecker)
|