class DockerRegistry2::Registry

Public Class Methods

new(uri, options = {}) click to toggle source

@param [#to_s] base_uri Docker registry base URI @param [Hash] options Client options @option options [#to_s] :user User name for basic authentication @option options [#to_s] :password Password for basic authentication @option options [#to_s] :open_timeout Time to wait for a connection with a registry.

It is ignored if http_options[:open_timeout] is also specified.

@option options [#to_s] :read_timeout Time to wait for data from a registry.

It is ignored if http_options[:read_timeout] is also specified.

@option options [Hash] :http_options Extra options for RestClient::Request.execute.

# File lib/registry/registry.rb, line 18
def initialize(uri, options = {})
  @uri = URI.parse(uri)
  @base_uri = +"#{@uri.scheme}://#{@uri.host}:#{@uri.port}#{@uri.path}"
  # `URI.join("https://example.com/foo/bar", "v2")` drops `bar` in the base URL. A trailing slash prevents that.
  @base_uri << '/' unless @base_uri.end_with? '/'
  @user = options[:user]
  @password = options[:password]
  @http_options = options[:http_options] || {}
  @http_options[:open_timeout] ||= options[:open_timeout] || 2
  @http_options[:read_timeout] ||= options[:read_timeout] || 5
end

Public Instance Methods

_pull_v1(repo, manifest, dir) click to toggle source
# File lib/registry/registry.rb, line 205
def _pull_v1(repo, manifest, dir)
  # make sure the directory exists
  FileUtils.mkdir_p dir
  return false unless manifest['schemaVersion'] == 1

  # pull each of the layers
  manifest['fsLayers'].each do |layer|
    # define path of file to save layer in
    layer_file = "#{dir}/#{layer['blobSum']}"
    # skip layer if we already got it
    next if File.file? layer_file

    # download layer
    # puts "getting layer (v1) #{layer['blobSum']}"
    blob(repo, layer['blobSum'], layer_file)
    # return layer file
  end
end
_pull_v2(repo, manifest, dir) click to toggle source
# File lib/registry/registry.rb, line 187
def _pull_v2(repo, manifest, dir)
  # make sure the directory exists
  FileUtils.mkdir_p dir
  return false unless manifest['schemaVersion'] == 2

  # pull each of the layers
  manifest['layers'].each do |layer|
    # define path of file to save layer in
    layer_file = "#{dir}/#{layer['digest']}"
    # skip layer if we already got it
    next if File.file? layer_file

    # download layer
    # puts "getting layer (v2) #{layer['digest']}"
    blob(repo, layer['digest'], layer_file)
  end
end
blob(repo, digest, outpath = nil) click to toggle source
# File lib/registry/registry.rb, line 119
def blob(repo, digest, outpath = nil)
  blob_url = "v2/#{repo}/blobs/#{digest}"
  if outpath.nil?
    response = doget(blob_url)
    DockerRegistry2::Blob.new(response.headers, response.body)
  else
    File.open(outpath, 'w') do |fd|
      doreq('get', blob_url, fd)
    end

    outpath
  end
end
blob_size(repo, blobSum) click to toggle source

gets the size of a particular blob, given the repo and the content-addressable hash usually unneeded, since manifest includes it

# File lib/registry/registry.rb, line 238
def blob_size(repo, blobSum)
  response = dohead "v2/#{repo}/blobs/#{blobSum}"
  Integer(response.headers[:content_length], 10)
end
copy(repo, tag, newregistry, newrepo, newtag) click to toggle source
# File lib/registry/registry.rb, line 234
def copy(repo, tag, newregistry, newrepo, newtag); end
digest(image, tag, architecture = nil, os = nil, variant = nil) click to toggle source
# File lib/registry/registry.rb, line 141
def digest(image, tag, architecture = nil, os = nil, variant = nil)
  manifest = manifest(image, tag)
  parsed_manifest = JSON.parse(manifest.body)

  # Multi-arch images
  if parsed_manifest.key?('manifests')
    manifests = parsed_manifest['manifests']

    return manifests if architecture.nil? || os.nil?

    manifests.each do |entry|
      if !variant.nil?
        return entry['digest'] if entry['platform']['architecture'] == architecture && entry['platform']['os'] == os && entry['platform']['variant'] == variant
      elsif entry['platform']['architecture'] == architecture && entry['platform']['os'] == os
        return entry['digest']
      end
    end

    raise DockerRegistry2::NotFound, "No matches found for the image=#{image} tag=#{tag} os=#{os} architecture=#{architecture}"

  end

  manifest.headers[:docker_content_digest]
end
dodelete(url) click to toggle source
# File lib/registry/registry.rb, line 38
def dodelete(url)
  doreq 'delete', url
end
doget(url) click to toggle source
# File lib/registry/registry.rb, line 30
def doget(url)
  doreq 'get', url
end
dohead(url) click to toggle source
# File lib/registry/registry.rb, line 42
def dohead(url)
  doreq 'head', url
end
doput(url, payload = nil) click to toggle source
# File lib/registry/registry.rb, line 34
def doput(url, payload = nil)
  doreq 'put', url, nil, payload
end
last(header) click to toggle source
# File lib/registry/registry.rb, line 260
def last(header)
  links = parse_link_header(header)
  if links[:next]
    query = URI(links[:next]).query
    last = URI.decode_www_form(query).to_h['last']
  end
  last
end
manifest(repo, tag) click to toggle source
# File lib/registry/registry.rb, line 109
def manifest(repo, tag)
  # first get the manifest
  response = doget "v2/#{repo}/manifests/#{tag}"
  parsed = JSON.parse response.body
  manifest = DockerRegistry2::Manifest[parsed]
  manifest.body = response.body
  manifest.headers = response.headers
  manifest
end
manifest_digest(repo, tag) click to toggle source
# File lib/registry/registry.rb, line 133
def manifest_digest(repo, tag)
  tag_path = "v2/#{repo}/manifests/#{tag}"
  dohead(tag_path).headers[:docker_content_digest]
rescue DockerRegistry2::InvalidMethod
  # Pre-2.3.0 registries didn't support manifest HEAD requests
  doget(tag_path).headers[:docker_content_digest]
end
manifest_sum(manifest) click to toggle source
# File lib/registry/registry.rb, line 269
def manifest_sum(manifest)
  size = 0
  manifest['layers'].each do |layer|
    size += layer['size']
  end
  size
end
paginate_doget(url) { |response| ... } click to toggle source

When a result set is too large, the Docker registry returns only the first items and adds a Link header in the response with the URL of the next page. See <docs.docker.com/registry/spec/api/#pagination>. This method iterates over the pages and calls the given block with each response.

# File lib/registry/registry.rb, line 49
def paginate_doget(url)
  loop do
    response = doget(url)
    yield response

    link_header = response.headers[:link] or break
    next_url = parse_link_header(link_header)[:next] or break

    # The next URL in the Link header may be relative to the request URL, or absolute.
    # URI.join handles both cases nicely.
    url = URI.join(response.request.url, next_url)
  end
end
pull(repo, tag, dir) click to toggle source
# File lib/registry/registry.rb, line 173
def pull(repo, tag, dir)
  # make sure the directory exists
  FileUtils.mkdir_p dir
  # get the manifest
  m = manifest repo, tag
  # puts "pulling #{repo}:#{tag} into #{dir}"
  # manifest can contain multiple manifests one for each API version
  downloaded_layers = []
  downloaded_layers += _pull_v2(repo, m, dir) if m['schemaVersion'] == 2
  downloaded_layers += _pull_v1(repo, m, dir) if m['schemaVersion'] == 1
  # return downloaded_layers
  downloaded_layers
end
push(manifest, dir) click to toggle source
# File lib/registry/registry.rb, line 224
def push(manifest, dir); end
rmtag(image, tag) click to toggle source
# File lib/registry/registry.rb, line 166
def rmtag(image, tag)
  # TODO: Need full response back. Rewrite other manifests() calls without JSON?
  reference = doget("v2/#{image}/manifests/#{tag}").headers[:docker_content_digest]

  dodelete("v2/#{image}/manifests/#{reference}").code
end
tag(repo, tag, newrepo, newtag) click to toggle source
# File lib/registry/registry.rb, line 226
def tag(repo, tag, newrepo, newtag)
  manifest = manifest(repo, tag)

  raise DockerRegistry2::RegistryVersionException unless manifest['schemaVersion'] == 2

  doput "v2/#{newrepo}/manifests/#{newtag}", manifest.to_json
end
tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false) click to toggle source
# File lib/registry/registry.rb, line 73
def tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false)
  # create query params
  params = []
  params.push(['last', last]) if last && last != ''
  params.push(['n', count]) unless count.nil?

  query_vars = ''
  query_vars = "?#{URI.encode_www_form(params)}" if params.length.positive?

  response = doget "v2/#{repo}/tags/list#{query_vars}"
  # parse the response
  resp = JSON.parse response
  # parse out next page link if necessary
  resp['last'] = last(response.headers[:link]) if response.headers[:link]

  # do we include the hashes?
  if withHashes
    resp['hashes'] = {}
    resp['tags'].each do |tag|
      resp['hashes'][tag] = digest(repo, tag)
    end
  end

  return resp unless auto_paginate

  while (last_tag = resp.delete('last'))
    additional_tags = tags(repo, count, last_tag, withHashes)
    resp['last'] = additional_tags['last']
    resp['tags'] += additional_tags['tags']
    resp['tags'] = resp['tags'].uniq
    resp['hashes'].merge!(additional_tags['hashes']) if withHashes
  end

  resp
