class Dependabot::Terraform::FileParser
Constants
- ARCHIVE_EXTENSIONS
- DEFAULT_NAMESPACE
- DEFAULT_REGISTRY
- PROVIDER_SOURCE_ADDRESS
www.terraform.io/docs/language/providers/requirements.html#source-addresses
Public Instance Methods
parse()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 28 def parse dependency_set = DependencySet.new terraform_files.each do |file| modules = parsed_file(file).fetch("module", {}) modules.each do |name, details| dependency_set << build_terraform_dependency(file, name, details) end parsed_file(file).fetch("terraform", []).each do |terraform| required_providers = terraform.fetch("required_providers", {}) required_providers.each do |provider| provider.each do |name, details| dependency_set << build_provider_dependency(file, name, details) end end end end terragrunt_files.each do |file| modules = parsed_file(file).fetch("terraform", []) modules.each do |details| next unless details["source"] dependency_set << build_terragrunt_dependency(file, details) end end dependency_set.dependencies.sort_by(&:name) end
Private Instance Methods
build_provider_dependency(file, name, details = {})
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 89 def build_provider_dependency(file, name, details = {}) deprecated_provider_error(file) if deprecated_provider?(details) source_address = details.fetch("source", nil) version_req = details["version"]&.strip hostname, namespace, name = provider_source_from(source_address, name) dependency_name = source_address ? "#{namespace}/#{name}" : name Dependency.new( name: dependency_name, version: determine_version_for(hostname, namespace, name, version_req), package_manager: "terraform", requirements: [ requirement: version_req, groups: [], file: file.name, source: { type: "provider", registry_hostname: hostname, module_identifier: "#{namespace}/#{name}" } ] ) end
build_terraform_dependency(file, name, details)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 61 def build_terraform_dependency(file, name, details) details = details.first source = source_from(details) dep_name = case source[:type] when "registry" then source[:module_identifier] when "provider" then details["source"] else name end version_req = details["version"]&.strip version = if source[:type] == "git" then version_from_ref(source[:ref]) elsif version_req&.match?(/^\d/) then version_req end Dependency.new( name: dep_name, version: version, package_manager: "terraform", requirements: [ requirement: version_req, groups: [], file: file.name, source: source ] ) end
build_terragrunt_dependency(file, details)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 129 def build_terragrunt_dependency(file, details) source = source_from(details) dep_name = if Source.from_url(source[:url]) Source.from_url(source[:url]).repo else source[:url] end version = version_from_ref(source[:ref]) Dependency.new( name: dep_name, version: version, package_manager: "terraform", requirements: [ requirement: nil, groups: [], file: file.name, source: source ] ) end
check_required_files()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 351 def check_required_files return if [*terraform_files, *terragrunt_files].any? raise "No Terraform configuration file!" end
deprecated_provider?(details)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 123 def deprecated_provider?(details) # The old syntax for terraform providers v0.12- looked like # "tls ~> 2.1" which gets parsed as a string instead of a hash details.is_a?(String) end
deprecated_provider_error(file)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 114 def deprecated_provider_error(file) raise Dependabot::DependencyFileNotParseable.new( file.path, "This terraform provider syntax is now deprecated.\n"\ "See https://www.terraform.io/docs/language/providers/requirements.html "\ "for the new Terraform v0.13+ provider syntax." ) end
determine_version_for(hostname, namespace, name, constraint)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 357 def determine_version_for(hostname, namespace, name, constraint) return constraint if constraint&.match?(/\A\d/) lock_file_content. dig("provider", "#{hostname}/#{namespace}/#{name}", 0, "version") end
get_proxied_source(raw_source)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity See www.terraform.io/docs/modules/sources.html#http-urls for details of how Terraform
handle HTTP(S) sources for modules
# File lib/dependabot/terraform/file_parser.rb, line 234 def get_proxied_source(raw_source) # rubocop:disable Metrics/AbcSize return raw_source unless raw_source.start_with?("http") uri = URI.parse(raw_source.split(%r{(?<!:)//}).first) return raw_source if uri.path.end_with?(*ARCHIVE_EXTENSIONS) return raw_source if URI.parse(raw_source).query&.include?("archive=") url = raw_source.split(%r{(?<!:)//}).first + "?terraform-get=1" host = URI.parse(raw_source).host response = Excon.get( url, idempotent: true, **SharedHelpers.excon_defaults ) raise PrivateSourceAuthenticationFailure, host if response.status == 401 return response.headers["X-Terraform-Get"] if response.headers["X-Terraform-Get"] doc = Nokogiri::XML(response.body) doc.css("meta").find do |tag| tag.attributes&.fetch("name", nil)&.value == "terraform-get" end&.attributes&.fetch("content", nil)&.value rescue Excon::Error::Socket, Excon::Error::Timeout => e raise PrivateSourceAuthenticationFailure, host if e.message.include?("no address for") raw_source end
git_source_details_from(source_string)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 202 def git_source_details_from(source_string) git_url = source_string.strip.gsub(/^git::/, "") git_url = "https://" + git_url unless git_url.start_with?("git@") || git_url.include?("://") bare_uri = if git_url.include?("git@") git_url.split("git@").last.sub(":", "/") else git_url.sub(%r{.*?://}, "") end querystr = URI.parse("https://" + bare_uri).query git_url = git_url.gsub("?#{querystr}", "").split(%r{(?<!:)//}).first { type: "git", url: git_url, branch: nil, ref: CGI.parse(querystr.to_s)["ref"].first&.split(%r{(?<!:)//})&.first } end
lock_file_content()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 364 def lock_file_content @lock_file_content ||= begin lock_file = dependency_files.find do |file| file.name == ".terraform.lock.hcl" end lock_file ? parsed_file(lock_file) : {} end end
native_helpers_root()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 346 def native_helpers_root default_path = File.join(__dir__, "../../../helpers/install-dir") ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path) end
parsed_file(file)
click to toggle source
Returns:¶ ↑
A Hash representing each module found in the specified file
E.g. {
"module" => { { "consul" => [ { "source"=>"consul/aws", "version"=>"0.1.0" } ] } }, "terragrunt"=>[ { "include"=>[{ "path"=>"${find_in_parent_folders()}" }], "terraform"=>[{ "source" => "git::git@github.com:gruntwork-io/modules-example.git//consul?ref=v0.0.2" }] } ],
}
# File lib/dependabot/terraform/file_parser.rb, line 308 def parsed_file(file) @parsed_buildfile ||= {} @parsed_buildfile[file.name] ||= SharedHelpers.in_a_temporary_directory do File.write("tmp.tf", file.content) command = "#{terraform_hcl2_parser_path} < tmp.tf" start = Time.now stdout, stderr, process = Open3.capture3(command) time_taken = Time.now - start unless process.success? raise SharedHelpers::HelperSubprocessFailed.new( message: stderr, error_context: { command: command, time_taken: time_taken, process_exit_value: process.to_s } ) end JSON.parse(stdout) end rescue SharedHelpers::HelperSubprocessFailed => e msg = e.message.strip raise Dependabot::DependencyFileNotParseable.new(file.path, msg) end
provider_source_from(source_address, name)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 172 def provider_source_from(source_address, name) matches = source_address&.match(PROVIDER_SOURCE_ADDRESS) [ matches.try(:[], :hostname) || DEFAULT_REGISTRY, matches.try(:[], :namespace) || DEFAULT_NAMESPACE, matches.try(:[], :name) || name ] end
registry_source_details_from(source_string)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 181 def registry_source_details_from(source_string) parts = source_string.split("//").first.split("/") if parts.count == 3 { type: "registry", registry_hostname: "registry.terraform.io", module_identifier: source_string.split("//").first } elsif parts.count == 4 { type: "registry", registry_hostname: parts.first, module_identifier: parts[1..3].join("/") } else msg = "Invalid registry source specified: '#{source_string}'" raise DependencyFileNotEvaluatable, msg end end
source_from(details_hash)
click to toggle source
Full docs at www.terraform.io/docs/modules/sources.html
# File lib/dependabot/terraform/file_parser.rb, line 154 def source_from(details_hash) raw_source = details_hash.fetch("source") bare_source = get_proxied_source(raw_source) source_details = case source_type(bare_source) when :http_archive, :path, :mercurial, :s3 { type: source_type(bare_source).to_s, url: bare_source } when :github, :bitbucket, :git git_source_details_from(bare_source) when :registry registry_source_details_from(bare_source) end source_details[:proxy_url] = raw_source if raw_source != bare_source source_details end
source_type(source_string)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/terraform/file_parser.rb, line 265 def source_type(source_string) return :path if source_string.start_with?(".") return :github if source_string.include?("github.com") return :bitbucket if source_string.start_with?("bitbucket.org/") return :git if source_string.start_with?("git::") return :mercurial if source_string.start_with?("hg::") return :s3 if source_string.start_with?("s3::") raise "Unknown src: #{source_string}" if source_string.split("/").first.include?("::") return :registry unless source_string.start_with?("http") path_uri = URI.parse(source_string.split(%r{(?<!:)//}).first) query_uri = URI.parse(source_string) return :http_archive if path_uri.path.end_with?(*ARCHIVE_EXTENSIONS) return :http_archive if query_uri.query&.include?("archive=") raise "HTTP source, but not an archive!" end
terraform_hcl2_parser_path()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 341 def terraform_hcl2_parser_path helper_bin_dir = File.join(native_helpers_root, "terraform/bin") Pathname.new(File.join(helper_bin_dir, "hcl2json")).cleanpath.to_path end
terraform_parser_path()
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 336 def terraform_parser_path helper_bin_dir = File.join(native_helpers_root, "terraform/bin") Pathname.new(File.join(helper_bin_dir, "json2hcl")).cleanpath.to_path end
version_from_ref(ref)
click to toggle source
# File lib/dependabot/terraform/file_parser.rb, line 224 def version_from_ref(ref) version_regex = GitCommitChecker::VERSION_REGEX return unless ref&.match?(version_regex) ref.match(version_regex).named_captures.fetch("version") end