Class: Dependabot::Terraform::RegistryClient

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/dependabot/terraform/registry_client.rb

Overview

Terraform::RegistryClient is a basic API client to interact with a terraform registry: www.terraform.io/docs/registry/api.html

Constant Summary collapse

ARCHIVE_EXTENSIONS =

Archive extensions supported by Terraform for HTTP URLs developer.hashicorp.com/terraform/language/modules/sources#http-urls

T.let(
  %w(.zip .bz2 .tar.bz2 .tar.tbz2 .tbz2 .gz .tar.gz .tgz .xz .tar.xz .txz).freeze,
  T::Array[String]
)
PUBLIC_HOSTNAME =
"registry.terraform.io"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hostname: PUBLIC_HOSTNAME, credentials: []) ⇒ RegistryClient



26
27
28
29
30
31
32
33
34
# File 'lib/dependabot/terraform/registry_client.rb', line 26

def initialize(hostname: PUBLIC_HOSTNAME, credentials: [])
  @hostname = hostname
  @tokens = T.let(
    credentials.each_with_object({}) do |item, memo|
      memo[item["host"]] = item["token"] if item["type"] == "terraform_registry"
    end,
    T::Hash[String, String]
  )
end

Class Method Details

.get_proxied_source(raw_source) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/dependabot/terraform/registry_client.rb', line 42

def self.get_proxied_source(raw_source)
  return raw_source unless raw_source.start_with?("http")

  uri = URI.parse(T.must(raw_source.split(%r{(?<!:)//}).first))
  return raw_source if ARCHIVE_EXTENSIONS.any? { |ext| uri.path&.end_with?(ext) }
  return raw_source if URI.parse(raw_source).query&.include?("archive=")

  url = T.must(raw_source.split(%r{(?<!:)//}).first) + "?terraform-get=1"
  host = URI.parse(raw_source).host

  response = Dependabot::RegistryClient.get(url: url)
  raise PrivateSourceAuthenticationFailure, host if response.status == 401

  return T.must(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

Instance Method Details

#all_module_versions(identifier:) ⇒ Object



97
98
99
100
101
102
103
104
# File 'lib/dependabot/terraform/registry_client.rb', line 97

def all_module_versions(identifier:)
  base_url = service_url_for("modules.v1")
  response = http_get!(URI.join(base_url, "#{identifier}/versions"))

  JSON.parse(response.body)
      .fetch("modules").first.fetch("versions")
      .map { |release| version_class.new(release.fetch("version")) }
end

#all_provider_versions(identifier:) ⇒ Object



78
79
80
81
82
83
84
85
86
87
# File 'lib/dependabot/terraform/registry_client.rb', line 78

def all_provider_versions(identifier:)
  base_url = service_url_for("providers.v1")
  response = http_get!(URI.join(base_url, "#{identifier}/versions"))

  JSON.parse(response.body)
      .fetch("versions")
      .map { |release| version_class.new(release.fetch("version")) }
rescue Excon::Error
  raise error("Could not fetch provider versions")
end

#service_url_for(service_key) ⇒ Object



149
150
151
152
153
# File 'lib/dependabot/terraform/registry_client.rb', line 149

def service_url_for(service_key)
  url_for(services.fetch(service_key))
rescue KeyError
  raise Dependabot::PrivateSourceAuthenticationFailure, "Host does not support required Terraform-native service"
end

#source(dependency:) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/dependabot/terraform/registry_client.rb', line 115

def source(dependency:)
  type = T.must(dependency.requirements.first)[:source][:type]
  base_url = service_url_for(service_key_for(type))
  case type
  # https://www.terraform.io/internals/module-registry-protocol#download-source-code-for-a-specific-module-version
  when "module", "modules", "registry"
    download_url = URI.join(base_url, "#{dependency.name}/#{dependency.version}/download")
    response = http_get(download_url)
    return nil unless response.status == 204

    source_url = response.headers.fetch("X-Terraform-Get")
    source_url = URI.join(download_url, source_url) if
      source_url.start_with?("/", "./", "../")
    source_url = RegistryClient.get_proxied_source(source_url) if source_url
  when "provider", "providers"
    response = http_get(URI.join(base_url, "#{dependency.name}/#{dependency.version}"))
    return nil unless response.status == 200

    source_url = JSON.parse(response.body).fetch("source")
  end

  Source.from_url(source_url) if source_url
rescue JSON::ParserError, Excon::Error::Timeout
  nil
end