diff --git a/lib/registry/registry.rb b/lib/registry/registry.rb index e89a326..12c432d 100644 --- a/lib/registry/registry.rb +++ b/lib/registry/registry.rb @@ -17,7 +17,9 @@ class Registry # rubocop:disable Metrics/ClassLength # @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}#{@uri.path}" + @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] || {} @@ -49,16 +51,18 @@ def paginate_doget(url) response = doget(url) yield response - link_header = response.headers[:link] - break unless link_header + link_header = response.headers[:link] or break + next_url = parse_link_header(link_header)[:next] or break - url = parse_link_header(link_header)[:next] + # 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 def search(query = '') all_repos = [] - paginate_doget('/v2/_catalog') do |response| + paginate_doget('v2/_catalog') do |response| repos = JSON.parse(response)['repositories'] repos.select! { |repo| repo.match?(/#{query}/) } unless query.empty? all_repos += repos @@ -75,7 +79,7 @@ def tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false) query_vars = '' query_vars = "?#{URI.encode_www_form(params)}" if params.length.positive? - response = doget "/v2/#{repo}/tags/list#{query_vars}" + response = doget "v2/#{repo}/tags/list#{query_vars}" # parse the response resp = JSON.parse response # parse out next page link if necessary @@ -104,7 +108,7 @@ def tags(repo, count = nil, last = '', withHashes = false, auto_paginate: false) def manifest(repo, tag) # first get the manifest - response = doget "/v2/#{repo}/manifests/#{tag}" + response = doget "v2/#{repo}/manifests/#{tag}" parsed = JSON.parse response.body manifest = DockerRegistry2::Manifest[parsed] manifest.body = response.body @@ -113,7 +117,7 @@ def manifest(repo, tag) end def blob(repo, digest, outpath = nil) - blob_url = "/v2/#{repo}/blobs/#{digest}" + blob_url = "v2/#{repo}/blobs/#{digest}" if outpath.nil? response = doget(blob_url) DockerRegistry2::Blob.new(response.headers, response.body) @@ -127,7 +131,7 @@ def blob(repo, digest, outpath = nil) end def manifest_digest(repo, tag) - tag_path = "/v2/#{repo}/manifests/#{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 @@ -161,9 +165,9 @@ def digest(image, tag, architecture = nil, os = nil, variant = nil) 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] + reference = doget("v2/#{image}/manifests/#{tag}").headers[:docker_content_digest] - dodelete("/v2/#{image}/manifests/#{reference}").code + dodelete("v2/#{image}/manifests/#{reference}").code end def pull(repo, tag, dir) @@ -226,7 +230,7 @@ def tag(repo, tag, newrepo, newtag) raise DockerRegistry2::RegistryVersionException unless manifest['schemaVersion'] == 2 - doput "/v2/#{newrepo}/manifests/#{newtag}", manifest.to_json + doput "v2/#{newrepo}/manifests/#{newtag}", manifest.to_json end def copy(repo, tag, newregistry, newrepo, newtag); end @@ -234,7 +238,7 @@ 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}" + response = dohead "v2/#{repo}/blobs/#{blobSum}" Integer(response.headers[:content_length], 10) end @@ -290,7 +294,7 @@ def doreq(type, url, stream = nil, payload = nil) end response = RestClient::Request.execute(@http_options.merge( method: type, - url: @base_uri + url, + url: URI.join(@base_uri, url).to_s, headers: headers(payload: payload), block_response: block, payload: payload @@ -327,7 +331,7 @@ def do_basic_req(type, url, stream = nil, payload = nil) end response = RestClient::Request.execute(@http_options.merge( method: type, - url: @base_uri + url, + url: URI.join(@base_uri, url).to_s, user: @user, password: @password, headers: headers(payload: payload), @@ -360,7 +364,7 @@ def do_bearer_req(type, url, header, stream = false, payload = nil) end response = RestClient::Request.execute(@http_options.merge( method: type, - url: @base_uri + url, + url: URI.join(@base_uri, url).to_s, headers: headers(payload: payload, bearer_token: token), block_response: block, payload: payload diff --git a/spec/docker_registry2_spec.rb b/spec/docker_registry2_spec.rb index c9cb92d..91f8fbf 100644 --- a/spec/docker_registry2_spec.rb +++ b/spec/docker_registry2_spec.rb @@ -31,12 +31,11 @@ end end - describe 'search' do - let(:search_hello_world) do - VCR.use_cassette('search/hello_world') { connected_object.search('hello-world') } + describe '#search' do + it 'lists all the repositories matching the query' do + repos = VCR.use_cassette('search/hello_world') { connected_object.search('hello-world') } + expect(repos).to eq %w[hello-world-v1 hello-world-v2 hello-world-v3 hello-world-v4] end - it { expect { search_hello_world }.not_to raise_error } - it { expect(search_hello_world.size).to eq 2 } end describe 'manifest' do @@ -88,7 +87,7 @@ let(:registry) { DockerRegistry2::Registry.new(uri) } it 'The @path should be empty' do - expect(registry.instance_variable_get(:@base_uri)).to eq('https://example.com:443') + expect(registry.instance_variable_get(:@base_uri)).to eq('https://example.com:443/') end end @@ -97,7 +96,7 @@ let(:registry) { DockerRegistry2::Registry.new(uri) } it 'The @path is included' do - expect(registry.instance_variable_get(:@base_uri)).to eq('https://registry.myCompany.com:443/dockerproxy') + expect(registry.instance_variable_get(:@base_uri)).to eq('https://registry.myCompany.com:443/dockerproxy/') end end diff --git a/spec/vcr/search/hello_world.yml b/spec/vcr/search/hello_world.yml index 968de6c..0cf47f9 100644 --- a/spec/vcr/search/hello_world.yml +++ b/spec/vcr/search/hello_world.yml @@ -31,10 +31,40 @@ http_interactions: - Tue, 14 Mar 2023 12:56:29 GMT Content-Length: - '101' + Link: + - ; rel="next" body: encoding: UTF-8 string: '{"repositories":["hello-world-v1","hello-world-v2","my-ubuntu","my-ubuntu-amd64","my-ubuntu-arm64"]} ' recorded_at: Tue, 14 Mar 2023 12:56:29 GMT +- request: + method: get + uri: http://localhost:5000/v2/_catalog?page=2 + response: + status: + code: 200 + headers: + Content-Type: + - application/json; charset=utf-8 + Link: + - ; rel="next" + body: + encoding: UTF-8 + string: '{"repositories":["hello-world-v3"]}' + recorded_at: Tue, 14 Mar 2023 12:56:29 GMT +- request: + method: get + uri: http://localhost:5000/v2/_catalog?page=3 + response: + status: + code: 200 + headers: + Content-Type: + - application/json; charset=utf-8 + body: + encoding: UTF-8 + string: '{"repositories":["hello-world-v4"]}' + recorded_at: Tue, 14 Mar 2023 12:56:29 GMT recorded_with: VCR 6.1.0