Class: Dependabot::Opentofu::RegistryClient

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

Overview

Opentofu::RegistryClient is a basic API client to interact with a OpenTofu registry: api.opentofu.org/

Constant Summary collapse

ARCHIVE_EXTENSIONS =

Archive extensions supported by OpenTofu for HTTP URLs opentofu.org/docs/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.opentofu.org"
API_BASE_URL =
"api.opentofu.org"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of RegistryClient.



27
28
29
30
31
32
33
34
35
36
# File 'lib/dependabot/opentofu/registry_client.rb', line 27

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

Class Method Details

.get_proxied_source(raw_source) ⇒ Object



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

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) + "?opentofu-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-OpenTofu-Get"]) if response.headers["X-OpenTofu-Get"]

  doc = Nokogiri::XML(response.body)
  doc.css("meta").find do |tag|
    tag.attributes&.fetch("name", nil)&.value == "opentofu-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



99
100
101
102
103
104
105
106
# File 'lib/dependabot/opentofu/registry_client.rb', line 99

def all_module_versions(identifier:)
  base_url = service_url_for_registry("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



80
81
82
83
84
85
86
87
88
89
# File 'lib/dependabot/opentofu/registry_client.rb', line 80

def all_provider_versions(identifier:)
  base_url = service_url_for_registry("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_registry(service_key) ⇒ Object



150
151
152
153
154
# File 'lib/dependabot/opentofu/registry_client.rb', line 150

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

#source(dependency:) ⇒ Object



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

def source(dependency:)
  type = T.must(dependency.requirements.first)[:source][:type]
  base_url = url_for_api("/registry/docs/")
  case type
  when "module", "modules", "registry"
    download_url = URI.join(base_url, "modules/#{dependency.name}/#{dependency.version}/download")
    response = http_get(download_url)
    return nil unless response.status == 204

    source_url = response.headers.fetch("X-OpenTofu-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"
    url = URI.join(base_url, "providers/#{dependency.name}/v#{dependency.version}/index.json")
    response = http_get(url)
    return nil unless response.status == 200

    source_url = JSON.parse(response.body).dig("docs", "index", "edit_link")
  end

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