end

Private Instance Methods

authenticate_bearer(header) click to toggle source
# File lib/registry/registry.rb, line 380
def authenticate_bearer(header)
  # get the parts we need
  target = split_auth_header(header)
  # did we have a username and password?
  target[:params][:account] = @user if defined? @user && !@user.to_s.strip.empty?
  # authenticate against the realm
  uri = URI.parse(target[:realm])
  begin
    response = RestClient::Request.execute(@http_options.merge(
                                             method: :get,
                                             url: uri.to_s, headers: { params: target[:params] },
                                             user: @user,
                                             password: @password
                                           ))
  rescue RestClient::Unauthorized, RestClient::Forbidden
    # bad authentication
    raise DockerRegistry2::RegistryAuthenticationException
  rescue RestClient::NotFound => e
    raise DockerRegistry2::NotFound, e
  end
  # now save the web token
  result = JSON.parse(response)
  result['token'] || result['access_token']
end
do_basic_req(type, url, stream = nil, payload = nil) click to toggle source
# File lib/registry/registry.rb, line 316
def do_basic_req(type, url, stream = nil, payload = nil)
  begin
    block = if stream.nil?
              nil
            else
              proc { |response|
                response.read_body do |chunk|
                  stream.write chunk
                end
              }
            end
    response = RestClient::Request.execute(@http_options.merge(
                                             method: type,
                                             url: URI.join(@base_uri, url).to_s,
                                             user: @user,
                                             password: @password,
                                             headers: headers(payload: payload),
                                             block_response: block,
                                             payload: payload
                                           ))
  rescue SocketError
    raise DockerRegistry2::RegistryUnknownException
  rescue RestClient::Unauthorized
    raise DockerRegistry2::RegistryAuthenticationException
  rescue RestClient::MethodNotAllowed
    raise DockerRegistry2::InvalidMethod
  rescue RestClient::NotFound => e
    raise DockerRegistry2::NotFound, e
  end
  response
