dependabot-terraform 0.76.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 781b56819ef8fc54cb1b635f913f81139d149c80e6461d86b9e61e36916aa9c8
4
+ data.tar.gz: ed1697ae2b43e42d4b76db373643d28c3dff4c0f67c07291f8180fc56da0c341
5
+ SHA512:
6
+ metadata.gz: b51f5746555c34bffed4736fa11217c8b24e8fb1d4f464846c750572e9e55f77680231b0d2603555f06ae14b35dc34bcc16808cd9740566c0d7ae24a2951900e
7
+ data.tar.gz: 01cbd90a18f476d9c7521c7facc5e225e1c2afd3b8954d4f0efb88daf69d370630d1a840b6eccbdd0add3c2fa277d46aa44935e6c6fd673d70dbe0b174f2cb54
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ install_dir=$1
6
+ if [ -z "$install_dir" ]; then
7
+ echo "usage: $0 INSTALL_DIR"
8
+ exit 1
9
+ fi
10
+
11
+ if [ ! -d "$install_dir/bin" ]; then
12
+ mkdir -p "$install_dir/bin"
13
+ fi
14
+
15
+ os="$(uname -s | tr '[:upper:]' '[:lower:]')"
16
+ github_url="https://github.com/kvz/json2hcl"
17
+ url="${github_url}/releases/download/v0.0.6/json2hcl_v0.0.6_${os}_amd64"
18
+ wget -O "$install_dir/bin/json2hcl" "$url"
19
+ chmod +x "$install_dir/bin/json2hcl"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These all need to be required so the various classes can be registered in a
4
+ # lookup table of package manager names to concrete classes.
5
+ require "dependabot/terraform/file_fetcher"
6
+ require "dependabot/terraform/file_parser"
7
+ require "dependabot/terraform/update_checker"
8
+ require "dependabot/terraform/file_updater"
9
+ require "dependabot/terraform/metadata_finder"
10
+ require "dependabot/terraform/requirement"
11
+ require "dependabot/terraform/version"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_fetchers"
4
+ require "dependabot/file_fetchers/base"
5
+
6
+ module Dependabot
7
+ module Terraform
8
+ class FileFetcher < Dependabot::FileFetchers::Base
9
+ def self.required_files_in?(filenames)
10
+ filenames.any? { |f| f.end_with?(".tf", ".tfvars") }
11
+ end
12
+
13
+ def self.required_files_message
14
+ "Repo must contain a Terraform configuration file."
15
+ end
16
+
17
+ private
18
+
19
+ def fetch_files
20
+ fetched_files = []
21
+ fetched_files += terraform_files
22
+ fetched_files += terragrunt_files
23
+
24
+ return fetched_files if fetched_files.any?
25
+
26
+ raise(
27
+ Dependabot::DependencyFileNotFound,
28
+ File.join(directory, "<anything>.tf")
29
+ )
30
+ end
31
+
32
+ def terraform_files
33
+ @terraform_files ||=
34
+ repo_contents(raise_errors: false).
35
+ select { |f| f.type == "file" && f.name.end_with?(".tf") }.
36
+ map { |f| fetch_file_from_host(f.name) }
37
+ end
38
+
39
+ def terragrunt_files
40
+ @terragrunt_files ||=
41
+ repo_contents(raise_errors: false).
42
+ select { |f| f.type == "file" && f.name.end_with?(".tfvars") }.
43
+ map { |f| fetch_file_from_host(f.name) }
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ Dependabot::FileFetchers.
50
+ register("terraform", Dependabot::Terraform::FileFetcher)
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "excon"
5
+ require "nokogiri"
6
+ require "dependabot/dependency"
7
+ require "dependabot/file_parsers"
8
+ require "dependabot/file_parsers/base"
9
+ require "dependabot/git_commit_checker"
10
+ require "dependabot/shared_helpers"
11
+ require "dependabot/errors"
12
+
13
+ module Dependabot
14
+ module Terraform
15
+ class FileParser < Dependabot::FileParsers::Base
16
+ require "dependabot/file_parsers/base/dependency_set"
17
+
18
+ ARCHIVE_EXTENSIONS = %w(.zip .tbz2 .tgz .txz).freeze
19
+
20
+ def parse
21
+ dependency_set = DependencySet.new
22
+
23
+ terraform_files.each do |file|
24
+ modules = parsed_file(file).fetch("module", []).map(&:first)
25
+ modules.each do |name, details|
26
+ dependency_set << build_terraform_dependency(file, name, details)
27
+ end
28
+ end
29
+
30
+ terragrunt_files.each do |file|
31
+ modules = parsed_file(file).fetch("terragrunt", []).first || {}
32
+ modules = modules.fetch("terraform", [])
33
+ modules.each do |details|
34
+ next unless details["source"]
35
+
36
+ dependency_set << build_terragrunt_dependency(file, details)
37
+ end
38
+ end
39
+
40
+ dependency_set.dependencies
41
+ end
42
+
43
+ private
44
+
45
+ def build_terraform_dependency(file, name, details)
46
+ details = details.first
47
+
48
+ source = source_from(details)
49
+ dep_name =
50
+ source[:type] == "registry" ? source[:module_identifier] : name
51
+ version_req = details["version"]&.strip
52
+ version =
53
+ if source[:type] == "git" then version_from_ref(source[:ref])
54
+ elsif version_req&.match?(/^\d/) then version_req
55
+ end
56
+
57
+ Dependency.new(
58
+ name: dep_name,
59
+ version: version,
60
+ package_manager: "terraform",
61
+ requirements: [
62
+ requirement: version_req,
63
+ groups: [],
64
+ file: file.name,
65
+ source: source
66
+ ]
67
+ )
68
+ end
69
+
70
+ def build_terragrunt_dependency(file, details)
71
+ source = source_from(details)
72
+ dep_name =
73
+ if Source.from_url(source[:url])
74
+ Source.from_url(source[:url]).repo
75
+ else
76
+ source[:url]
77
+ end
78
+
79
+ version = version_from_ref(source[:ref])
80
+
81
+ Dependency.new(
82
+ name: dep_name,
83
+ version: version,
84
+ package_manager: "terraform",
85
+ requirements: [
86
+ requirement: nil,
87
+ groups: [],
88
+ file: file.name,
89
+ source: source
90
+ ]
91
+ )
92
+ end
93
+
94
+ # Full docs at https://www.terraform.io/docs/modules/sources.html
95
+ def source_from(details_hash)
96
+ raw_source = details_hash.fetch("source")
97
+ bare_source = get_proxied_source(raw_source)
98
+
99
+ source_details =
100
+ case source_type(bare_source)
101
+ when :http_archive, :path, :mercurial, :s3
102
+ { type: source_type(bare_source).to_s, url: bare_source }
103
+ when :github, :bitbucket, :git
104
+ git_source_details_from(bare_source)
105
+ when :registry
106
+ registry_source_details_from(bare_source)
107
+ end
108
+
109
+ source_details[:proxy_url] = raw_source if raw_source != bare_source
110
+ source_details
111
+ end
112
+
113
+ def registry_source_details_from(source_string)
114
+ parts = source_string.split("/")
115
+
116
+ if parts.count == 3
117
+ {
118
+ type: "registry",
119
+ registry_hostname: "registry.terraform.io",
120
+ module_identifier: source_string
121
+ }
122
+ elsif parts.count == 4
123
+ {
124
+ type: "registry",
125
+ registry_hostname: parts.first,
126
+ module_identifier: parts[1..3].join("/")
127
+ }
128
+ else
129
+ msg = "Invalid registry source specified: '#{source_string}'"
130
+ raise DependencyFileNotEvaluatable, msg
131
+ end
132
+ end
133
+
134
+ def git_source_details_from(source_string)
135
+ git_url = source_string.strip.gsub(/^git::/, "")
136
+ unless git_url.start_with?("git@") || git_url.include?("://")
137
+ git_url = "https://" + git_url
138
+ end
139
+
140
+ bare_uri =
141
+ if git_url.include?("git@")
142
+ git_url.split("git@").last.sub(":", "/")
143
+ else
144
+ git_url.sub(%r{.*?://}, "")
145
+ end
146
+
147
+ querystr = URI.parse("https://" + bare_uri).query
148
+ git_url = git_url.split(%r{(?<!:)//}).first.gsub("?#{querystr}", "")
149
+
150
+ {
151
+ type: "git",
152
+ url: git_url,
153
+ branch: nil,
154
+ ref: CGI.parse(querystr.to_s)["ref"].first
155
+ }
156
+ end
157
+
158
+ def version_from_ref(ref)
159
+ version_regex = GitCommitChecker::VERSION_REGEX
160
+ return unless ref&.match?(version_regex)
161
+
162
+ ref.match(version_regex).named_captures.fetch("version")
163
+ end
164
+
165
+ # See https://www.terraform.io/docs/modules/sources.html#http-urls for
166
+ # details of how Terraform handle HTTP(S) sources for modules
167
+ def get_proxied_source(raw_source)
168
+ return raw_source unless raw_source.start_with?("http")
169
+
170
+ uri = URI.parse(raw_source.split(%r{(?<!:)//}).first)
171
+ return raw_source if uri.path.end_with?(*ARCHIVE_EXTENSIONS)
172
+ return raw_source if URI.parse(raw_source).query.include?("archive=")
173
+
174
+ url = raw_source.split(%r{(?<!:)//}).first + "?terraform-get=1"
175
+
176
+ response = Excon.get(
177
+ url,
178
+ idempotent: true,
179
+ **SharedHelpers.excon_defaults
180
+ )
181
+
182
+ if response.headers["X-Terraform-Get"]
183
+ return response.headers["X-Terraform-Get"]
184
+ end
185
+
186
+ doc = Nokogiri::XML(response.body)
187
+ doc.css("meta").find do |tag|
188
+ tag.attributes&.fetch("name", nil)&.value == "terraform-get"
189
+ end&.attributes&.fetch("content", nil)&.value
190
+ end
191
+
192
+ # rubocop:disable Metrics/CyclomaticComplexity
193
+ # rubocop:disable Metrics/PerceivedComplexity
194
+ def source_type(source_string)
195
+ return :path if source_string.start_with?(".")
196
+ return :github if source_string.start_with?("github.com/")
197
+ return :bitbucket if source_string.start_with?("bitbucket.org/")
198
+ return :git if source_string.start_with?("git::")
199
+ return :mercurial if source_string.start_with?("hg::")
200
+ return :s3 if source_string.start_with?("s3::")
201
+
202
+ if source_string.split("/").first.include?("::")
203
+ raise "Unknown src: #{source_string}"
204
+ end
205
+
206
+ return :registry unless source_string.start_with?("http")
207
+
208
+ path_uri = URI.parse(source_string.split(%r{(?<!:)//}).first)
209
+ query_uri = URI.parse(source_string)
210
+ return :http_archive if path_uri.path.end_with?(*ARCHIVE_EXTENSIONS)
211
+ return :http_archive if query_uri.query.include?("archive=")
212
+
213
+ raise "HTTP source, but not an archive!"
214
+ end
215
+ # rubocop:enable Metrics/CyclomaticComplexity
216
+ # rubocop:enable Metrics/PerceivedComplexity
217
+
218
+ def parsed_file(file)
219
+ @parsed_buildfile ||= {}
220
+ @parsed_buildfile[file.name] ||=
221
+ SharedHelpers.in_a_temporary_directory do
222
+ File.write("tmp.tf", file.content)
223
+
224
+ command = "#{terraform_parser_path} -reverse < tmp.tf"
225
+ raw_response = nil
226
+ IO.popen(command) { |process| raw_response = process.read }
227
+
228
+ unless $CHILD_STATUS.success?
229
+ raise SharedHelpers::HelperSubprocessFailed.new(
230
+ raw_response,
231
+ command
232
+ )
233
+ end
234
+
235
+ JSON.parse(raw_response)
236
+ end
237
+ end
238
+
239
+ def terraform_parser_path
240
+ helper_bin_dir = File.join(native_helpers_root, "terraform/bin")
241
+ Pathname.new(File.join(helper_bin_dir, "json2hcl")).cleanpath.to_path
242
+ end
243
+
244
+ def native_helpers_root
245
+ default_path = File.join(__dir__, "../../../helpers/install-dir")
246
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
247
+ end
248
+
249
+ def terraform_files
250
+ dependency_files.select { |f| f.name.end_with?(".tf") }
251
+ end
252
+
253
+ def terragrunt_files
254
+ dependency_files.select { |f| f.name.end_with?(".tfvars") }
255
+ end
256
+
257
+ def check_required_files
258
+ return if [*terraform_files, *terragrunt_files].any?
259
+
260
+ raise "No Terraform configuration file!"
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ Dependabot::FileParsers.
267
+ register("terraform", Dependabot::Terraform::FileParser)
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+ require "dependabot/errors"
6
+
7
+ module Dependabot
8
+ module Terraform
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
+ def self.updated_files_regex
11
+ [/\.tf$/, /\.tfvars$/]
12
+ end
13
+
14
+ def updated_dependency_files
15
+ updated_files = []
16
+
17
+ [*terraform_files, *terragrunt_files].each do |file|
18
+ next unless file_changed?(file)
19
+
20
+ updated_content = updated_terraform_file_content(file)
21
+ raise "Content didn't change!" if updated_content == file.content
22
+
23
+ updated_files << updated_file(file: file, content: updated_content)
24
+ end
25
+
26
+ raise "No files changed!" if updated_files.none?
27
+
28
+ updated_files
29
+ end
30
+
31
+ private
32
+
33
+ def updated_terraform_file_content(file)
34
+ content = file.content.dup
35
+
36
+ reqs = dependency.requirements.zip(dependency.previous_requirements).
37
+ reject { |new_req, old_req| new_req == old_req }
38
+
39
+ # Loop through each changed requirement and update the files
40
+ reqs.each do |new_req, old_req|
41
+ raise "Bad req match" unless new_req[:file] == old_req[:file]
42
+ next unless new_req.fetch(:file) == file.name
43
+
44
+ case new_req[:source][:type]
45
+ when "git"
46
+ update_git_declaration(new_req, old_req, content, file.name)
47
+ when "registry"
48
+ update_registry_declaration(new_req, old_req, content)
49
+ else
50
+ raise "Don't know how to update a #{new_req[:source][:type]} "\
51
+ "declaration!"
52
+ end
53
+ end
54
+
55
+ content
56
+ end
57
+
58
+ def update_git_declaration(new_req, old_req, updated_content, filename)
59
+ url = old_req.fetch(:source)[:url].gsub(%r{^https://}, "")
60
+ tag = old_req.fetch(:source)[:ref]
61
+ url_regex = /#{Regexp.quote(url)}.*ref=#{Regexp.quote(tag)}/
62
+
63
+ declaration_regex = git_declaration_regex(filename)
64
+
65
+ updated_content.sub!(declaration_regex) do |regex_match|
66
+ regex_match.sub(url_regex) do |url_match|
67
+ url_match.sub(old_req[:source][:ref], new_req[:source][:ref])
68
+ end
69
+ end
70
+ end
71
+
72
+ def update_registry_declaration(new_req, old_req, updated_content)
73
+ updated_content.sub!(registry_declaration_regex) do |regex_match|
74
+ regex_match.sub(/version\s*=.*/) do |req_line_match|
75
+ req_line_match.sub(old_req[:requirement], new_req[:requirement])
76
+ end
77
+ end
78
+ end
79
+
80
+ def dependency
81
+ # Terraform updates will only ever be updating a single dependency
82
+ dependencies.first
83
+ end
84
+
85
+ def files_with_requirement
86
+ filenames = dependency.requirements.map { |r| r[:file] }
87
+ dependency_files.select { |file| filenames.include?(file.name) }
88
+ end
89
+
90
+ def terraform_files
91
+ dependency_files.select { |f| f.name.end_with?(".tf") }
92
+ end
93
+
94
+ def terragrunt_files
95
+ dependency_files.select { |f| f.name.end_with?(".tfvars") }
96
+ end
97
+
98
+ def check_required_files
99
+ return if [*terraform_files, *terragrunt_files].any?
100
+
101
+ raise "No Terraform configuration file!"
102
+ end
103
+
104
+ def registry_declaration_regex
105
+ /
106
+ (?<=\{)
107
+ (?:(?!^\}).)*
108
+ source\s*=\s*["']#{Regexp.escape(dependency.name)}["']
109
+ (?:(?!^\}).)*
110
+ /mx
111
+ end
112
+
113
+ def git_declaration_regex(filename)
114
+ # For terragrunt dependencies there's not a lot we can base the
115
+ # regex on. Just look for declarations within a `terraform` block
116
+ return /terraform\s*\{(?:(?!^\}).)*/m if filename.end_with?(".tfvars")
117
+
118
+ # For modules we can do better - filter for module blocks that use the
119
+ # name of the dependency
120
+ /
121
+ module\s+["']#{Regexp.escape(dependency.name)}["']\s*\{
122
+ (?:(?!^\}).)*
123
+ /mx
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ Dependabot::FileUpdaters.
130
+ register("terraform", Dependabot::Terraform::FileUpdater)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "json"
5
+ require "dependabot/metadata_finders"
6
+ require "dependabot/metadata_finders/base"
7
+ require "dependabot/shared_helpers"
8
+
9
+ module Dependabot
10
+ module Terraform
11
+ class MetadataFinder < Dependabot::MetadataFinders::Base
12
+ private
13
+
14
+ def look_up_source
15
+ case new_source_type
16
+ when "git" then find_source_from_git_url
17
+ when "registry" then find_source_from_registry_details
18
+ else raise "Unexpected source type: #{new_source_type}"
19
+ end
20
+ end
21
+
22
+ def new_source_type
23
+ sources =
24
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
25
+
26
+ return "default" if sources.empty?
27
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
28
+
29
+ sources.first[:type] || sources.first.fetch("type")
30
+ end
31
+
32
+ def find_source_from_git_url
33
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
34
+
35
+ url = info[:url] || info.fetch("url")
36
+ Source.from_url(url)
37
+ end
38
+
39
+ # Registry API docs:
40
+ # https://www.terraform.io/docs/registry/api.html
41
+ def find_source_from_registry_details
42
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
43
+
44
+ hostname = info[:registry_hostname] || info["registry_hostname"]
45
+
46
+ # TODO: Implement service discovery for custom registries
47
+ return unless hostname == "registry.terraform.io"
48
+
49
+ url = "https://registry.terraform.io/v1/modules/"\
50
+ "#{dependency.name}/#{dependency.version}"
51
+
52
+ response = Excon.get(
53
+ url,
54
+ idempotent: true,
55
+ **SharedHelpers.excon_defaults
56
+ )
57
+
58
+ unless response.status == 200
59
+ raise "Response from registry was #{response.status}"
60
+ end
61
+
62
+ source_url = JSON.parse(response.body).fetch("source")
63
+ Source.from_url(source_url) if source_url
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ Dependabot::MetadataFinders.
70
+ register("terraform", Dependabot::Terraform::MetadataFinder)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/terraform/version"
5
+
6
+ # Just ensures that Terraform requirements use Terraform versions
7
+ module Dependabot
8
+ module Terraform
9
+ class Requirement < Gem::Requirement
10
+ def self.parse(obj)
11
+ return ["=", Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
12
+
13
+ unless (matches = PATTERN.match(obj.to_s))
14
+ msg = "Illformed requirement [#{obj.inspect}]"
15
+ raise BadRequirementError, msg
16
+ end
17
+
18
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
19
+
20
+ [matches[1] || "=", Terraform::Version.new(matches[2])]
21
+ end
22
+
23
+ # For consistency with other langauges, we define a requirements array.
24
+ # Terraform doesn't have an `OR` separator for requirements, so it
25
+ # always contains a single element.
26
+ def self.requirements_array(requirement_string)
27
+ [new(requirement_string)]
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Dependabot::Utils.
34
+ register_requirement_class("terraform", Dependabot::Terraform::Requirement)
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ ####################################################################
4
+ # For more details on Terraform version constraints, see: #
5
+ # https://www.terraform.io/docs/modules/usage.html#module-versions #
6
+ ####################################################################
7
+
8
+ require "dependabot/terraform/version"
9
+ require "dependabot/terraform/requirement"
10
+
11
+ module Dependabot
12
+ module Terraform
13
+ class RequirementsUpdater
14
+ def initialize(requirements:, latest_version:,
15
+ tag_for_latest_version:)
16
+ @requirements = requirements
17
+ @tag_for_latest_version = tag_for_latest_version
18
+
19
+ return unless latest_version
20
+ return unless version_class.correct?(latest_version)
21
+
22
+ @latest_version = version_class.new(latest_version)
23
+ end
24
+
25
+ def updated_requirements
26
+ return requirements unless latest_version
27
+
28
+ # Note: Order is important here. The FileUpdater needs the updated
29
+ # requirement at index `i` to correspond to the previous requirement
30
+ # at the same index.
31
+ requirements.map do |req|
32
+ case req.dig(:source, :type)
33
+ when "git" then update_git_requirement(req)
34
+ when "registry" then update_registry_requirement(req)
35
+ else req
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :requirements, :latest_version, :tag_for_latest_version
43
+
44
+ def update_git_requirement(req)
45
+ return req unless req.dig(:source, :ref)
46
+ return req unless tag_for_latest_version
47
+
48
+ req.merge(source: req[:source].merge(ref: tag_for_latest_version))
49
+ end
50
+
51
+ def update_registry_requirement(req)
52
+ return req if req.fetch(:requirement).nil?
53
+
54
+ string_req = req.fetch(:requirement).strip
55
+ ruby_req = requirement_class.new(string_req)
56
+ return req if ruby_req.satisfied_by?(latest_version)
57
+
58
+ new_req =
59
+ if ruby_req.exact? then latest_version.to_s
60
+ elsif string_req.start_with?("~>")
61
+ update_twiddle_version(string_req).to_s
62
+ else
63
+ update_range(string_req).map(&:to_s).join(", ")
64
+ end
65
+
66
+ req.merge(requirement: new_req)
67
+ end
68
+
69
+ # Updates the version in a "~>" constraint to allow the given version
70
+ def update_twiddle_version(req_string)
71
+ old_version = requirement_class.new(req_string).
72
+ requirements.first.last
73
+ updated_version = at_same_precision(latest_version, old_version)
74
+ req_string.sub(old_version.to_s, updated_version)
75
+ end
76
+
77
+ def update_range(req_string)
78
+ requirement_class.new(req_string).requirements.flat_map do |r|
79
+ next r if r.satisfied_by?(latest_version)
80
+
81
+ case op = r.requirements.first.first
82
+ when "<", "<=" then [update_greatest_version(r, latest_version)]
83
+ when "!=" then []
84
+ else raise "Unexpected operation for unsatisfied req: #{op}"
85
+ end
86
+ end
87
+ end
88
+
89
+ def at_same_precision(new_version, old_version)
90
+ release_precision =
91
+ old_version.to_s.split(".").select { |i| i.match?(/^\d+$/) }.count
92
+ prerelease_precision =
93
+ old_version.to_s.split(".").count - release_precision
94
+
95
+ new_release =
96
+ new_version.to_s.split(".").first(release_precision)
97
+ new_prerelease =
98
+ new_version.to_s.split(".").
99
+ drop_while { |i| i.match?(/^\d+$/) }.
100
+ first([prerelease_precision, 1].max)
101
+
102
+ [*new_release, *new_prerelease].join(".")
103
+ end
104
+
105
+ # Updates the version in a "<" or "<=" constraint to allow the given
106
+ # version
107
+ def update_greatest_version(requirement, version_to_be_permitted)
108
+ if version_to_be_permitted.is_a?(String)
109
+ version_to_be_permitted =
110
+ version_class.new(version_to_be_permitted)
111
+ end
112
+ op, version = requirement.requirements.first
113
+ version = version.release if version.prerelease?
114
+
115
+ index_to_update =
116
+ version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
117
+
118
+ new_segments = version.segments.map.with_index do |_, index|
119
+ if index < index_to_update
120
+ version_to_be_permitted.segments[index]
121
+ elsif index == index_to_update
122
+ version_to_be_permitted.segments[index] + 1
123
+ else 0
124
+ end
125
+ end
126
+
127
+ requirement_class.new("#{op} #{new_segments.join('.')}")
128
+ end
129
+
130
+ def version_class
131
+ Version
132
+ end
133
+
134
+ def requirement_class
135
+ Requirement
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/update_checkers"
4
+ require "dependabot/update_checkers/base"
5
+ require "dependabot/git_commit_checker"
6
+ require "dependabot/terraform/requirements_updater"
7
+ require "dependabot/terraform/requirement"
8
+ require "dependabot/terraform/version"
9
+
10
+ module Dependabot
11
+ module Terraform
12
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
13
+ def latest_version
14
+ return latest_version_for_git_dependency if git_dependency?
15
+ return latest_version_for_registry_dependency if registry_dependency?
16
+ # Other sources (mercurial, path dependencies) just return `nil`
17
+ end
18
+
19
+ def latest_resolvable_version
20
+ # No concept of resolvability for terraform modules (that we're aware
21
+ # of - there may be in future).
22
+ latest_version
23
+ end
24
+
25
+ def latest_resolvable_version_with_no_unlock
26
+ # Irrelevant, since Terraform doesn't have a lockfile
27
+ nil
28
+ end
29
+
30
+ def updated_requirements
31
+ RequirementsUpdater.new(
32
+ requirements: dependency.requirements,
33
+ latest_version: latest_version&.to_s,
34
+ tag_for_latest_version: tag_for_latest_version
35
+ ).updated_requirements
36
+ end
37
+
38
+ def requirements_unlocked_or_can_be?
39
+ # If the requirement comes from a proxy URL then there's no way for
40
+ # us to update it
41
+ !proxy_requirement?
42
+ end
43
+
44
+ def requirement_class
45
+ Requirement
46
+ end
47
+
48
+ def version_class
49
+ Version
50
+ end
51
+
52
+ private
53
+
54
+ def latest_version_resolvable_with_full_unlock?
55
+ # Full unlock checks aren't relevant for Terraform files
56
+ false
57
+ end
58
+
59
+ def updated_dependencies_after_full_unlock
60
+ raise NotImplementedError
61
+ end
62
+
63
+ def latest_version_for_registry_dependency
64
+ return unless registry_dependency?
65
+
66
+ if @latest_version_for_registry_dependency
67
+ return @latest_version_for_registry_dependency
68
+ end
69
+
70
+ versions = all_registry_versions
71
+ versions.reject!(&:prerelease?) unless wants_prerelease?
72
+ versions.reject! { |v| ignore_reqs.any? { |r| r.satisfied_by?(v) } }
73
+
74
+ @latest_version_for_registry_dependency = versions.max
75
+ end
76
+
77
+ def all_registry_versions
78
+ hostname = dependency_source_details.fetch(:registry_hostname)
79
+ identifier = dependency_source_details.fetch(:module_identifier)
80
+
81
+ # TODO: Implement service discovery for custom registries
82
+ return unless hostname == "registry.terraform.io"
83
+
84
+ url = "https://registry.terraform.io/v1/modules/"\
85
+ "#{identifier}/versions"
86
+
87
+ response = Excon.get(
88
+ url,
89
+ idempotent: true,
90
+ **SharedHelpers.excon_defaults
91
+ )
92
+
93
+ unless response.status == 200
94
+ raise "Response from registry was #{response.status}"
95
+ end
96
+
97
+ JSON.parse(response.body).
98
+ fetch("modules").first.fetch("versions").
99
+ map { |release| version_class.new(release.fetch("version")) }
100
+ end
101
+
102
+ def wants_prerelease?
103
+ current_version = dependency.version
104
+ if current_version &&
105
+ version_class.correct?(current_version) &&
106
+ version_class.new(current_version).prerelease?
107
+ return true
108
+ end
109
+
110
+ dependency.requirements.any? do |req|
111
+ req[:requirement]&.match?(/\d-[A-Za-z0-9]/)
112
+ end
113
+ end
114
+
115
+ def latest_version_for_git_dependency
116
+ # If the module isn't pinned then there's nothing for us to update
117
+ # (since there's no lockfile to update the version in). We still
118
+ # return the latest commit for the given branch, in order to keep
119
+ # this method consistent
120
+ unless git_commit_checker.pinned?
121
+ return git_commit_checker.head_commit_for_current_branch
122
+ end
123
+
124
+ # If the dependency is pinned to a tag that looks like a version then
125
+ # we want to update that tag. Because we don't have a lockfile, the
126
+ # latest version is the tag itself.
127
+ if git_commit_checker.pinned_ref_looks_like_version?
128
+ latest_tag = git_commit_checker.local_tag_for_latest_version&.
129
+ fetch(:tag)
130
+ version_rgx = GitCommitChecker::VERSION_REGEX
131
+ return unless latest_tag.match(version_rgx)
132
+
133
+ version = latest_tag.match(version_rgx).
134
+ named_captures.fetch("version")
135
+ return version_class.new(version)
136
+ end
137
+
138
+ # If the dependency is pinned to a tag that doesn't look like a
139
+ # version then there's nothing we can do.
140
+ nil
141
+ end
142
+
143
+ def tag_for_latest_version
144
+ return unless git_commit_checker.git_dependency?
145
+ return unless git_commit_checker.pinned?
146
+ return unless git_commit_checker.pinned_ref_looks_like_version?
147
+
148
+ latest_tag = git_commit_checker.local_tag_for_latest_version&.
149
+ fetch(:tag)
150
+
151
+ version_rgx = GitCommitChecker::VERSION_REGEX
152
+ return unless latest_tag.match(version_rgx)
153
+
154
+ latest_tag
155
+ end
156
+
157
+ def proxy_requirement?
158
+ dependency.requirements.any? do |req|
159
+ req.fetch(:source)&.fetch(:proxy_url, nil)
160
+ end
161
+ end
162
+
163
+ def registry_dependency?
164
+ return false if dependency_source_details.nil?
165
+
166
+ dependency_source_details.fetch(:type) == "registry"
167
+ end
168
+
169
+ def dependency_source_details
170
+ sources =
171
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
172
+
173
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
174
+
175
+ sources.first
176
+ end
177
+
178
+ def git_dependency?
179
+ git_commit_checker.git_dependency?
180
+ end
181
+
182
+ def git_commit_checker
183
+ @git_commit_checker ||=
184
+ GitCommitChecker.new(
185
+ dependency: dependency,
186
+ credentials: credentials,
187
+ ignored_versions: ignored_versions,
188
+ requirement_class: Requirement,
189
+ version_class: Version
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ Dependabot::UpdateCheckers.
197
+ register("terraform", Dependabot::Terraform::UpdateChecker)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Terraform pre-release versions use 1.0.1-rc1 syntax, which Gem::Version
4
+ # converts into 1.0.1.pre.rc1. We override the `to_s` method to stop that
5
+ # alteration.
6
+ #
7
+ # See, for example, https://releases.hashicorp.com/terraform/
8
+
9
+ module Dependabot
10
+ module Terraform
11
+ class Version < Gem::Version
12
+ def initialize(version)
13
+ @version_string = version.to_s
14
+ super
15
+ end
16
+
17
+ def to_s
18
+ @version_string
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ Dependabot::Utils.
25
+ register_version_class("terraform", Dependabot::Terraform::Version)
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dependabot-terraform
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.76.1
5
+ platform: ruby
6
+ authors:
7
+ - Dependabot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dependabot-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.76.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.76.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec_junit_formatter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.61'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.61'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '4.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.4'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.4'
139
+ description: Automated dependency management for Ruby, JavaScript, Python, PHP, Elixir,
140
+ Rust, Java, .NET, Elm and Go
141
+ email: support@dependabot.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - helpers/build
147
+ - lib/dependabot/terraform.rb
148
+ - lib/dependabot/terraform/file_fetcher.rb
149
+ - lib/dependabot/terraform/file_parser.rb
150
+ - lib/dependabot/terraform/file_updater.rb
151
+ - lib/dependabot/terraform/metadata_finder.rb
152
+ - lib/dependabot/terraform/requirement.rb
153
+ - lib/dependabot/terraform/requirements_updater.rb
154
+ - lib/dependabot/terraform/update_checker.rb
155
+ - lib/dependabot/terraform/version.rb
156
+ homepage: https://github.com/dependabot/dependabot-core
157
+ licenses:
158
+ - Nonstandard
159
+ metadata: {}
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: 2.5.0
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: 2.5.0
174
+ requirements: []
175
+ rubyforge_project:
176
+ rubygems_version: 2.7.6
177
+ signing_key:
178
+ specification_version: 4
179
+ summary: Terraform support for dependabot-core
180
+ test_files: []