Class: Dependabot::Terraform::RegistryClient

Inherits:
Object
  • Object
show all
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 =
%w(.zip .tbz2 .tgz .txz).freeze
PUBLIC_HOSTNAME =
"registry.terraform.io"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of RegistryClient.



18
19
20
21
22
23
# File 'lib/dependabot/terraform/registry_client.rb', line 18

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

Class Method Details

.get_proxied_source(raw_source) ⇒ Object

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 rubocop:disable Metrics/AbcSize rubocop:disable Metrics/CyclomaticComplexity



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/dependabot/terraform/registry_client.rb', line 30

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

  uri = URI.parse(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 = 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 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:) ⇒ Array<Dependabot::Terraform::Version>

Fetch all the versions of a module, and return a Version representation of them.

“hashicorp/consul/aws”

Parameters:

  • identifier (String)

    the identifier for the dependency, i.e:

Returns:

Raises:

  • (Dependabot::DependabotError)

    when the versions cannot be retrieved



83
84
85
86
87
88
89
90
# File 'lib/dependabot/terraform/registry_client.rb', line 83

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:) ⇒ Array<Dependabot::Terraform::Version>

Fetch all the versions of a provider, and return a Version representation of them.

“hashicorp/aws”

Parameters:

  • identifier (String)

    the identifier for the dependency, i.e:

Returns:

Raises:

  • (Dependabot::DependabotError)

    when the versions cannot be retrieved



65
66
67
68
69
70
71
72
73
74
# File 'lib/dependabot/terraform/registry_client.rb', line 65

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

Perform service discovery and return the absolute URL for the requested service. www.terraform.io/docs/internals/remote-service-discovery.html

Parameters:

Raises:

  • (Dependabot::PrivateSourceAuthenticationFailure)

    when the service is not available



133
134
135
136
137
# File 'lib/dependabot/terraform/registry_client.rb', line 133

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:) ⇒ nil, Dependabot::Source

Fetch the “source” for a module or provider. We use the API to fetch the source for a dependency, this typically points to a source code repository, and then instantiate a Dependabot::Source object that we can use to fetch Metadata about a specific version of the dependency.

we’re attempting to find

Parameters:

  • dependency (Dependabot::Dependency)

    the dependency who’s source

Returns:

  • (nil, Dependabot::Source)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/dependabot/terraform/registry_client.rb', line 100

def source(dependency:)
  type = 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