class Dependabot::Terraform::FileUpdater

Constants

PRIVATE_MODULE_ERROR

Public Class Methods

updated_files_regex() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 16
def self.updated_files_regex
  [/\.tf$/, /\.hcl$/]
end

Public Instance Methods

updated_dependency_files() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 20
def updated_dependency_files
  updated_files = []

  [*terraform_files, *terragrunt_files].each do |file|
    next unless file_changed?(file)

    updated_content = updated_terraform_file_content(file)

    raise "Content didn't change!" if updated_content == file.content

    updated_files << updated_file(file: file, content: updated_content)
  end
  updated_lockfile_content = update_lockfile_declaration(updated_files)

  if updated_lockfile_content && lock_file.content != updated_lockfile_content
    updated_files << updated_file(file: lock_file, content: updated_lockfile_content)
  end

  updated_files.compact!

  raise "No files changed!" if updated_files.none?

  updated_files
end

Private Instance Methods

check_required_files() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 162
def check_required_files
  return if [*terraform_files, *terragrunt_files].any?

  raise "No Terraform configuration file!"
end
dependency() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 152
def dependency
  # Terraform updates will only ever be updating a single dependency
  dependencies.first
end
files_with_requirement() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 157
def files_with_requirement
  filenames = dependency.requirements.map { |r| r[:file] }
  dependency_files.select { |file| filenames.include?(file.name) }
end
git_declaration_regex(filename) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 189
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 dependency
  /
    module\s+["']#{Regexp.escape(dependency.name)}["']\s*\{
    (?:(?!^\}).)*
  /mx
end
lockfile_declaration_regex(provider_source) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 207
def lockfile_declaration_regex(provider_source)
  /
    (?:(?!^\}).)*
    provider\s*["']#{Regexp.escape(provider_source)}["']\s*\{
    (?:(?!^\}).)*}
  /mix
end
provider_declaration_regex() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 168
def provider_declaration_regex
  name = Regexp.escape(dependency.name)
  %r{
    ((source\s*=\s*["'](#{Regexp.escape(registry_host_for(dependency))}/)?#{name}["']|\s*#{name}\s*=\s*\{.*)
    (?:(?!^\}).)+)
  }mx
end
registry_declaration_regex() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 176
def registry_declaration_regex
  %r{
    (?<=\{)
    (?:(?!^\}).)*
    source\s*=\s*["']
      (#{Regexp.escape(registry_host_for(dependency))}/)?
      #{Regexp.escape(dependency.name)}
      (//modules/\S+)?
      ["']
    (?:(?!^\}).)*
  }mx
end
registry_host_for(dependency) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 202
def registry_host_for(dependency)
  source = dependency.requirements.map { |r| r[:source] }.compact.first
  source[:registry_hostname] || source["registry_hostname"] || "registry.terraform.io"
end
run_terraform_init() click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 137
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
    # -input=false option used to immediately fail if it needs user input
    # -no-color option used to prevent any color characters being printed in the output
    SharedHelpers.run_shell_command("terraform init -backend=false -input=false -no-color")
  rescue SharedHelpers::HelperSubprocessFailed => e
    output = e.message

    if output.match?(PRIVATE_MODULE_ERROR)
      raise PrivateSourceAuthenticationFailure, output.match(PRIVATE_MODULE_ERROR).named_captures.fetch("repo")
    end
  end
end
update_git_declaration(new_req, old_req, updated_content, filename) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 72
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_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])
    end
  end
end
update_lockfile_declaration(updated_manifest_files) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 95
def update_lockfile_declaration(updated_manifest_files) # rubocop:disable Metrics/AbcSize
  return if lock_file.nil?

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

  content = lock_file.content.dup
  provider_source = new_req[:source][:registry_hostname] + "/" + new_req[:source][:module_identifier]
  declaration_regex = lockfile_declaration_regex(provider_source)
  lockfile_dependency_removed = content.sub(declaration_regex, "")

  base_dir = dependency_files.first.directory
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
    # Update the provider requirements in case the previous requirement doesn't allow the new version
    updated_manifest_files.each { |f| File.write(f.name, f.content) }

    File.write(".terraform.lock.hcl", lockfile_dependency_removed)
    SharedHelpers.run_shell_command("terraform providers lock #{provider_source}")

    updated_lockfile = File.read(".terraform.lock.hcl")
    updated_dependency = updated_lockfile.scan(declaration_regex).first

    # Terraform will occasionally update h1 hashes without updating the version of the dependency
    # Here we make sure the dependency's version actually changes in the lockfile
    unless updated_dependency.scan(declaration_regex).first.scan(/^\s*version\s*=.*/) ==
           content.scan(declaration_regex).first.scan(/^\s*version\s*=.*/)
      content.sub!(declaration_regex, updated_dependency)
    end
  rescue SharedHelpers::HelperSubprocessFailed => e
    raise if @retrying_lock || !e.message.include?("terraform init")

    # NOTE: Modules need to be installed before terraform can update the
    # lockfile
    @retrying_lock = true
    run_terraform_init
    retry
  end

  content
end
update_registry_declaration(new_req, old_req, updated_content) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 86
def update_registry_declaration(new_req, old_req, updated_content)
  regex = new_req[:source][:type] == "provider" ? provider_declaration_regex : registry_declaration_regex
  updated_content.sub!(regex) do |regex_match|
    regex_match.sub(/^\s*version\s*=.*/) do |req_line_match|
      req_line_match.sub(old_req[:requirement], new_req[:requirement])
    end
  end
end
updated_terraform_file_content(file) click to toggle source
# File lib/dependabot/terraform/file_updater.rb, line 47
def updated_terraform_file_content(file)
  content = file.content.dup

  reqs = dependency.requirements.zip(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]
    next unless new_req.fetch(:file) == file.name

    case new_req[:source][:type]
    when "git"
      update_git_declaration(new_req, old_req, content, file.name)
    when "registry", "provider"
      update_registry_declaration(new_req, old_req, content)
    else
      raise "Don't know how to update a #{new_req[:source][:type]} "\
            "declaration!"
    end
  end

  content
end