end
do_bearer_req(type, url, header, stream = false, payload = nil) click to toggle source
# File lib/registry/registry.rb, line 348
def do_bearer_req(type, url, header, stream = false, payload = nil)
  token = authenticate_bearer(header)
  begin
    block = if stream.nil?
              nil
            else
              proc { |response|
                response.read_body do |chunk|
                  stream.write chunk
                end
              }
            end
    response = RestClient::Request.execute(@http_options.merge(
                                             method: type,
                                             url: URI.join(@base_uri, url).to_s,
                                             headers: headers(payload: payload, bearer_token: token),
                                             block_response: block,
                                             payload: payload
                                           ))
  rescue SocketError
    raise DockerRegistry2::RegistryUnknownException
  rescue RestClient::Unauthorized
    raise DockerRegistry2::RegistryAuthenticationException
  rescue RestClient::MethodNotAllowed
    raise DockerRegistry2::InvalidMethod
  rescue RestClient::NotFound => e
    raise DockerRegistry2::NotFound, e
  end

  response
end
doreq(type, url, stream = nil, payload = nil) click to toggle source
# File lib/registry/registry.rb, line 279
def doreq(type, url, stream = nil, payload = nil)
  begin
    block = if stream.nil?
              nil
            else
              proc { |response|
                response.read_body do |chunk|
                  stream.write chunk
                end
              }
            end
    response = RestClient::Request.execute(@http_options.merge(
                                             method: type,
                                             url: URI.join(@base_uri, url).to_s,
                                             headers: headers(payload: payload),
                                             block_response: block,
                                             payload: payload
                                           ))
  rescue SocketError
    raise DockerRegistry2::RegistryUnknownException
  rescue RestClient::NotFound
    raise DockerRegistry2::NotFound, "Image not found at #{@uri.host}"
  rescue RestClient::Unauthorized => e
    header = e.response.headers[:www_authenticate]
    method = header.to_s.downcase.split[0]
    case method
    when 'basic'
      response = do_basic_req(type, url, stream, payload)
    when 'bearer'
      response = do_bearer_req(type, url, header, stream, payload)
    else
      raise DockerRegistry2::RegistryUnknownException
    end
  end
  response
end
headers(payload: nil, bearer_token: nil) click to toggle source
# File lib/registry/registry.rb, line 419
def headers(payload: nil, bearer_token: nil)
  headers = {}
  headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.nil?
  if payload.nil?
    headers['Accept'] =
      %w[application/vnd.docker.distribution.manifest.v2+json
         application/vnd.docker.distribution.manifest.list.v2+json
         application/vnd.oci.image.manifest.v1+json
         application/vnd.oci.image.index.v1+json
         application/json].join(',')
  end
  headers['Content-Type'] = 'application/vnd.docker.distribution.manifest.v2+json' unless payload.nil?

  headers
end
split_auth_header(header = '') click to toggle source
# File lib/registry/registry.rb, line 405
def split_auth_header(header = '')
  h = {}
  h = { params: {} }
  header.scan(/(\w+)="([^"]+)"/) do |entry|
    case entry[0]
    when 'realm'
      h[:realm] = entry[1]
    else
      h[:params][entry[0]] = entry[1]
    end
  end
  h
end