diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24df25a..f238b97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,13 +18,16 @@ jobs: build: name: Build runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['2.7', '3.1'] steps: - name: checkout uses: actions/checkout@v1 - uses: actions/setup-ruby@v1 with: - ruby-version: '2.x' - - name: report gemfile name + ruby-version: ${{ matrix.ruby-version }} + - name: report gemfile name ruby ${{ matrix.ruby-version }} run: make gem - name: test run: make build test BUILD=local diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 0000000..a8fa08f --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,20 @@ +name: Rubocop + +on: [pull_request] + +jobs: + rubocop: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + + + - name: Run rubocop + run: | + bundle install + bundle exec rubocop diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..cc32da4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..ac12175 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,82 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2022-12-02 09:10:59 UTC using RuboCop version 1.39.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Configuration parameters: Include. +# Include: **/*.gemspec +Gemspec/RequiredRubyVersion: + Exclude: + - 'docker_registry2.gemspec' + +# Offense count: 5 +Lint/UselessAssignment: + Exclude: + - 'lib/registry/registry.rb' + - 'test/test.rb' + +# Offense count: 2 +# Configuration parameters: CheckForMethodsWithNoSideEffects. +Lint/Void: + Exclude: + - 'lib/registry/registry.rb' + +# Offense count: 3 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 44 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 312 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +Metrics/CyclomaticComplexity: + Max: 13 + +# Offense count: 6 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +Metrics/MethodLength: + Max: 34 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +Metrics/PerceivedComplexity: + Max: 14 + +# Offense count: 2 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: as, at, by, db, id, if, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: + Exclude: + - 'lib/registry/registry.rb' + +# Offense count: 9 +# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, camelCase +Naming/VariableName: + Exclude: + - 'lib/registry/registry.rb' + +# Offense count: 3 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'lib/docker_registry2.rb' + - 'lib/registry/blob.rb' + - 'lib/registry/registry.rb' + +# Offense count: 2 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/registry/registry.rb' diff --git a/Gemfile b/Gemfile index b4e2a20..7f4f5e9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' gemspec diff --git a/alpine_manifest_test.rb b/alpine_manifest_test.rb index 02807b9..55aaaf0 100644 --- a/alpine_manifest_test.rb +++ b/alpine_manifest_test.rb @@ -1,9 +1,10 @@ -require './lib/docker_registry2.rb' +# frozen_string_literal: true + +require './lib/docker_registry2' reg = DockerRegistry2.connect man = reg.manifest 'library/alpine', 'latest' -puts "MANIFEST" +puts 'MANIFEST' puts man -puts "HEADER" +puts 'HEADER' puts man.headers - diff --git a/docker_registry2.gemspec b/docker_registry2.gemspec index 03707fc..39a7ae5 100644 --- a/docker_registry2.gemspec +++ b/docker_registry2.gemspec @@ -1,5 +1,6 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'registry/version' @@ -7,19 +8,21 @@ Gem::Specification.new do |spec| spec.name = 'docker_registry2' spec.version = DockerRegistry2::VERSION spec.authors = [ - 'Avi Deitcher https://github.com/deitch', - 'Jonathan Hurter https://github.com/johnsudaar', - 'Dmitry Fleytman https://github.com/dmitryfleytman', - 'Grey Baker https://github.com/greysteil' - ] + 'Avi Deitcher https://github.com/deitch', + 'Jonathan Hurter https://github.com/johnsudaar', + 'Dmitry Fleytman https://github.com/dmitryfleytman', + 'Grey Baker https://github.com/greysteil' + ] spec.summary = 'Docker v2 registry HTTP API client' spec.description = 'Docker v2 registry HTTP API client with support for token authentication' spec.homepage = 'https://github.com/deitch/docker_registry2' spec.license = 'MIT' - spec.files = %w{README.md} + Dir.glob("*.gemspec") + Dir.glob("{lib}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } - spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) } - spec.test_files = spec.files.grep(/^(test|spec|features)\//) + spec.files = %w[README.md] + Dir.glob('*.gemspec') + Dir.glob('{lib}/**/*', File::FNM_DOTMATCH).reject do |f| + File.directory?(f) + end + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] spec.add_development_dependency 'bundler' diff --git a/get-gem-name.rb b/get-gem-name.rb index 4c593fa..cf5ce9b 100755 --- a/get-gem-name.rb +++ b/get-gem-name.rb @@ -1,6 +1,7 @@ #!ruby +# frozen_string_literal: true -require "rubygems" +require 'rubygems' -spec = Gem::Specification::load("docker_registry2.gemspec") -puts spec.name.to_s+'-'+spec.version.to_s+'.gem' +spec = Gem::Specification.load('docker_registry2.gemspec') +puts "#{spec.name}-#{spec.version}.gem" diff --git a/lib/docker_registry2.rb b/lib/docker_registry2.rb index 5ea7d17..a7740e5 100644 --- a/lib/docker_registry2.rb +++ b/lib/docker_registry2.rb @@ -1,13 +1,14 @@ -require File.dirname(__FILE__) + '/registry/version' -require File.dirname(__FILE__) + '/registry/registry' -require File.dirname(__FILE__) + '/registry/exceptions' -require File.dirname(__FILE__) + '/registry/manifest' -require File.dirname(__FILE__) + '/registry/blob' +# frozen_string_literal: true +require "#{File.dirname(__FILE__)}/registry/version" +require "#{File.dirname(__FILE__)}/registry/registry" +require "#{File.dirname(__FILE__)}/registry/exceptions" +require "#{File.dirname(__FILE__)}/registry/manifest" +require "#{File.dirname(__FILE__)}/registry/blob" module DockerRegistry2 - def self.connect(uri="https://registry.hub.docker.com",opts={}) - @reg = DockerRegistry2::Registry.new(uri,opts) + def self.connect(uri = 'https://registry.hub.docker.com', opts = {}) + @reg = DockerRegistry2::Registry.new(uri, opts) end def self.search(query = '') @@ -18,7 +19,7 @@ def self.tags(repository) @reg.tags(repository) end - def self.manifest(repository,tag) - @reg.manifest(repository,tag) + def self.manifest(repository, tag) + @reg.manifest(repository, tag) end end diff --git a/lib/registry/blob.rb b/lib/registry/blob.rb index 9b225df..cfbbafc 100644 --- a/lib/registry/blob.rb +++ b/lib/registry/blob.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module DockerRegistry2 class Blob attr_reader :body, :headers - + def initialize(headers, body) @headers = headers @body = body diff --git a/lib/registry/exceptions.rb b/lib/registry/exceptions.rb index 8b002b0..b0de138 100644 --- a/lib/registry/exceptions.rb +++ b/lib/registry/exceptions.rb @@ -1,32 +1,33 @@ +# frozen_string_literal: true + module DockerRegistry2 class Exception < RuntimeError - end - - class RegistryAuthenticationException < Exception + + class RegistryAuthenticationException < StandardError end - class RegistryAuthorizationException < Exception + class RegistryAuthorizationException < StandardError end - class RegistryUnknownException < Exception + class RegistryUnknownException < StandardError end - class RegistrySSLException < Exception + class RegistrySSLException < StandardError end - class RegistryVersionException < Exception + class RegistryVersionException < StandardError end - - class ReauthenticatedException < Exception + + class ReauthenticatedException < StandardError end - - class UnknownRegistryException < Exception + + class UnknownRegistryException < StandardError end - class NotFound < Exception + class NotFound < StandardError end - class InvalidMethod < Exception + class InvalidMethod < StandardError end -end \ No newline at end of file +end diff --git a/lib/registry/manifest.rb b/lib/registry/manifest.rb index 564aa96..e75765f 100644 --- a/lib/registry/manifest.rb +++ b/lib/registry/manifest.rb @@ -1,9 +1,7 @@ -module DockerRegistry2 +# frozen_string_literal: true +module DockerRegistry2 class Manifest < Hash attr_accessor :body, :headers - def initialize - super - end end end diff --git a/lib/registry/registry.rb b/lib/registry/registry.rb index 4017b65..11dc64a 100644 --- a/lib/registry/registry.rb +++ b/lib/registry/registry.rb @@ -1,288 +1,295 @@ +# frozen_string_literal: true + require 'fileutils' require 'rest-client' require 'json' -class DockerRegistry2::Registry - # @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. - def initialize(uri, options = {}) - @uri = URI.parse(uri) - @base_uri = "#{@uri.scheme}://#{@uri.host}:#{@uri.port}" - @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 +module DockerRegistry2 + class Registry + # @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. + def initialize(uri, options = {}) + @uri = URI.parse(uri) + @base_uri = "#{@uri.scheme}://#{@uri.host}:#{@uri.port}" + @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 - def doget(url) - return doreq "get", url - end + def doget(url) + doreq 'get', url + end - def doput(url,payload=nil) - return doreq "put", url, nil, payload - end + def doput(url, payload = nil) + doreq 'put', url, nil, payload + end - def dodelete(url) - return doreq "delete", url - end + def dodelete(url) + doreq 'delete', url + end - def dohead(url) - return doreq "head", url - end + def dohead(url) + doreq 'head', url + end + + # 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 . This method + # iterates over the pages and calls the given block with each response. + def paginate_doget(url) + while url + response = doget(url) + yield response - # 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 . This method - # iterates over the pages and calls the given block with each response. - def paginate_doget(url) - while url - response = doget(url) - yield response + break unless (link = response.headers[:link]) - if (link = response.headers[:link]) url = parse_link_header(link)[:next] - else - break + end end - end - def search(query = '') - all_repos = [] - paginate_doget "/v2/_catalog" do |response| - # parse the response - repos = JSON.parse(response)["repositories"] - if query.strip.length > 0 - re = Regexp.new query - repos = repos.find_all {|e| re =~ e } + def search(query = '') + all_repos = [] + paginate_doget '/v2/_catalog' do |response| + # parse the response + repos = JSON.parse(response)['repositories'] + if query.strip.length.positive? + re = Regexp.new query + repos = repos.find_all { |e| re =~ e } + end + all_repos += repos end - all_repos += repos + all_repos end - all_repos - end - 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 > 0 - - 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 - useGet = false - resp["hashes"] = {} - resp["tags"].each do |tag| - if useGet - head = doget "/v2/#{repo}/manifests/#{tag}" - else - begin - head = dohead "/v2/#{repo}/manifests/#{tag}" - rescue DockerRegistry2::InvalidMethod - # in case we are in a registry pre-2.3.0, which did not support manifest HEAD - useGet = true + 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 + useGet = false + resp['hashes'] = {} + resp['tags'].each do |tag| + if useGet head = doget "/v2/#{repo}/manifests/#{tag}" + else + begin + head = dohead "/v2/#{repo}/manifests/#{tag}" + rescue DockerRegistry2::InvalidMethod + # in case we are in a registry pre-2.3.0, which did not support manifest HEAD + useGet = true + head = doget "/v2/#{repo}/manifests/#{tag}" + end end + resp['hashes'][tag] = head.headers[:docker_content_digest] end - resp["hashes"][tag] = head.headers[:docker_content_digest] end - end - return resp unless auto_paginate + 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 + 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 - resp - end + 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 - 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 + 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 - 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) + outpath end - - outpath end - end - def 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 + def 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 - 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] + 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] - return dodelete("/v2/#{image}/manifests/#{reference}").code - end + dodelete("/v2/#{image}/manifests/#{reference}").code + end - 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 + 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 - 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) - layer_file + 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) + layer_file + end end - end - 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 - layer_file + 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 + layer_file + end end - end - def push(manifest,dir) - end + def push(manifest, dir); end - def tag(repo,tag,newrepo,newtag) - manifest = manifest(repo, tag) + def tag(repo, tag, newrepo, newtag) + manifest = manifest(repo, tag) + + raise DockerRegistry2::RegistryVersionException unless manifest['schemaVersion'] == 2 - if manifest['schemaVersion'] == 2 doput "/v2/#{newrepo}/manifests/#{newtag}", manifest.to_json - else - raise DockerRegistry2::RegistryVersionException end - end - def copy(repo,tag,newregistry,newrepo,newtag) - end + def copy(repo, tag, newregistry, newrepo, newtag); end - # gets the size of a particular blob, given the repo and the content-addressable hash - # usually unneeded, since manifest includes it - def blob_size(repo,blobSum) - response = dohead "/v2/#{repo}/blobs/#{blobSum}" - Integer(response.headers[:content_length],10) - end + # gets the size of a particular blob, given the repo and the content-addressable hash + # usually unneeded, since manifest includes it + def blob_size(repo, blobSum) + response = dohead "/v2/#{repo}/blobs/#{blobSum}" + Integer(response.headers[:content_length], 10) + end + + # Parse the value of the Link HTTP header and return a Hash whose keys are the rel values turned into symbols, and + # the values are URLs. For example, `{ next: '/v2/_catalog?n=100&last=x' }`. + def parse_link_header(header) + last = '' + parts = header.split(',') + links = {} + + # Parse each part into a named link + parts.each do |part, _index| + section = part.split(';') + url = section[0][/<(.*)>/, 1] + name = section[1][/rel="?([^"]*)"?/, 1].to_sym + links[name] = url + end - # Parse the value of the Link HTTP header and return a Hash whose keys are the rel values turned into symbols, and - # the values are URLs. For example, `{ next: '/v2/_catalog?n=100&last=x' }`. - def parse_link_header(header) - last='' - parts = header.split(',') - links = Hash.new - - # Parse each part into a named link - parts.each do |part, index| - section = part.split(';') - url = section[0][/<(.*)>/,1] - name = section[1][/rel="?([^"]*)"?/,1].to_sym - links[name] = url + links end - links - end + def last(header) + links = parse_link_header(header) + if links[:next] + query = URI(links[:next]).query + link_key = @uri.host.eql?('quay.io') ? 'next_page' : 'last' + last = URI.decode_www_form(query).to_h[link_key] - def last(header) - links = parse_link_header(header) - if links[:next] - query=URI(links[:next]).query - link_key = @uri.host.eql?('quay.io') ? 'next_page' : 'last' - last=URI::decode_www_form(query).to_h[link_key] + end + last + end + def manifest_sum(manifest) + size = 0 + manifest['layers'].each do |layer| + size += layer['size'] + end + size end - last - end - def manifest_sum(manifest) - size = 0 - manifest["layers"].each { |layer| - size += layer["size"] - } - size - end + private - private - def doreq(type,url,stream=nil,payload=nil) + def doreq(type, url, stream = nil, payload = nil) begin - block = stream.nil? ? nil : proc { |response| - response.read_body do |chunk| - stream.write chunk - end - } + 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: @base_uri+url, - headers: headers(payload: payload), - block_response: block, - payload: payload - )) + method: type, + url: @base_uri + url, + headers: headers(payload: payload), + block_response: block, + payload: payload + )) rescue SocketError raise DockerRegistry2::RegistryUnknownException - rescue RestClient::NotFound => error - raise DockerRegistry2::NotFound, error + rescue RestClient::NotFound => e + raise DockerRegistry2::NotFound, e rescue RestClient::Unauthorized => e header = e.response.headers[:www_authenticate] method = header.to_s.downcase.split(' ')[0] @@ -295,96 +302,102 @@ def doreq(type,url,stream=nil,payload=nil) raise DockerRegistry2::RegistryUnknownException end end - return response + response end - def do_basic_req(type, url, stream=nil, payload=nil) + def do_basic_req(type, url, stream = nil, payload = nil) begin - block = stream.nil? ? nil : proc { |response| - response.read_body do |chunk| - stream.write chunk - end - } + 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: @base_uri+url, - user: @user, - password: @password, - headers: headers(payload: payload), - block_response: block, - payload: payload - )) + method: type, + url: @base_uri + url, + 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 => error - raise DockerRegistry2::NotFound, error + rescue RestClient::NotFound => e + raise DockerRegistry2::NotFound, e end - return response + response end - def do_bearer_req(type, url, header, stream=false, payload=nil) + def do_bearer_req(type, url, header, stream = false, payload = nil) token = authenticate_bearer(header) begin - block = stream.nil? ? nil : proc { |response| - response.read_body do |chunk| - stream.write chunk - end - } + 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: @base_uri+url, - headers: headers(payload: payload, bearer_token: token), - block_response: block, - payload: payload - )) + method: type, + url: @base_uri + url, + 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 => error - raise DockerRegistry2::NotFound, error + rescue RestClient::NotFound => e + raise DockerRegistry2::NotFound, e end - return response + response end def authenticate_bearer(header) # get the parts we need target = split_auth_header(header) # did we have a username and password? - if defined? @user and @user.to_s.strip.length != 0 - target[:params][:account] = @user - end + 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, - )) + 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 => error - raise DockerRegistry2::NotFound, error + rescue RestClient::NotFound => e + raise DockerRegistry2::NotFound, e end # now save the web token result = JSON.parse(response) - return result["token"] || result["access_token"] + result['token'] || result['access_token'] end def split_auth_header(header = '') - h = Hash.new - h = {params: {}} - header.scan(/([\w]+)\=\"([^"]+)\"/) do |entry| + h = {} + h = { params: {} } + header.scan(/(\w+)="([^"]+)"/) do |entry| case entry[0] when 'realm' h[:realm] = entry[1] @@ -396,11 +409,12 @@ def split_auth_header(header = '') end def headers(payload: nil, bearer_token: nil) - headers={} - headers['Authorization']="Bearer #{bearer_token}" unless bearer_token.nil? - headers['Accept']='application/vnd.docker.distribution.manifest.v2+json, application/json' if payload.nil? - headers['Content-Type']='application/vnd.docker.distribution.manifest.v2+json' unless payload.nil? + headers = {} + headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.nil? + headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/json' if payload.nil? + headers['Content-Type'] = 'application/vnd.docker.distribution.manifest.v2+json' unless payload.nil? headers end + end end diff --git a/lib/registry/version.rb b/lib/registry/version.rb index ac094ac..9426328 100644 --- a/lib/registry/version.rb +++ b/lib/registry/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DockerRegistry2 VERSION = '1.12.0' end diff --git a/test/test.rb b/test/test.rb old mode 100644 new mode 100755 index 530791a..0d06274 --- a/test/test.rb +++ b/test/test.rb @@ -1,75 +1,70 @@ #!ruby +# frozen_string_literal: true + require 'tmpdir' require_relative '../lib/docker_registry2' def within_tmpdir - tmpdir = Dir.mktmpdir - yield(tmpdir) + tmpdir = Dir.mktmpdir + yield(tmpdir) ensure - FileUtils.remove_entry_secure tmpdir + FileUtils.remove_entry_secure tmpdir end -version = ENV["VERSION"] -regurl = ENV["REGISTRY"] +version = ENV['VERSION'] +regurl = ENV['REGISTRY'] reg = DockerRegistry2.connect regurl # do we have tags? -image = "hello-world-"+version +image = "hello-world-#{version}" tags = reg.tags image -if tags == nil || tags["name"] != image || tags["tags"] != ["latest"] - abort "Bad tags" -end +abort 'Bad tags' if tags.nil? || tags['name'] != image || tags['tags'] != ['latest'] # Tests only to run against the v2 Registry API -if version == "v2" - # can we add tags? - random_tag = ('a'..'z').to_a.shuffle[0,8].join - reg.tag image, "latest", image, random_tag +if version == 'v2' + # can we add tags? + random_tag = ('a'..'z').to_a.sample(8).join + reg.tag image, 'latest', image, random_tag - # give the registry a chance to catch up - sleep 1 + # give the registry a chance to catch up + sleep 1 - more_tags = reg.tags image - unless (more_tags["tags"] - [random_tag, "latest"]).empty? - abort "Failed to add tag" - end + more_tags = reg.tags image + abort 'Failed to add tag' unless (more_tags['tags'] - [random_tag, 'latest']).empty? - # can we delete tags? - reg.rmtag image, random_tag + # can we delete tags? + reg.rmtag image, random_tag - # give the registry a chance to catch up - sleep 1 + # give the registry a chance to catch up + sleep 1 - even_more_tags = reg.tags image - if even_more_tags["tags"] != ["latest"] - abort "Failed to delete tag" - end + even_more_tags = reg.tags image + abort 'Failed to delete tag' if even_more_tags['tags'] != ['latest'] end # can we read the manfiest? -manifest = reg.manifest image, "latest" +manifest = reg.manifest image, 'latest' # can we get the blob? case version when 'v1' - layer_blob = within_tmpdir do |tmpdir| - tmpfile = File.join(tmpdir, 'first_layer.blob') - reg.blob image, manifest['fsLayers'].first['blobSum'], tmpfile - end + layer_blob = within_tmpdir do |tmpdir| + tmpfile = File.join(tmpdir, 'first_layer.blob') + reg.blob image, manifest['fsLayers'].first['blobSum'], tmpfile + end when 'v2' - image_blob = reg.blob image, manifest['config']['digest'] - layer_blob = within_tmpdir do |tmpdir| - tmpfile = File.join(tmpdir, 'first_layer.blob') - reg.blob image, manifest['layers'].first['digest'], tmpfile - end -else + image_blob = reg.blob image, manifest['config']['digest'] + layer_blob = within_tmpdir do |tmpdir| + tmpfile = File.join(tmpdir, 'first_layer.blob') + reg.blob image, manifest['layers'].first['digest'], tmpfile + end end - + # can we get the digest? -digest = reg.digest image, "latest" +digest = reg.digest image, 'latest' # can we pull an image? -within_tmpdir {|tmpdir| reg.pull image, "latest", tmpdir } +within_tmpdir { |tmpdir| reg.pull image, 'latest', tmpdir } # success exit