From 9243b6d0197ee7e3f1856d7e16f750fa74341851 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 14:22:54 +0200 Subject: [PATCH] Implement Container Registry API client --- app/controllers/projects/images_controller.rb | 16 +++-- app/models/project.rb | 5 +- app/models/registry.rb | 42 ------------ app/views/projects/images/index.html.haml | 28 ++++---- lib/image_registry/blob.rb | 47 ++++++++++++++ lib/image_registry/client.rb | 64 +++++++++++++++++++ lib/image_registry/config.rb | 15 +++++ lib/image_registry/registry.rb | 14 ++++ lib/image_registry/repository.rb | 38 +++++++++++ lib/image_registry/tag.rb | 62 ++++++++++++++++++ lib/registry_client.rb | 38 ----------- 11 files changed, 263 insertions(+), 106 deletions(-) delete mode 100644 app/models/registry.rb create mode 100644 lib/image_registry/blob.rb create mode 100644 lib/image_registry/client.rb create mode 100644 lib/image_registry/config.rb create mode 100644 lib/image_registry/registry.rb create mode 100644 lib/image_registry/repository.rb create mode 100644 lib/image_registry/tag.rb delete mode 100644 lib/registry_client.rb diff --git a/app/controllers/projects/images_controller.rb b/app/controllers/projects/images_controller.rb index 5b10746aa0..cf3bdd42cf 100644 --- a/app/controllers/projects/images_controller.rb +++ b/app/controllers/projects/images_controller.rb @@ -5,22 +5,24 @@ class Projects::ImagesController < Projects::ApplicationController layout 'project' def index - @tags = registry.tags + @tags = image_repository.tags end def destroy - # registry.destroy_tag(tag['fsLayers'].first['blobSum']) - registry.destroy_tag(registry.tag_digest(params[:id])) - redirect_to namespace_project_images_path(project.namespace, project) + if tag.delete + redirect_to namespace_project_images_path(project.namespace, project) + else + redirect_to namespace_project_images_path(project.namespace, project), alert: 'Failed to remove tag' + end end private - def registry - @registry ||= project.registry + def image_repository + @image_repository ||= project.image_repository end def tag - @tag ||= registry.tag(params[:id]) + @tag ||= image_repository[params[:id]] end end diff --git a/app/models/project.rb b/app/models/project.rb index 496f9f3e34..b905ebbfca 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -370,8 +370,9 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def registry - @registry ||= Registry.new(path_with_namespace, self) + def image_repository + @registry ||= ImageRegistry::Registry.new(Gitlab.config.registry.api_url) + @image_repository ||= ImageRegistry::Repository.new(@registry, path_with_namespace) end def registry_repository_url diff --git a/app/models/registry.rb b/app/models/registry.rb deleted file mode 100644 index b4ef60a016..0000000000 --- a/app/models/registry.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'net/http' - -class Registry - attr_accessor :path_with_namespace, :project - - def initialize(path_with_namespace, project) - @path_with_namespace = path_with_namespace - @project = project - end - - def tags - @tags ||= client.tags(path_with_namespace) - end - - def tag(reference) - return @tag[reference] if defined?(@tag[reference]) - @tag ||= {} - @tag[reference] ||= client.tag(path_with_namespace, reference) - end - - def tag_digest(reference) - return @tag_digest[reference] if defined?(@tag_digest[reference]) - @tag_digest ||= {} - @tag_digest[reference] ||= client.tag_digest(path_with_namespace, reference) - end - - def destroy_tag(reference) - client.delete_tag(path_with_namespace, reference) - end - - def blob_size(blob) - return @blob_size[blob] if defined?(@blob_size[blob]) - @blob_size ||= {} - @blob_size[blob] ||= client.blob_size(path_with_namespace, blob) - end - - private - - def client - @client ||= RegistryClient.new(Gitlab.config.registry.api_url) - end -end diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/images/index.html.haml index 338f3e5662..0987c7a39e 100644 --- a/app/views/projects/images/index.html.haml +++ b/app/views/projects/images/index.html.haml @@ -17,32 +17,26 @@ %thead %tr %th Name - %th Layers + %th Revision %th Size %th Created - %th Docker %th - - @tags.sort.each do |tag| - - details = @registry.tag(tag) - - layer = details['history'].first - - if layer && layer['v1Compatibility'] - - layer_data = JSON.parse(layer['v1Compatibility']) + - @tags.each do |tag| %tr %td - = link_to namespace_project_image_path(@project.namespace, @project, tag) do - #{details['name']}:#{details['tag']} + = link_to namespace_project_image_path(@project.namespace, @project, tag.name) do + #{tag.repository.name}:#{tag.name} %td - = details['fsLayers'].length + - if layer = tag.layers.first + \##{layer.short_revision} %td - = number_to_human_size(details['fsLayers'].inject(0) { |sum, d| sum + @registry.blob_size(d['blobSum']) }.bytes) + = pluralize(tag.layers.size, "layer") +   + = number_to_human_size(tag.total_size) %td - - if layer_data - = time_ago_in_words(DateTime.rfc3339(layer_data['created'])) - %td - - if layer_data - = layer_data['docker_version'] + = time_ago_in_words(tag.created_at) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_image_path(@project.namespace, @project, tag), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_image_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/lib/image_registry/blob.rb b/lib/image_registry/blob.rb new file mode 100644 index 0000000000..1aeeba7a68 --- /dev/null +++ b/lib/image_registry/blob.rb @@ -0,0 +1,47 @@ +module ImageRegistry + class Blob + attr_reader :repository, :config + + def initialize(repository, config) + @repository = repository + @config = config || {} + end + + def valid? + digest.present? + end + + def digest + config['digest'] + end + + def type + config['mediaType'] + end + + def size + config['size'] + end + + def revision + digest.split(':')[1] + end + + def short_revision + revision[0..8] + end + + def client + @client ||= repository.client + end + + def delete + client.delete_blob(repository.name, digest) + end + + def data + return @data if defined?(@data) + @data ||= client.blob(repository.name, digest, type) + end + end +end diff --git a/lib/image_registry/client.rb b/lib/image_registry/client.rb new file mode 100644 index 0000000000..b2e43ce4ae --- /dev/null +++ b/lib/image_registry/client.rb @@ -0,0 +1,64 @@ +require 'faraday' +require 'faraday_middleware' + +module ImageRegistry + class Client + attr_accessor :uri + + MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json' + + def initialize(base_uri, options = {}) + @base_uri = base_uri + @faraday = Faraday.new(@base_uri) do |builder| + builder.request :json + builder.headers['Accept'] = MANIFEST_VERSION + + builder.response :json, :content_type => /\bjson$/ + builder.response :json, :content_type => 'application/vnd.docker.distribution.manifest.v1+prettyjws' + + if options[:user] && options[:password] + builder.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + builder.request(:authentication, :Bearer, options[:token].to_s) + end + + builder.adapter :net_http + end + end + + def repository_tags(name) + @faraday.get("/v2/#{name}/tags/list").body + end + + def repository_manifest(name, reference) + @faraday.get("/v2/#{name}/manifests/#{reference}").body + end + + def put_repository_manifest(name, reference, manifest) + @faraday.put("/v2/#{name}/manifests/#{reference}", manifest, { "Content-Type" => MANIFEST_VERSION }).success? + end + + def repository_mount_blob(name, digest, from) + @faraday.post("/v2/#{name}/blobls/uploads/?mount=#{digest}&from=#{from}").status == 201 + end + + def repository_tag_digest(name, reference) + response = @faraday.head("/v2/#{name}/manifests/#{reference}") + response.headers['docker-content-digest'] if response.success? + end + + def delete_repository_tag(name, reference) + @faraday.delete("/v2/#{name}/manifests/#{reference}").success? + end + + def blob(name, digest, type = nil) + headers = {} + headers['Accept'] = type if type + @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body + end + + def delete_blob(name, digest) + @faraday.delete("/v2/#{name}/blobs/#{digest}").success? + end + end +end diff --git a/lib/image_registry/config.rb b/lib/image_registry/config.rb new file mode 100644 index 0000000000..1c2abec1bf --- /dev/null +++ b/lib/image_registry/config.rb @@ -0,0 +1,15 @@ +module ImageRegistry + class Config + attr_reader :tag, :blob, :data + + def initialize(tag, blob) + @tag, @blob = tag, blob + @data = JSON.parse(blob.data) + end + + def [](key) + return unless data + data[key] + end + end +end diff --git a/lib/image_registry/registry.rb b/lib/image_registry/registry.rb new file mode 100644 index 0000000000..d8de8e392e --- /dev/null +++ b/lib/image_registry/registry.rb @@ -0,0 +1,14 @@ +module ImageRegistry + class Registry + attr_reader :uri, :client + + def initialize(uri, options = {}) + @uri = URI.parse(uri) + @client = ImageRegistry::Client.new(uri, options) + end + + def [](name) + ImageRegistry::Repository.new(self, name) + end + end +end diff --git a/lib/image_registry/repository.rb b/lib/image_registry/repository.rb new file mode 100644 index 0000000000..f4f4ba65af --- /dev/null +++ b/lib/image_registry/repository.rb @@ -0,0 +1,38 @@ +module ImageRegistry + class Repository + attr_reader :registry, :name + + def initialize(registry, name) + @registry, @name = registry, name + end + + def client + @client ||= registry.client + end + + def [](tag) + ImageRegistry::Tag.new(self, tag) + end + + def manifest + return @manifest if defined?(@manifest) + @manifest = client.repository_tags(name) + end + + def valid? + manifest.present? + end + + def tags + return @tags if defined?(@tags) + return unless manifest && manifest['tags'] + @tags = manifest['tags'].map do |tag| + ImageRegistry::Tag.new(self, tag) + end + end + + def delete + tags.each(:delete) + end + end +end diff --git a/lib/image_registry/tag.rb b/lib/image_registry/tag.rb new file mode 100644 index 0000000000..2bf0b8e345 --- /dev/null +++ b/lib/image_registry/tag.rb @@ -0,0 +1,62 @@ +module ImageRegistry + class Tag + attr_reader :repository, :name + + def initialize(repository, name) + @repository, @name = repository, name + end + + def valid? + manifest.present? + end + + def manifest + return @manifest if defined?(@manifest) + @manifest = client.repository_manifest(repository.name, name) + end + + def [](key) + return unless manifest + manifest[key] + end + + def digest + return @digest if defined?(@digest) + @digest = client.repository_tag_digest(repository.name, name) + end + + def config + return @config if defined?(@config) + return unless manifest && manifest['config'] + blob = ImageRegistry::Blob.new(repository, manifest['config']) + @config = ImageRegistry::Config.new(self, blob) + end + + def created_at + return unless config + @created_at ||= DateTime.rfc3339(config['created']) + end + + def layers + return @layers if defined?(@layers) + return unless manifest + @layers = manifest['layers'].map do |layer| + ImageRegistry::Blob.new(repository, layer) + end + end + + def total_size + return unless layers + layers.map(&:size).sum + end + + def delete + return unless digest + client.delete_repository_tag(repository.name, digest) + end + + def client + @client ||= repository.client + end + end +end diff --git a/lib/registry_client.rb b/lib/registry_client.rb deleted file mode 100644 index 87518a7b39..0000000000 --- a/lib/registry_client.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'HTTParty' - -class RegistryClient - attr_accessor :uri - - def initialize(uri) - @uri = uri - end - - def tags(name) - response = HTTParty.get("#{uri}/v2/#{name}/tags/list") - response.parsed_response['tags'] - end - - def tag(name, reference) - response = HTTParty.get("#{uri}/v2/#{name}/manifests/#{reference}") - JSON.parse(response) - end - - def tag_digest(name, reference) - response = HTTParty.head("#{uri}/v2/#{name}/manifests/#{reference}") - response.headers['docker-content-digest'].split(':') - end - - def delete_tag(name, reference) - response = HTTParty.delete("#{uri}/v2/#{name}/manifests/#{reference}") - response.parsed_response - end - - def blob_size(name, digest) - response = HTTParty.head("#{uri}/v2/#{name}/blobs/#{digest}") - response.headers.content_length - end - - def delete_blob(name, digest) - HTTParty.delete("#{uri}/v2/#{name}/blobs/#{digest}") - end -end