dependabot-terraform 0.76.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []