Skip to content

Commit

Permalink
Strict type Dependabot::Terraform::FileUpdater
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Sep 23, 2024
1 parent f5645f7 commit 2ff86f1
Showing 1 changed file with 60 additions and 17 deletions.
77 changes: 60 additions & 17 deletions terraform/lib/dependabot/terraform/file_updater.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"
Expand All @@ -20,10 +20,12 @@ class FileUpdater < Dependabot::FileUpdaters::Base
MODULE_NOT_INSTALLED_ERROR = /Module not installed.*module\s*\"(?<mod>\S+)\"/m
GIT_HTTPS_PREFIX = %r{^git::https://}

sig { override.returns(T::Array[Regexp]) }
def self.updated_files_regex
[/\.tf$/, /\.hcl$/]
end

sig { override.returns(T::Array[Dependabot::DependencyFile]) }
def updated_dependency_files
updated_files = []

Expand Down Expand Up @@ -69,23 +71,25 @@ def updated_dependency_files
# (requirements - previous_requirements) | (previous_requirements - requirements)
# => [{requirement: "0.9.1"}]
# we can detect that change.
sig { params(file: Dependabot::DependencyFile, dependency: Dependabot::Dependency).returns(T::Boolean) }
def requirement_changed?(file, dependency)
changed_requirements =
(dependency.requirements - dependency.previous_requirements) |
(dependency.previous_requirements - dependency.requirements)
(dependency.requirements - T.must(dependency.previous_requirements)) |
(T.must(dependency.previous_requirements) - dependency.requirements)

changed_requirements.any? { |f| f[:file] == file.name }
end

sig { params(file: Dependabot::DependencyFile).returns(String) }
def updated_terraform_file_content(file)
content = file.content.dup
content = T.must(file.content.dup)

reqs = dependency.requirements.zip(dependency.previous_requirements)
reqs = dependency.requirements.zip(T.must(dependency.previous_requirements))
.reject { |new_req, old_req| new_req == old_req }

# Loop through each changed requirement and update the files and lockfile
reqs.each do |new_req, old_req|
raise "Bad req match" unless new_req[:file] == old_req[:file]
raise "Bad req match" unless new_req[:file] == old_req&.fetch(:file)
next unless new_req.fetch(:file) == file.name

case new_req[:source][:type]
Expand All @@ -102,20 +106,37 @@ def updated_terraform_file_content(file)
content
end

sig do
params(
new_req: T::Hash[Symbol, T.untyped],
old_req: T.nilable(T::Hash[Symbol, T.untyped]),
updated_content: String,
filename: String
)
.void
end
def update_git_declaration(new_req, old_req, updated_content, filename)
url = old_req.fetch(:source)[:url].gsub(%r{^https://}, "")
tag = old_req.fetch(:source)[:ref]
url = old_req&.dig(:source, :url)&.gsub(%r{^https://}, "")
tag = old_req&.dig(:source, :ref)
url_regex = /#{Regexp.quote(url)}.*ref=#{Regexp.quote(tag)}/

declaration_regex = git_declaration_regex(filename)

updated_content.sub!(declaration_regex) do |regex_match|
regex_match.sub(url_regex) do |url_match|
url_match.sub(old_req[:source][:ref], new_req[:source][:ref])
url_match.sub(old_req&.dig(:source, :ref), new_req[:source][:ref])
end
end
end

sig do
params(
new_req: T::Hash[Symbol, T.untyped],
old_req: T.nilable(T::Hash[Symbol, T.untyped]),
updated_content: String
)
.void
end
def update_registry_declaration(new_req, old_req, updated_content)
regex = if new_req[:source][:type] == "provider"
provider_declaration_regex(updated_content)
Expand All @@ -124,18 +145,20 @@ def update_registry_declaration(new_req, old_req, updated_content)
end
updated_content.gsub!(regex) do |regex_match|
regex_match.sub(/^\s*version\s*=.*/) do |req_line_match|
req_line_match.sub(old_req[:requirement], new_req[:requirement])
req_line_match.sub(old_req&.fetch(:requirement), new_req[:requirement])
end
end
end

sig { params(content: String, declaration_regex: Regexp).returns(T::Array[String]) }
def extract_provider_h1_hashes(content, declaration_regex)
content.match(declaration_regex).to_s
.match(hashes_object_regex).to_s
.split("\n").map { |hash| hash.match(hashes_string_regex).to_s }
.select { |h| h&.match?(/^h1:/) }
.select { |h| h.match?(/^h1:/) }
end

sig { params(content: String, declaration_regex: Regexp).returns(String) }
def remove_provider_h1_hashes(content, declaration_regex)
content.match(declaration_regex).to_s
.sub(hashes_object_regex, "")
Expand All @@ -155,8 +178,9 @@ def lockfile_details(new_req)
[T.must(content), provider_source, declaration_regex]
end

sig { returns(T.nilable(T::Array[Symbol])) }
def lookup_hash_architecture # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
new_req = dependency.requirements.first
new_req = T.must(dependency.requirements.first)

# NOTE: Only providers are included in the lockfile, modules are not
return unless new_req[:source][:type] == "provider"
Expand Down Expand Up @@ -222,14 +246,23 @@ def lookup_hash_architecture # rubocop:disable Metrics/AbcSize, Metrics/MethodLe
architectures.to_a
end

sig { returns(T::Array[Symbol]) }
def architecture_type
@architecture_type ||= lookup_hash_architecture.empty? ? [:linux_amd64] : lookup_hash_architecture
@architecture_type ||= T.let(
if lookup_hash_architecture.nil? || lookup_hash_architecture&.empty?
[:linux_amd64]
else
T.must(lookup_hash_architecture)
end,
T.nilable(T::Array[Symbol])
)
end

sig { params(updated_manifest_files: T::Array[Dependabot::DependencyFile]).returns(T.nilable(String)) }
def update_lockfile_declaration(updated_manifest_files) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
return if lockfile.nil?

new_req = dependency.requirements.first
new_req = T.must(dependency.requirements.first)
# NOTE: Only providers are included in the lockfile, modules are not
return unless new_req[:source][:type] == "provider"

Expand Down Expand Up @@ -268,14 +301,15 @@ def update_lockfile_declaration(updated_manifest_files) # rubocop:disable Metric
raise if @retrying_lock || !e.message.include?("terraform init")

# NOTE: Modules need to be installed before terraform can update the lockfile
@retrying_lock = true
@retrying_lock = T.let(true, T.nilable(T::Boolean))
run_terraform_init
retry
end

content
end

sig { void }
def run_terraform_init
SharedHelpers.with_git_configured(credentials: credentials) do
# -backend=false option used to ignore any backend configuration, as these won't be accessible
Expand All @@ -298,30 +332,36 @@ def run_terraform_init
end
end

sig { returns(Dependabot::Dependency) }
def dependency
# Terraform updates will only ever be updating a single dependency
dependencies.first
T.must(dependencies.first)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def files_with_requirement
filenames = dependency.requirements.map { |r| r[:file] }
dependency_files.select { |file| filenames.include?(file.name) }
end

sig { override.void }
def check_required_files
return if [*terraform_files, *terragrunt_files].any?

raise "No Terraform configuration file!"
end

sig { returns(Regexp) }
def hashes_object_regex
/hashes\s*=\s*[^\]]*\]/m
end

sig { returns(Regexp) }
def hashes_string_regex
/(?<=\").*(?=\")/
end

sig { params(updated_content: String).returns(Regexp) }
def provider_declaration_regex(updated_content)
name = Regexp.escape(dependency.name)
registry_host = Regexp.escape(registry_host_for(dependency))
Expand All @@ -341,6 +381,7 @@ def provider_declaration_regex(updated_content)
end
end

sig { returns(Regexp) }
def registry_declaration_regex
%r{
(?<=\{)
Expand All @@ -354,20 +395,22 @@ def registry_declaration_regex
}mx
end

sig { params(filename: String).returns(Regexp) }
def git_declaration_regex(filename)
# For terragrunt dependencies there's not a lot we can base the
# regex on. Just look for declarations within a `terraform` block
return /terraform\s*\{(?:(?!^\}).)*/m if terragrunt_file?(filename)

# For modules we can do better - filter for module blocks that use the
# name of the module
module_name = dependency.name.split("::").first
module_name = T.must(dependency.name.split("::").first)
/
module\s+["']#{Regexp.escape(module_name)}["']\s*\{
(?:(?!^\}).)*
/mx
end

sig { params(dependency: Dependabot::Dependency).returns(String) }
def registry_host_for(dependency)
source = dependency.requirements.filter_map { |r| r[:source] }.first
source[:registry_hostname] || source["registry_hostname"] || "registry.terraform.io"
Expand Down

0 comments on commit 2ff86f1

Please sign in to comment.