Commit 08fddae7 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'docker-registry-view' into 'master'

Add container registry support

Tasks:

- [x] Merge docker/distribution authentication service: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3787
- [x] Implement Docker Registry API
- [x] Show a list of docker images in GitLab
- [x] Remove registry repository on project deletion
- [x] Support project rename, move and namespace rename
- [x] Use token when connecting the registry
- [x] Allow to delete images from GitLab
- [x] Support pushing from GitLab CI (gitlab-ci-token / $CI_BUILD_TOKEN)
- [x] Support GitLab Runner pulling for public repositories
- [ ] Support GitLab Runner pulling for private repositories
- [x] Add tests for Docker Registry API
- [x] Add tests for a views
- [x] Make texts nicer
- [x] Implement a backup support

- [ ] Create administration documentation https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4141
- [ ] Create user documentation

See merge request !4040
parents b7d83acf 2bea20b8
class Projects::ContainerRegistryController < Projects::ApplicationController
before_action :verify_registry_enabled
before_action :authorize_read_container_image!
before_action :authorize_update_container_image!, only: [:destroy]
layout 'project'
def index
@tags = container_registry_repository.tags
end
def destroy
url = namespace_project_container_registry_index_path(project.namespace, project)
if tag.delete
redirect_to url
else
redirect_to url, alert: 'Failed to remove tag'
end
end
private
def verify_registry_enabled
render_404 unless Gitlab.config.registry.enabled
end
def container_registry_repository
@container_registry_repository ||= project.container_registry_repository
end
def tag
@tag ||= container_registry_repository.tag(params[:id])
end
end
...@@ -33,6 +33,10 @@ module GitlabRoutingHelper ...@@ -33,6 +33,10 @@ module GitlabRoutingHelper
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
def project_container_registry_path(project, *args)
namespace_project_container_registry_index_path(project.namespace, project, *args)
end
def activity_project_path(project, *args) def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args) activity_namespace_project_path(project.namespace, project, *args)
end end
......
...@@ -152,6 +152,10 @@ module ProjectsHelper ...@@ -152,6 +152,10 @@ module ProjectsHelper
nav_tabs << :builds nav_tabs << :builds
end end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry
end
if can?(current_user, :admin_project, project) if can?(current_user, :admin_project, project)
nav_tabs << :settings nav_tabs << :settings
end end
......
...@@ -110,6 +110,10 @@ class Namespace < ActiveRecord::Base ...@@ -110,6 +110,10 @@ class Namespace < ActiveRecord::Base
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(path_was) gitlab_shell.add_namespace(path_was)
if any_project_has_container_registry_tags?
raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
if gitlab_shell.mv_namespace(path_was, path) if gitlab_shell.mv_namespace(path_was, path)
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
...@@ -131,6 +135,10 @@ class Namespace < ActiveRecord::Base ...@@ -131,6 +135,10 @@ class Namespace < ActiveRecord::Base
end end
end end
def any_project_has_container_registry_tags?
projects.any?(&:has_container_registry_tags?)
end
def send_update_instructions def send_update_instructions
projects.each do |project| projects.each do |project|
project.send_move_instructions("#{path_was}/#{project.path}") project.send_move_instructions("#{path_was}/#{project.path}")
......
...@@ -329,10 +329,28 @@ class Project < ActiveRecord::Base ...@@ -329,10 +329,28 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self) @repository ||= Repository.new(path_with_namespace, self)
end end
def container_registry_url def container_registry_repository
if container_registry_enabled? && Gitlab.config.registry.enabled return unless Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}"
@container_registry_repository ||= begin
token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace)
url = Gitlab.config.registry.api_url
host_port = Gitlab.config.registry.host_port
registry = ContainerRegistry::Registry.new(url, token: token, path: host_port)
registry.repository(path_with_namespace)
end
end end
def container_registry_repository_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{path_with_namespace}"
end
end
def has_container_registry_tags?
return unless container_registry_repository
container_registry_repository.tags.any?
end end
def commit(id = 'HEAD') def commit(id = 'HEAD')
...@@ -746,6 +764,11 @@ class Project < ActiveRecord::Base ...@@ -746,6 +764,11 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace) expire_caches_before_rename(old_path_with_namespace)
if has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry
raise Exception.new('Project cannot be renamed, because tags are present in its container registry')
end
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users. # If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository # However we cannot allow rollback since we moved repository
......
...@@ -6,7 +6,7 @@ module Auth ...@@ -6,7 +6,7 @@ module Auth
return error('not found', 404) unless registry.enabled return error('not found', 404) unless registry.enabled
if params[:offline_token] if params[:offline_token]
return error('forbidden', 403) unless current_user return error('unauthorized', 401) unless current_user
else else
return error('forbidden', 403) unless scope return error('forbidden', 403) unless scope
end end
...@@ -14,6 +14,17 @@ module Auth ...@@ -14,6 +14,17 @@ module Auth
{ token: authorized_token(scope).encoded } { token: authorized_token(scope).encoded }
end end
def self.full_access_token(*names)
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
token.audience = AUDIENCE
token[:access] = names.map do |name|
{ type: 'repository', name: name, actions: %w(pull push) }
end
token.encoded
end
private private
def authorized_token(*accesses) def authorized_token(*accesses)
......
...@@ -26,6 +26,10 @@ module Projects ...@@ -26,6 +26,10 @@ module Projects
Project.transaction do Project.transaction do
project.destroy! project.destroy!
unless remove_registry_tags
raise_error('Failed to remove project container registry. Please try again or contact administrator')
end
unless remove_repository(repo_path) unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator') raise_error('Failed to remove project repository. Please try again or contact administrator')
end end
...@@ -59,6 +63,12 @@ module Projects ...@@ -59,6 +63,12 @@ module Projects
end end
end end
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
project.container_registry_repository.delete_tags
end
def raise_error(message) def raise_error(message)
raise DestroyError.new(message) raise DestroyError.new(message)
end end
......
...@@ -34,6 +34,11 @@ module Projects ...@@ -34,6 +34,11 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
if project.has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry
raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
end
project.expire_caches_before_rename(old_path) project.expire_caches_before_rename(old_path)
# Apply new namespace id and visibility level # Apply new namespace id and visibility level
......
...@@ -46,6 +46,13 @@ ...@@ -46,6 +46,13 @@
Builds Builds
%span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :container_registry
= nav_link(controller: %w(container_registry)) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= icon('hdd-o fw')
%span
Container Registry
- if project_nav_tab? :graphs - if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do = nav_link(controller: %w(graphs)) do
= link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
......
- header_title project_title(@project, "Container Registry", project_container_registry_path(@project))
%tr.tag
%td
= escape_once(tag.name)
= clipboard_button(clipboard_text: "docker pull #{tag.path}")
%td
- if layer = tag.layers.first
%span.has-tooltip{ title: "#{layer.revision}" }
= layer.short_revision
- else
\-
%td
= number_to_human_size(tag.total_size)
&middot;
= pluralize(tag.layers.size, "layer")
%td
= time_ago_in_words(tag.created_at)
- if can?(current_user, :update_container_image, @project)
%td.content
.controls.hidden-xs.pull-right
= link_to namespace_project_container_registry_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")
- page_title "Container Registry"
= render "header_title"
%hr
%ul.content-list
.light.prepend-top-default
%p
A 'container image' is a snapshot of a container.
You can host your container images with GitLab.
%br
To start using container images hosted on GitLab you first need to login:
%pre
%code
docker login #{Gitlab.config.registry.host_port}
%br
Then you are free to create and upload a container image with build and push commands:
%pre
docker build -t #{escape_once(@project.container_registry_repository_url)} .
%br
docker push #{escape_once(@project.container_registry_repository_url)}
- if @tags.blank?
%li
.nothing-here-block No images in Container Registry for this project.
- else
.table-holder
%table.table.tags
%thead
%tr
%th Name
%th Image ID
%th Size
%th Created
- if can?(current_user, :update_container_image, @project)
%th
- @tags.each do |tag|
= render 'tag', tag: tag
\ No newline at end of file
...@@ -183,6 +183,7 @@ production: &base ...@@ -183,6 +183,7 @@ production: &base
# api_url: http://localhost:5000/ # api_url: http://localhost:5000/
# key: config/registry.key # key: config/registry.key
# issuer: omnibus-certificate # issuer: omnibus-certificate
# path: shared/registry
# #
# 2. GitLab CI settings # 2. GitLab CI settings
......
...@@ -249,9 +249,12 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes ...@@ -249,9 +249,12 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings['registry'] ||= Settingslogic.new({}) Settings['registry'] ||= Settingslogic.new({})
Settings.registry['enabled'] ||= false Settings.registry['enabled'] ||= false
Settings.registry['host'] ||= "example.com" Settings.registry['host'] ||= "example.com"
Settings.registry['port'] ||= nil
Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['api_url'] ||= "http://localhost:5000/"
Settings.registry['key'] ||= nil Settings.registry['key'] ||= nil
Settings.registry['issuer'] ||= nil Settings.registry['issuer'] ||= nil
Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':')
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
# #
# Git LFS # Git LFS
......
...@@ -693,6 +693,8 @@ Rails.application.routes.draw do ...@@ -693,6 +693,8 @@ Rails.application.routes.draw do
end end
end end
resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
resources :milestones, constraints: { id: /\d+/ } do resources :milestones, constraints: { id: /\d+/ } do
member do member do
put :sort_issues put :sort_issues
......
...@@ -39,6 +39,7 @@ documentation](../workflow/add-user/add-user.md). ...@@ -39,6 +39,7 @@ documentation](../workflow/add-user/add-user.md).
| Cancel and retry builds | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ |
......
...@@ -157,7 +157,7 @@ module Backup ...@@ -157,7 +157,7 @@ module Backup
end end
def archives_to_backup def archives_to_backup
%w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact %w{uploads builds artifacts lfs registry}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
end end
def folders_to_backup def folders_to_backup
......
require 'backup/files'
module Backup
class Registry < Files
def initialize
super('registry', Settings.registry.path)
end
def create_files_dir
Dir.mkdir(app_files_dir, 0700)
end
end
end
module ContainerRegistry
class Blob
attr_reader :repository, :config
delegate :registry, :client, to: :repository
def initialize(repository, config)
@repository = repository
@config = config || {}
end
def valid?
digest.present?
end
def path
"#{repository.path}@#{digest}"
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 delete
client.delete_blob(repository.name, digest)
end
def data
@data ||= client.blob(repository.name, digest, type)
end
end
end
require 'faraday'
require 'faraday_middleware'
module ContainerRegistry
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 |conn|
initialize_connection(conn, options)
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 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
private
def initialize_connection(conn, options)
conn.request :json
conn.headers['Accept'] = MANIFEST_VERSION
conn.response :json, content_type: /\bjson$/
if options[:user] && options[:password]
conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
elsif options[:token]
conn.request(:authorization, :bearer, options[:token].to_s)
end
conn.adapter :net_http
end
end
end
module ContainerRegistry
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
module ContainerRegistry
class Registry
attr_reader :uri, :client, :path
def initialize(uri, options = {})
@uri = uri
@path = options[:path] || default_path
@client = ContainerRegistry::Client.new(uri, options)
end
def repository(name)
ContainerRegistry::Repository.new(self, name)
end
private
def default_path
@uri.sub(/^https?:\/\//, '')
end
end
end
module ContainerRegistry
class Repository
attr_reader :registry, :name
delegate :client, to: :registry
def initialize(registry, name)
@registry, @name = registry, name
end
def path
[registry.path, name].compact.join('/')
end
def tag(tag)
ContainerRegistry::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|
ContainerRegistry::Tag.new(self, tag)
end
end
def blob(config)
ContainerRegistry::Blob.new(self, config)
end
def delete_tags
return unless tags
tags.all?(&:delete)
end
end
end
module ContainerRegistry
class Tag
attr_reader :repository, :name
delegate :registry, :client, to: :repository
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 path
"#{repository.path}:#{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_blob
return @config_blob if defined?(@config_blob)
return unless manifest && manifest['config']
@config_blob = repository.blob(manifest['config'])
end
def config
return unless config_blob
@config ||= ContainerRegistry::Config.new(self, config_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|
repository.blob(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
end
end
...@@ -96,5 +96,9 @@ module Gitlab ...@@ -96,5 +96,9 @@ module Gitlab
(?<![\/.]) (?# rule #6-7) (?<![\/.]) (?# rule #6-7)
}x.freeze }x.freeze
end end
def container_registry_reference_regex
git_reference_regex
end
end end
end end
...@@ -14,6 +14,7 @@ namespace :gitlab do ...@@ -14,6 +14,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
backup = Backup::Manager.new backup = Backup::Manager.new
backup.pack backup.pack
...@@ -54,6 +55,7 @@ namespace :gitlab do ...@@ -54,6 +55,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke Rake::Task['gitlab:shell:setup'].invoke
backup.cleanup backup.cleanup
...@@ -173,6 +175,25 @@ namespace :gitlab do ...@@ -173,6 +175,25 @@ namespace :gitlab do
end end
end end
namespace :registry do
task create: :environment do
$progress.puts "Dumping container registry images ... ".blue
if ENV["SKIP"] && ENV["SKIP"].include?("registry")
$progress.puts "[SKIPPED]".cyan
else
Backup::Registry.new.dump
$progress.puts "done".green
end
end
task restore: :environment do
$progress.puts "Restoring container registry images ... ".blue
Backup::Registry.new.restore
$progress.puts "done".green
end
end
def configure_cron_mode def configure_cron_mode
if ENV['CRON'] if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a # We need an object we can say 'puts' and 'print' to; let's use a
......
require 'spec_helper'
describe "Container Registry" do
let(:project) { create(:empty_project) }
let(:repository) { project.container_registry_repository }
let(:tag_name) { 'latest' }
let(:tags) { [tag_name] }
before do
login_as(:user)
project.team << [@user, :developer]
stub_container_registry_tags(*tags)
stub_container_registry_config(enabled: true)
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
end
describe 'GET /:project/container_registry' do
before do
visit namespace_project_container_registry_index_path(project.namespace, project)
end
context 'when no tags' do
let(:tags) { [] }
it { expect(page).to have_content('No images in Container Registry for this project') }
end
context 'when there are tags' do
it { expect(page).to have_content(tag_name)}
end
end
describe 'DELETE /:project/container_registry/tag' do
before do
visit namespace_project_container_registry_index_path(project.namespace, project)
end
it do
expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
click_on 'Remove'
end
end
end
{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}}
{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
require 'spec_helper'
describe ContainerRegistry::Blob do
let(:digest) { 'sha256:0123456789012345' }
let(:config) do
{
'digest' => digest,
'mediaType' => 'binary',
'size' => 1000
}
end
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) }
it { expect(blob).to respond_to(:repository) }
it { expect(blob).to delegate_method(:registry).to(:repository) }
it { expect(blob).to delegate_method(:client).to(:repository) }
context '#path' do
subject { blob.path }
it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') }
end
context '#digest' do
subject { blob.digest }
it { is_expected.to eq(digest) }
end
context '#type' do
subject { blob.type }
it { is_expected.to eq('binary') }
end
context '#revision' do
subject { blob.revision }
it { is_expected.to eq('0123456789012345') }
end
context '#short_revision' do
subject { blob.short_revision }
it { is_expected.to eq('012345678') }
end
context '#delete' do
before do
stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
to_return(status: 200)
end
subject { blob.delete }
it { is_expected.to be_truthy }
end
end
require 'spec_helper'
describe ContainerRegistry::Registry do
let(:path) { nil }
let(:registry) { described_class.new('http://example.com', path: path) }
subject { registry }
it { is_expected.to respond_to(:client) }
it { is_expected.to respond_to(:uri) }
it { is_expected.to respond_to(:path) }
it { expect(subject.repository('test')).to_not be_nil }
context '#path' do
subject { registry.path }
context 'path from URL' do
it { is_expected.to eq('example.com') }
end
context 'custom path' do
let(:path) { 'registry.example.com' }
it { is_expected.to eq(path) }
end
end
end
require 'spec_helper'
describe ContainerRegistry::Repository do
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
it { expect(repository).to respond_to(:registry) }
it { expect(repository).to delegate_method(:client).to(:registry) }
it { expect(repository.tag('test')).to_not be_nil }
context '#path' do
subject { repository.path }
it { is_expected.to eq('example.com/group/test') }
end
context 'manifest processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/tags/list').
with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }).
to_return(
status: 200,
body: JSON.dump(tags: ['test']),
headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
end
context '#manifest' do
subject { repository.manifest }
it { is_expected.to_not be_nil }
end
context '#valid?' do
subject { repository.valid? }
it { is_expected.to be_truthy }
end
context '#tags' do
subject { repository.tags }
it { is_expected.to_not be_empty }
end
end
context '#delete_tags' do
let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') }
before { expect(repository).to receive(:tags).twice.and_return([tag]) }
subject { repository.delete_tags }
context 'succeeds' do
before { expect(tag).to receive(:delete).and_return(true) }
it { is_expected.to be_truthy }
end
context 'any fails' do
before { expect(tag).to receive(:delete).and_return(false) }
it { is_expected.to be_falsey }
end
end
end
require 'spec_helper'
describe ContainerRegistry::Tag do
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
let(:tag) { repository.tag('tag') }
let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } }
it { expect(tag).to respond_to(:repository) }
it { expect(tag).to delegate_method(:registry).to(:repository) }
it { expect(tag).to delegate_method(:client).to(:repository) }
context '#path' do
subject { tag.path }
it { is_expected.to eq('example.com/group/test:tag') }
end
context 'manifest processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
with(headers: headers).
to_return(
status: 200,
body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
end
context '#layers' do
subject { tag.layers }
it { expect(subject.length).to eq(1) }
end
context '#total_size' do
subject { tag.total_size }
it { is_expected.to eq(2319870) }
end
context 'config processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
with(headers: { 'Accept' => 'application/octet-stream' }).
to_return(
status: 200,
body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
end
context '#config' do
subject { tag.config }
it { is_expected.to_not be_nil }
end
context '#created_at' do
subject { tag.created_at }
it { is_expected.to_not be_nil }
end
end
end
context 'manifest digest' do
before do
stub_request(:head, 'http://example.com/v2/group/test/manifests/tag').
with(headers: headers).
to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
end
context '#digest' do
subject { tag.digest }
it { is_expected.to eq('sha256:digest') }
end
context '#delete' do
before do
stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest').
with(headers: headers).
to_return(status: 200)
end
subject { tag.delete }
it { is_expected.to be_truthy }
end
end
end
...@@ -70,6 +70,20 @@ describe Namespace, models: true do ...@@ -70,6 +70,20 @@ describe Namespace, models: true do
allow(@namespace).to receive(:path).and_return(new_path) allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace.move_dir).to be_truthy expect(@namespace.move_dir).to be_truthy
end end
context "when any project has container tags" do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
create(:empty_project, namespace: @namespace)
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return('new_path')
end
it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
end
end end
describe :rm_dir do describe :rm_dir do
......
...@@ -634,11 +634,11 @@ describe Project, models: true do ...@@ -634,11 +634,11 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier. # call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
end
it 'renames a repository' do
allow(project).to receive(:previous_changes).and_return('path' => ['foo']) allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end
it 'renames a repository' do
ns = project.namespace_dir ns = project.namespace_dir
expect(gitlab_shell).to receive(:mv_repository). expect(gitlab_shell).to receive(:mv_repository).
...@@ -663,6 +663,17 @@ describe Project, models: true do ...@@ -663,6 +663,17 @@ describe Project, models: true do
project.rename_repo project.rename_repo
end end
context 'container registry with tags' do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
end
subject { project.rename_repo }
it { expect{subject}.to raise_error(Exception) }
end
end end
describe '#expire_caches_before_rename' do describe '#expire_caches_before_rename' do
...@@ -772,4 +783,71 @@ describe Project, models: true do ...@@ -772,4 +783,71 @@ describe Project, models: true do
expect(project.protected_branch?('foo')).to eq(false) expect(project.protected_branch?('foo')).to eq(false)
end end
end end
describe '#container_registry_repository' do
let(:project) { create(:empty_project) }
before { stub_container_registry_config(enabled: true) }
subject { project.container_registry_repository }
it { is_expected.to_not be_nil }
end
describe '#container_registry_repository_url' do
let(:project) { create(:empty_project) }
subject { project.container_registry_repository_url }
before { stub_container_registry_config(**registry_settings) }
context 'for enabled registry' do
let(:registry_settings) do
{
enabled: true,
host_port: 'example.com',
}
end
it { is_expected.to_not be_nil }
end
context 'for disabled registry' do
let(:registry_settings) do
{
enabled: false
}
end
it { is_expected.to be_nil }
end
end
describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) }
subject { project.has_container_registry_tags? }
context 'for enabled registry' do
before { stub_container_registry_config(enabled: true) }
context 'with tags' do
before { stub_container_registry_tags('test', 'test2') }
it { is_expected.to be_truthy }
end
context 'when no tags' do
before { stub_container_registry_tags }
it { is_expected.to be_falsey }
end
end
context 'for disabled registry' do
before { stub_container_registry_config(enabled: false) }
it { is_expected.to be_falsey }
end
end
end end
...@@ -5,19 +5,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -5,19 +5,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_user) { nil } let(:current_user) { nil }
let(:current_params) { {} } let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:registry_settings) do
{
enabled: true,
issuer: 'rspec',
key: nil
}
end
let(:payload) { JWT.decode(subject[:token], rsa_key).first } let(:payload) { JWT.decode(subject[:token], rsa_key).first }
subject { described_class.new(current_project, current_user, current_params).execute } subject { described_class.new(current_project, current_user, current_params).execute }
before do before do
allow(Gitlab.config.registry).to receive_messages(registry_settings) stub_container_registry_config(enabled: true, issuer: 'rspec', key: nil)
allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
end end
...@@ -57,6 +50,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -57,6 +50,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
end end
shared_examples 'an unauthorized' do
it { is_expected.to include(http_status: 401) }
it { is_expected.to_not include(:token) }
end
shared_examples 'a forbidden' do shared_examples 'a forbidden' do
it { is_expected.to include(http_status: 403) } it { is_expected.to include(http_status: 403) }
it { is_expected.to_not include(:token) } it { is_expected.to_not include(:token) }
...@@ -123,7 +121,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -123,7 +121,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
{ offline_token: true } { offline_token: true }
end end
it_behaves_like 'a forbidden' it_behaves_like 'an unauthorized'
end end
context 'allow to pull and push images' do context 'allow to pull and push images' do
...@@ -164,6 +162,20 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -164,6 +162,20 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
end end
end end
context 'for project without container registry' do
let(:project) { create(:empty_project, :public, container_registry_enabled: false) }
before { project.update(container_registry_enabled: false) }
context 'disallow when pulling' do
let(:current_params) do
{ scope: "repository:#{project.path_with_namespace}:pull" }
end
it_behaves_like 'a forbidden'
end
end
end end
context 'unauthorized' do context 'unauthorized' do
...@@ -172,7 +184,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -172,7 +184,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
{ offline_token: true } { offline_token: true }
end end
it_behaves_like 'a forbidden' it_behaves_like 'an unauthorized'
end end
context 'for invalid scope' do context 'for invalid scope' do
......
...@@ -28,6 +28,29 @@ describe Projects::DestroyService, services: true do ...@@ -28,6 +28,29 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy } it { expect(Dir.exist?(remove_path)).to be_truthy }
end end
context 'container registry' do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
end
context 'tags deletion succeeds' do
it do
expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
destroy_project(project, user, {})
end
end
context 'tags deletion fails' do
before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) }
subject { destroy_project(project, user, {}) }
it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) }
end
end
def destroy_project(project, user, params) def destroy_project(project, user, params)
Projects::DestroyService.new(project, user, params).execute Projects::DestroyService.new(project, user, params).execute
end end
......
...@@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do ...@@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(user.namespace) } it { expect(project.namespace).to eq(user.namespace) }
end end
context 'disallow transfering of project with tags' do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
end
subject { transfer_project(project, user, group) }
it { is_expected.to be_falsey }
end
context 'namespace -> not allowed namespace' do context 'namespace -> not allowed namespace' do
before do before do
@result = transfer_project(project, user, group) @result = transfer_project(project, user, group)
......
...@@ -25,6 +25,23 @@ module StubGitlabCalls ...@@ -25,6 +25,23 @@ module StubGitlabCalls
allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false) allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
end end
def stub_container_registry_config(registry_settings)
allow(Gitlab.config.registry).to receive_messages(registry_settings)
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
end
def stub_container_registry_tags(*tags)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return(
{ "tags" => tags }
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
)
end
private private
def gitlab_url def gitlab_url
......
...@@ -21,7 +21,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -21,7 +21,7 @@ describe 'gitlab:app namespace rake task' do
end end
def reenable_backup_sub_tasks def reenable_backup_sub_tasks
%w{db repo uploads builds artifacts lfs}.each do |subtask| %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end end
end end
...@@ -65,6 +65,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -65,6 +65,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end end
...@@ -122,7 +123,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -122,7 +123,7 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen( tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
) )
expect(exit_status).to eq(0) expect(exit_status).to eq(0)
expect(tar_contents).to match('db/') expect(tar_contents).to match('db/')
...@@ -131,12 +132,13 @@ describe 'gitlab:app namespace rake task' do ...@@ -131,12 +132,13 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end end
it 'should delete temp directories' do it 'should delete temp directories' do
temp_dirs = Dir.glob( temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}') File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
) )
expect(temp_dirs).to be_empty expect(temp_dirs).to be_empty
...@@ -172,7 +174,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -172,7 +174,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen( tar_contents, _exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
) )
expect(tar_contents).to match('db/') expect(tar_contents).to match('db/')
...@@ -180,6 +182,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -180,6 +182,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/') expect(tar_contents).not_to match('repositories/')
end end
...@@ -195,6 +198,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -195,6 +198,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment