summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss16
-rw-r--r--app/controllers/admin/application_settings_controller.rb6
-rw-r--r--app/controllers/admin/container_registry_controller.rb11
-rw-r--r--app/controllers/projects/container_registry_controller.rb37
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/container_image.rb68
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/project.rb33
-rw-r--r--app/services/auth/container_registry_authentication_service.rb8
-rw-r--r--app/services/container_images/destroy_service.rb9
-rw-r--r--app/services/projects/destroy_service.rb10
-rw-r--r--app/services/projects/transfer_service.rb6
-rw-r--r--app/views/admin/container_registry/show.html.haml31
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/projects/container_registry/_image.html.haml35
-rw-r--r--app/views/projects/container_registry/_tag.html.haml2
-rw-r--r--app/views/projects/container_registry/index.html.haml24
-rw-r--r--config/routes/admin.rb2
-rw-r--r--db/migrate/20161031013926_create_container_image.rb16
-rw-r--r--db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb13
-rw-r--r--db/schema.rb11
-rw-r--r--doc/administration/container_registry.md18
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/user/project/container_registry.md19
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/registry_events.rb60
-rw-r--r--lib/container_registry/ROADMAP.md7
-rw-r--r--lib/container_registry/blob.rb4
-rw-r--r--lib/container_registry/client.rb4
-rw-r--r--lib/container_registry/registry.rb4
-rw-r--r--lib/container_registry/repository.rb48
-rw-r--r--lib/container_registry/tag.rb8
-rw-r--r--spec/factories/container_images.rb22
-rw-r--r--spec/features/container_registry_spec.rb32
-rw-r--r--spec/features/security/project/internal_access_spec.rb3
-rw-r--r--spec/features/security/project/private_access_spec.rb3
-rw-r--r--spec/features/security/project/public_access_spec.rb3
-rw-r--r--spec/lib/container_registry/blob_spec.rb15
-rw-r--r--spec/lib/container_registry/registry_spec.rb2
-rw-r--r--spec/lib/container_registry/repository_spec.rb65
-rw-r--r--spec/lib/container_registry/tag_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/container_image_spec.rb73
-rw-r--r--spec/models/namespace_spec.rb8
-rw-r--r--spec/models/project_spec.rb41
-rw-r--r--spec/services/container_images/destroy_service_spec.rb34
-rw-r--r--spec/services/projects/destroy_service_spec.rb15
-rw-r--r--spec/services/projects/transfer_service_spec.rb3
51 files changed, 622 insertions, 261 deletions
diff --git a/.gitignore b/.gitignore
index 680651986e8..51b4d06b01b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ eslint-report.html
/config/unicorn.rb
/config/secrets.yml
/config/sidekiq.yml
+/config/registry.key
/coverage/*
/coverage-javascript/
/db/*.sqlite3
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
new file mode 100644
index 00000000000..92543d7d714
--- /dev/null
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -0,0 +1,16 @@
+/**
+ * Container Registry
+ */
+
+.container-image {
+ border-bottom: 1px solid $white-normal;
+}
+
+.container-image-head {
+ padding: 0 16px;
+ line-height: 4;
+}
+
+.table.tags {
+ margin-bottom: 0;
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8d831ffdd70..1d0bd6e0b81 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -29,6 +29,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to :back
end
+ def reset_container_registry_token
+ @application_setting.reset_container_registry_access_token!
+ flash[:notice] = 'New container registry access token has been generated!'
+ redirect_to :back
+ end
+
def clear_repository_check_states
RepositoryCheck::ClearWorker.perform_async
diff --git a/app/controllers/admin/container_registry_controller.rb b/app/controllers/admin/container_registry_controller.rb
new file mode 100644
index 00000000000..265c032c67d
--- /dev/null
+++ b/app/controllers/admin/container_registry_controller.rb
@@ -0,0 +1,11 @@
+class Admin::ContainerRegistryController < Admin::ApplicationController
+ def show
+ @access_token = container_registry_access_token
+ end
+
+ private
+
+ def container_registry_access_token
+ current_application_settings.container_registry_access_token
+ end
+end
diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb
index d1f46497207..4981e57ed22 100644
--- a/app/controllers/projects/container_registry_controller.rb
+++ b/app/controllers/projects/container_registry_controller.rb
@@ -5,30 +5,49 @@ class Projects::ContainerRegistryController < Projects::ApplicationController
layout 'project'
def index
- @tags = container_registry_repository.tags
+ @images = project.container_images
end
def destroy
- url = namespace_project_container_registry_index_path(project.namespace, project)
-
- if tag.delete
- redirect_to url
+ if tag
+ delete_tag
else
- redirect_to url, alert: 'Failed to remove tag'
+ delete_image
end
end
private
+ def registry_url
+ @registry_url ||= namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
def verify_registry_enabled
render_404 unless Gitlab.config.registry.enabled
end
- def container_registry_repository
- @container_registry_repository ||= project.container_registry_repository
+ def delete_image
+ if image.destroy
+ redirect_to registry_url
+ else
+ redirect_to registry_url, alert: 'Failed to remove image'
+ end
+ end
+
+ def delete_tag
+ if tag.delete
+ image.destroy if image.tags.empty?
+ redirect_to registry_url
+ else
+ redirect_to registry_url, alert: 'Failed to remove tag'
+ end
+ end
+
+ def image
+ @image ||= project.container_images.find_by(id: params[:id])
end
def tag
- @tag ||= container_registry_repository.tag(params[:id])
+ @tag ||= image.tag(params[:tag]) if params[:tag].present?
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 671a0fe98cc..9d01a70c77d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -4,6 +4,7 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
+ add_authentication_token_field :container_registry_access_token
CACHE_KEY = 'application_setting.last'.freeze
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
@@ -157,6 +158,7 @@ class ApplicationSetting < ActiveRecord::Base
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
+ before_save :ensure_container_registry_access_token
after_commit do
Rails.cache.write(CACHE_KEY, self)
@@ -330,6 +332,10 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
+ def container_registry_access_token
+ ensure_container_registry_access_token!
+ end
+
def sidekiq_throttling_enabled?
return false unless sidekiq_throttling_column_exists?
diff --git a/app/models/container_image.rb b/app/models/container_image.rb
new file mode 100644
index 00000000000..583cb977910
--- /dev/null
+++ b/app/models/container_image.rb
@@ -0,0 +1,68 @@
+class ContainerImage < ActiveRecord::Base
+ belongs_to :project
+
+ delegate :container_registry, :container_registry_allowed_paths,
+ :container_registry_path_with_namespace, to: :project
+
+ delegate :client, to: :container_registry
+
+ validates :manifest, presence: true
+
+ before_destroy :delete_tags
+
+ before_validation :update_token, on: :create
+ def update_token
+ paths = container_registry_allowed_paths << name_with_namespace
+ token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths)
+ client.update_token(token)
+ end
+
+ def path
+ [container_registry.path, name_with_namespace].compact.join('/')
+ end
+
+ def name_with_namespace
+ [container_registry_path_with_namespace, name].reject(&:blank?).join('/')
+ end
+
+ def tag(tag)
+ ContainerRegistry::Tag.new(self, tag)
+ end
+
+ def manifest
+ @manifest ||= client.repository_tags(name_with_namespace)
+ 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
+
+ digests = tags.map {|tag| tag.digest }.to_set
+ digests.all? do |digest|
+ client.delete_repository_tag(name_with_namespace, digest)
+ end
+ end
+
+ # rubocop:disable RedundantReturn
+
+ def self.split_namespace(full_path)
+ image_name = full_path.split('/').last
+ namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1')
+ if namespace.count('/') < 1
+ namespace, image_name = full_path, ""
+ end
+ return namespace, image_name
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index d350f1d6770..4ae9d0122f2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -113,8 +113,8 @@ class Namespace < ActiveRecord::Base
end
def move_dir
- if any_project_has_container_registry_tags?
- raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ if any_project_has_container_registry_images?
+ raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has images in container registry')
end
# Move the namespace directory in all storages paths used by member projects
@@ -149,8 +149,8 @@ class Namespace < ActiveRecord::Base
end
end
- def any_project_has_container_registry_tags?
- projects.any?(&:has_container_registry_tags?)
+ def any_project_has_container_registry_images?
+ projects.joins(:container_images).any?
end
def send_update_instructions
diff --git a/app/models/project.rb b/app/models/project.rb
index da4704554b3..928965643a0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -157,6 +157,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
+ has_many :container_images, dependent: :destroy
has_many :commit_statuses, dependent: :destroy
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
@@ -408,30 +409,28 @@ class Project < ActiveRecord::Base
path_with_namespace.downcase
end
- def container_registry_repository
+ def container_registry_allowed_paths
+ @container_registry_allowed_paths ||= [container_registry_path_with_namespace] +
+ container_images.map { |i| i.name_with_namespace }
+ end
+
+ def container_registry
return unless Gitlab.config.registry.enabled
- @container_registry_repository ||= begin
- token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace)
+ @container_registry ||= begin
+ token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_allowed_paths)
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(container_registry_path_with_namespace)
+ ContainerRegistry::Registry.new(url, token: token, path: host_port)
end
end
- def container_registry_repository_url
+ def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}"
end
end
- def has_container_registry_tags?
- return unless container_registry_repository
-
- container_registry_repository.tags.any?
- end
-
def commit(ref = 'HEAD')
repository.commit(ref)
end
@@ -915,11 +914,11 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace)
- if has_container_registry_tags?
- Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
+ if container_images.present?
+ Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry images are present"
- # we currently doesn't support renaming repository if it contains tags in container registry
- raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
+ # we currently doesn't support renaming repository if it contains images in container registry
+ raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
@@ -1266,7 +1265,7 @@ class Project < ActiveRecord::Base
]
if container_registry_enabled?
- variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true }
+ variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true }
end
variables
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index db82b8f6c30..08fe6e3293e 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -17,6 +17,7 @@ module Auth
end
def self.full_access_token(*names)
+ names = names.flatten
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
@@ -61,7 +62,12 @@ module Auth
end
def process_repository_access(type, name, actions)
- requested_project = Project.find_by_full_path(name)
+ # Strips image name due to lack of
+ # per image authentication.
+ # Removes only last occurence in light
+ # of future nested groups
+ namespace, _ = ContainerImage::split_namespace(name)
+ requested_project = Project.find_by_full_path(namespace)
return unless requested_project
actions = actions.select do |action|
diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb
new file mode 100644
index 00000000000..15dca227291
--- /dev/null
+++ b/app/services/container_images/destroy_service.rb
@@ -0,0 +1,9 @@
+module ContainerImages
+ class DestroyService < BaseService
+ def execute(container_image)
+ return false unless can?(current_user, :update_container_image, project)
+
+ container_image.destroy!
+ end
+ end
+end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a7142d5950e..4e1964f79dd 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -31,10 +31,6 @@ module Projects
project.team.truncate
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)
raise_error('Failed to remove project repository. Please try again or contact administrator')
end
@@ -68,12 +64,6 @@ module Projects
end
end
- def remove_registry_tags
- return true unless Gitlab.config.registry.enabled
-
- project.container_registry_repository.delete_tags
- end
-
def raise_error(message)
raise DestroyError.new(message)
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index da6e6acd4a7..6d9e7de4f24 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -36,9 +36,9 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists")
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')
+ unless project.container_images.empty?
+ # we currently doesn't support renaming repository if it contains images in container registry
+ raise TransferError.new('Project cannot be transferred, because images are present in its container registry')
end
project.expire_caches_before_rename(old_path)
diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml
new file mode 100644
index 00000000000..ffaa7736d65
--- /dev/null
+++ b/app/views/admin/container_registry/show.html.haml
@@ -0,0 +1,31 @@
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: container_class }
+
+ %p.prepend-top-default
+ %span
+ To properly configure the Container Registry you should add the following
+ access token to the Docker Registry config.yml as follows:
+ %pre
+ %code
+ :plain
+ notifications:
+ endpoints:
+ - ...
+ headers:
+ X-Registry-Token: [#{@access_token}]
+ %br
+ Access token is
+ %code{ id: 'registry-token' }= @access_token
+
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset container registry access token by pressing the button below.
+ %p
+ = button_to reset_container_registry_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset container registry token?' } do
+ = icon('refresh')
+ Reset container registry access token
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7893c1dee97..dbd039547fa 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
+ = nav_link path: 'container_registry#show' do
+ = link_to admin_container_registry_path, title: 'Registry' do
+ %span
+ Registry
diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml
new file mode 100644
index 00000000000..72f2103b862
--- /dev/null
+++ b/app/views/projects/container_registry/_image.html.haml
@@ -0,0 +1,35 @@
+- expanded = false
+.container-image.js-toggle-container
+ .container-image-head
+ = link_to "#", class: "js-toggle-button" do
+ - if expanded
+ = icon("chevron-up")
+ - else
+ = icon("chevron-down")
+
+ = escape_once(image.name)
+ = clipboard_button(clipboard_text: "docker pull #{image.path}")
+ .controls.hidden-xs.pull-right
+ = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove image", data: { confirm: "Are you sure?" }, method: :delete do
+ = icon("trash cred")
+
+
+ .container-image-tags.js-toggle-content{ class: ("hide" unless expanded) }
+ - if image.tags.blank?
+ %li
+ .nothing-here-block No tags in Container Registry for this container image.
+
+ - else
+ .table-holder
+ %table.table.tags
+ %thead
+ %tr
+ %th Tag
+ %th Tag ID
+ %th Size
+ %th Created
+ - if can?(current_user, :update_container_image, @project)
+ %th
+
+ - image.tags.each do |tag|
+ = render 'tag', tag: tag
diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml
index 10822b6184c..f7161e85428 100644
--- a/app/views/projects/container_registry/_tag.html.haml
+++ b/app/views/projects/container_registry/_tag.html.haml
@@ -25,5 +25,5 @@
- 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
+ = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Due to a Docker limitation, all tags with the same ID will also be deleted. Are you sure?" }, method: :delete do
= icon("trash cred")
diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml
index 993da27310f..5508a3de396 100644
--- a/app/views/projects/container_registry/index.html.haml
+++ b/app/views/projects/container_registry/index.html.haml
@@ -15,25 +15,13 @@
%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)} .
+ docker build -t #{escape_once(@project.container_registry_url)}/image .
%br
- docker push #{escape_once(@project.container_registry_repository_url)}
+ docker push #{escape_once(@project.container_registry_url)}/image
- - if @tags.blank?
- %li
- .nothing-here-block No images in Container Registry for this project.
+ - if @images.blank?
+ .nothing-here-block No container 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
+ - @images.each do |image|
+ = render 'image', image: image
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 486ce3c5c87..fcbe2e2c435 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -63,6 +63,7 @@ namespace :admin do
resource :background_jobs, controller: 'background_jobs', only: [:show]
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
+ resource :container_registry, controller: 'container_registry', only: [:show]
resources :projects, only: [:index]
@@ -93,6 +94,7 @@ namespace :admin do
resources :services, only: [:index, :edit, :update]
put :reset_runners_token
put :reset_health_check_token
+ put :reset_container_registry_token
put :clear_repository_check_states
end
diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb
new file mode 100644
index 00000000000..884c78880eb
--- /dev/null
+++ b/db/migrate/20161031013926_create_container_image.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateContainerImage < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :container_images do |t|
+ t.integer :project_id
+ t.string :name
+ end
+ end
+end
diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb
new file mode 100644
index 00000000000..23d87cc6d0a
--- /dev/null
+++ b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddContainerRegistryAccessTokenToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :container_registry_access_token, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f96a7d21890..db57fb0a548 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170315194013) do
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
+ t.integer "max_pages_size", default: 100, null: false
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
@@ -107,6 +108,7 @@ ActiveRecord::Schema.define(version: 20170315194013) do
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
+ t.string "container_registry_access_token"
t.string "plantuml_url"
t.boolean "plantuml_enabled"
t.integer "terminal_max_session_time", default: 0, null: false
@@ -322,6 +324,11 @@ ActiveRecord::Schema.define(version: 20170315194013) do
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+ create_table "container_images", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "name"
+ end
+
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
@@ -688,8 +695,8 @@ ActiveRecord::Schema.define(version: 20170315194013) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.boolean "lfs_enabled"
t.text "description_html"
+ t.boolean "lfs_enabled"
t.integer "parent_id"
end
@@ -1231,8 +1238,8 @@ ActiveRecord::Schema.define(version: 20170315194013) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "organization"
t.string "incoming_email_token"
+ t.string "organization"
t.boolean "authorized_projects_populated"
t.boolean "ghost"
end
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index f707039827b..dc4e57f25fb 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -87,6 +87,23 @@ auth:
rootcertbundle: /root/certs/certbundle
```
+Also a notification endpoint must be configured with the token from
+Admin Area -> Overview -> Registry (`/admin/container_registry`) like in the following sample:
+
+```
+notifications:
+ endpoints:
+ - name: listener
+ url: https://gitlab.example.com/api/v3/registry_events
+ headers:
+ X-Registry-Token: [57Cx95fc2zHFh93VTiGD]
+ timeout: 500ms
+ threshold: 5
+ backoff: 1s
+```
+
+Check the [Registry endpoint configuration][registry-endpoint] for details.
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
@@ -583,6 +600,7 @@ notifications:
[storage-config]: https://docs.docker.com/registry/configuration/#storage
[registry-http-config]: https://docs.docker.com/registry/configuration/#http
[registry-auth]: https://docs.docker.com/registry/configuration/#auth
+[registry-endpoint]: https://docs.docker.com/registry/notifications/#/configuration
[token-config]: https://docs.docker.com/registry/configuration/#token
[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index b3c9fe275c4..f58ab4d87af 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -299,8 +299,8 @@ could look like:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- - docker build -t registry.example.com/group/project:latest .
- - docker push registry.example.com/group/project:latest
+ - docker build -t registry.example.com/group/project/image:latest .
+ - docker push registry.example.com/group/project/image:latest
```
You have to use the special `gitlab-ci-token` user created for you in order to
@@ -350,8 +350,8 @@ stages:
- deploy
variables:
- CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_COMMIT_REF_NAME
- CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest
+ CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
+ CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index b6221620e58..7524e70957f 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -10,6 +10,7 @@
- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
to pass a personal access token instead of your password in order to login to
GitLab's Container Registry.
+- Multiple level image names support was added in GitLab 8.15
With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images.
@@ -54,26 +55,23 @@ sure that you are using the Registry URL with the namespace and project name
that is hosted on GitLab:
```
-docker build -t registry.example.com/group/project .
-docker push registry.example.com/group/project
+docker build -t registry.example.com/group/project/image .
+docker push registry.example.com/group/project/image
```
Your image will be named after the following scheme:
```
-<registry URL>/<namespace>/<project>
+<registry URL>/<namespace>/<project>/<image>
```
-As such, the name of the image is unique, but you can differentiate the images
-using tags.
-
## Use images from GitLab Container Registry
To download and run a container from images hosted in GitLab Container Registry,
use `docker run`:
```
-docker run [options] registry.example.com/group/project [arguments]
+docker run [options] registry.example.com/group/project/image [arguments]
```
For more information on running Docker containers, visit the
@@ -87,7 +85,8 @@ and click **Registry** in the project menu.
This view will show you all tags in your project and will easily allow you to
delete them.
-![Container Registry panel](img/container_registry_panel.png)
+![Container Registry panel](image-needs-update)
+[//]: # (img/container_registry_panel.png)
## Build and push images using GitLab CI
@@ -136,7 +135,7 @@ A user attempted to enable an S3-backed Registry. The `docker login` step went
fine. However, when pushing an image, the output showed:
```
-The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test/docker-image]
dc5e59c14160: Pushing [==================================================>] 14.85 kB
03c20c1a019a: Pushing [==================================================>] 2.048 kB
a08f14ef632e: Pushing [==================================================>] 2.048 kB
@@ -229,7 +228,7 @@ a container image. You may need to run as root to do this. For example:
```sh
docker login s3-testing.myregistry.com:4567
-docker push s3-testing.myregistry.com:4567/root/docker-test
+docker push s3-testing.myregistry.com:4567/root/docker-test/docker-image
```
In the example above, we see the following trace on the mitmproxy window:
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 1bf20f76ad6..7c7bfada7d0 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -104,6 +104,7 @@ module API
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
+ mount ::API::RegistryEvents
mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::Projects
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index bd22b82476b..3c173b544aa 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -111,6 +111,16 @@ module API
end
end
+ def authenticate_container_registry_access_token!
+ token = request.headers['X-Registry-Token']
+ unless token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(
+ token,
+ current_application_settings.container_registry_access_token
+ )
+ unauthorized!
+ end
+ end
+
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.is_admin?
diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb
new file mode 100644
index 00000000000..fc6fc0b97e0
--- /dev/null
+++ b/lib/api/registry_events.rb
@@ -0,0 +1,60 @@
+module API
+ # RegistryEvents API
+ class RegistryEvents < Grape::API
+ before { authenticate_container_registry_access_token! }
+
+ content_type :json, 'application/vnd.docker.distribution.events.v1+json'
+
+ params do
+ requires :events, type: Array, desc: 'The ID of a project' do
+ requires :id, type: String, desc: 'The ID of the event'
+ requires :timestamp, type: String, desc: 'Timestamp of the event'
+ requires :action, type: String, desc: 'Action performed by event'
+ requires :target, type: Hash, desc: 'Target of the event' do
+ optional :mediaType, type: String, desc: 'Media type of the target'
+ optional :size, type: Integer, desc: 'Size in bytes of the target'
+ requires :digest, type: String, desc: 'Digest of the target'
+ requires :repository, type: String, desc: 'Repository of target'
+ optional :url, type: String, desc: 'Url of the target'
+ optional :tag, type: String, desc: 'Tag of the target'
+ end
+ requires :request, type: Hash, desc: 'Request of the event' do
+ requires :id, type: String, desc: 'The ID of the request'
+ optional :addr, type: String, desc: 'IP Address of the request client'
+ optional :host, type: String, desc: 'Hostname of the registry instance'
+ requires :method, type: String, desc: 'Request method'
+ requires :useragent, type: String, desc: 'UserAgent header of the request'
+ end
+ requires :actor, type: Hash, desc: 'Actor that initiated the event' do
+ optional :name, type: String, desc: 'Actor name'
+ end
+ requires :source, type: Hash, desc: 'Source of the event' do
+ optional :addr, type: String, desc: 'Hostname of source registry node'
+ optional :instanceID, type: String, desc: 'Source registry node instanceID'
+ end
+ end
+ end
+ resource :registry_events do
+ post do
+ params['events'].each do |event|
+ repository = event['target']['repository']
+
+ if event['action'] == 'push' && !!event['target']['tag']
+ namespace, container_image_name = ContainerImage::split_namespace(repository)
+ project = Project::find_by_full_path(namespace)
+
+ if project
+ container_image = project.container_images.find_or_create_by(name: container_image_name)
+
+ unless container_image.valid?
+ render_api_error!({ error: "Failed to create container image!" }, 400)
+ end
+ else
+ not_found!('Project')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/container_registry/ROADMAP.md b/lib/container_registry/ROADMAP.md
new file mode 100644
index 00000000000..e0a20776404
--- /dev/null
+++ b/lib/container_registry/ROADMAP.md
@@ -0,0 +1,7 @@
+## Road map
+
+### Initial thoughts
+
+- Determine if image names will be persisted or fetched from API
+- If persisted, how to update the stored names upon modification
+- If fetched, how to fetch only images of a given project
diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb
index eb5a2596177..8db8e483b1d 100644
--- a/lib/container_registry/blob.rb
+++ b/lib/container_registry/blob.rb
@@ -38,11 +38,11 @@ module ContainerRegistry
end
def delete
- client.delete_blob(repository.name, digest)
+ client.delete_blob(repository.name_with_namespace, digest)
end
def data
- @data ||= client.blob(repository.name, digest, type)
+ @data ||= client.blob(repository.name_with_namespace, digest, type)
end
end
end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 7f5f6d9ddb6..196cdd36a88 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -15,6 +15,10 @@ module ContainerRegistry
@options = options
end
+ def update_token(token)
+ @options[:token] = token
+ end
+
def repository_tags(name)
response_body faraday.get("/v2/#{name}/tags/list")
end
diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb
index 0e634f6b6ef..63bce655f57 100644
--- a/lib/container_registry/registry.rb
+++ b/lib/container_registry/registry.rb
@@ -8,10 +8,6 @@ module ContainerRegistry
@client = ContainerRegistry::Client.new(uri, options)
end
- def repository(name)
- ContainerRegistry::Repository.new(self, name)
- end
-
private
def default_path
diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb
deleted file mode 100644
index 0e4a7cb3cc9..00000000000
--- a/lib/container_registry/repository.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-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
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 59040199920..68dd87c979d 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -22,9 +22,7 @@ module ContainerRegistry
end
def manifest
- return @manifest if defined?(@manifest)
-
- @manifest = client.repository_manifest(repository.name, name)
+ @manifest ||= client.repository_manifest(repository.name_with_namespace, name)
end
def path
@@ -40,7 +38,7 @@ module ContainerRegistry
def digest
return @digest if defined?(@digest)
- @digest = client.repository_tag_digest(repository.name, name)
+ @digest = client.repository_tag_digest(repository.name_with_namespace, name)
end
def config_blob
@@ -82,7 +80,7 @@ module ContainerRegistry
def delete
return unless digest
- client.delete_repository_tag(repository.name, digest)
+ client.delete_repository_tag(repository.name_with_namespace, digest)
end
end
end
diff --git a/spec/factories/container_images.rb b/spec/factories/container_images.rb
new file mode 100644
index 00000000000..3693865101d
--- /dev/null
+++ b/spec/factories/container_images.rb
@@ -0,0 +1,22 @@
+FactoryGirl.define do
+ factory :container_image do
+ name "test_container_image"
+ project
+
+ transient do
+ tags ['tag']
+ stubbed true
+ end
+
+ after(:build) do |image, evaluator|
+ if evaluator.stubbed
+ allow(Gitlab.config.registry).to receive(:enabled).and_return(true)
+ allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ allow(image.client).to receive(:repository_tags).and_return({
+ name: image.name_with_namespace,
+ tags: evaluator.tags
+ })
+ end
+ end
+ end
+end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 203e55a36f2..862c9fbf6c0 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -2,15 +2,18 @@ require 'spec_helper'
describe "Container Registry" do
let(:project) { create(:empty_project) }
- let(:repository) { project.container_registry_repository }
+ let(:registry) { project.container_registry }
let(:tag_name) { 'latest' }
let(:tags) { [tag_name] }
+ let(:container_image) { create(:container_image) }
+ let(:image_name) { container_image.name }
before do
login_as(:user)
project.team << [@user, :developer]
- stub_container_registry_tags(*tags)
stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(*tags)
+ project.container_images << container_image unless container_image.nil?
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
end
@@ -19,15 +22,26 @@ describe "Container Registry" do
visit namespace_project_container_registry_index_path(project.namespace, project)
end
- context 'when no tags' do
- let(:tags) { [] }
+ context 'when no images' do
+ let(:container_image) { }
+
+ it { expect(page).to have_content('No container images in Container Registry for this project') }
+ end
- it { expect(page).to have_content('No images in Container Registry for this project') }
+ context 'when there are images' do
+ it { expect(page).to have_content(image_name) }
end
+ end
+
+ describe 'DELETE /:project/container_registry/:image_id' do
+ before do
+ visit namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
+ it do
+ expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(true)
- context 'when there are tags' do
- it { expect(page).to have_content(tag_name) }
- it { expect(page).to have_content('d7a513a66') }
+ click_on 'Remove image'
end
end
@@ -39,7 +53,7 @@ describe "Container Registry" do
it do
expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
- click_on 'Remove'
+ click_on 'Remove tag'
end
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 1a66d1a6a1e..350de2e5b6b 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -443,9 +443,12 @@ describe "Internal Project Access", feature: true do
end
describe "GET /:project_path/container_registry" do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_tags('latest')
stub_container_registry_config(enabled: true)
+ project.container_images << container_image
end
subject { namespace_project_container_registry_index_path(project.namespace, project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ad3bd60a313..62364206440 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -432,9 +432,12 @@ describe "Private Project Access", feature: true do
end
describe "GET /:project_path/container_registry" do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_tags('latest')
stub_container_registry_config(enabled: true)
+ project.container_images << container_image
end
subject { namespace_project_container_registry_index_path(project.namespace, project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index e06aab4e0b2..0e0c3140fd0 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -443,9 +443,12 @@ describe "Public Project Access", feature: true do
end
describe "GET /:project_path/container_registry" do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_tags('latest')
stub_container_registry_config(enabled: true)
+ project.container_images << container_image
end
subject { namespace_project_container_registry_index_path(project.namespace, project) }
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index bbacdc67ebd..f092449c4bd 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -9,12 +9,19 @@ describe ContainerRegistry::Blob do
'size' => 1000
}
end
- let(:token) { 'authorization-token' }
-
- let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) }
- let(:repository) { registry.repository('group/test') }
+ let(:token) { 'token' }
+
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+ let(:example_host) { 'example.com' }
+ let(:registry_url) { 'http://' + example_host }
+ let(:repository) { create(:container_image, name: '', project: project) }
let(:blob) { repository.blob(config) }
+ before do
+ stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host)
+ end
+
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) }
diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb
index 4f3f8b24fc4..4d6eea94bf0 100644
--- a/spec/lib/container_registry/registry_spec.rb
+++ b/spec/lib/container_registry/registry_spec.rb
@@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do
it { is_expected.to respond_to(:uri) }
it { is_expected.to respond_to(:path) }
- it { expect(subject.repository('test')).not_to be_nil }
+ it { expect(subject).not_to be_nil }
context '#path' do
subject { registry.path }
diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb
deleted file mode 100644
index c364e759108..00000000000
--- a/spec/lib/container_registry/repository_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-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')).not_to 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/json' })
- end
-
- context '#manifest' do
- subject { repository.manifest }
-
- it { is_expected.not_to 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.not_to 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
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index c5e31ae82b6..cdd0fe66bc3 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -1,11 +1,18 @@
require 'spec_helper'
describe ContainerRegistry::Tag do
- let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
- let(:repository) { registry.repository('group/test') }
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+ let(:example_host) { 'example.com' }
+ let(:registry_url) { 'http://' + example_host }
+ let(:repository) { create(:container_image, name: '', project: project) }
let(:tag) { repository.tag('tag') }
let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } }
+ before do
+ stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host)
+ end
+
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) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ddeb71730e7..c3ee743035a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -115,6 +115,8 @@ merge_access_levels:
- protected_branch
push_access_levels:
- protected_branch
+container_images:
+- name
project:
- taggings
- base_tags
@@ -199,6 +201,7 @@ project:
- project_authorizations
- route
- statistics
+- container_images
- uploads
award_emoji:
- awardable
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 8dbcf50ee0c..ac47b34b6fc 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1460,7 +1460,7 @@ describe Ci::Build, :models do
{ key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
end
let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true }
end
context 'and is disabled for project' do
diff --git a/spec/models/container_image_spec.rb b/spec/models/container_image_spec.rb
new file mode 100644
index 00000000000..e0bea737f59
--- /dev/null
+++ b/spec/models/container_image_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ContainerImage do
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+ let(:example_host) { 'example.com' }
+ let(:registry_url) { 'http://' + example_host }
+ let(:container_image) { create(:container_image, name: '', project: project, stubbed: false) }
+
+ before do
+ stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host)
+ 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/json' })
+ end
+
+ it { expect(container_image).to respond_to(:project) }
+ it { expect(container_image).to delegate_method(:container_registry).to(:project) }
+ it { expect(container_image).to delegate_method(:client).to(:container_registry) }
+ it { expect(container_image.tag('test')).not_to be_nil }
+
+ context '#path' do
+ subject { container_image.path }
+
+ it { is_expected.to eq('example.com/group/test') }
+ end
+
+ context 'manifest processing' do
+ context '#manifest' do
+ subject { container_image.manifest }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#valid?' do
+ subject { container_image.valid? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context '#tags' do
+ subject { container_image.tags }
+
+ it { is_expected.not_to be_empty }
+ end
+ end
+
+ context '#delete_tags' do
+ let(:tag) { ContainerRegistry::Tag.new(container_image, 'tag') }
+
+ before do
+ expect(container_image).to receive(:tags).twice.and_return([tag])
+ expect(tag).to receive(:digest).and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3672a172b088dac5b6d7ad7d49cd620d85cf')
+ end
+
+ subject { container_image.delete_tags }
+
+ context 'succeeds' do
+ before { expect(container_image.client).to receive(:delete_repository_tag).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'any fails' do
+ before { expect(container_image.client).to receive(:delete_repository_tag).and_return(false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 757f3921450..d22447c602f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -148,18 +148,20 @@ describe Namespace, models: true do
expect(@namespace.move_dir).to be_truthy
end
- context "when any project has container tags" do
+ context "when any project has container images" do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
- create(:empty_project, namespace: @namespace)
+ create(:empty_project, namespace: @namespace, container_images: [container_image])
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') }
+ it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has images in container registry') }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5e5f690acd4..aefbedf0b93 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1185,10 +1185,13 @@ describe Project, models: true do
project.rename_repo
end
- context 'container registry with tags' do
+ context 'container registry with images' do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
+ project.container_images << container_image
end
subject { project.rename_repo }
@@ -1395,20 +1398,20 @@ describe Project, models: true do
it { is_expected.to eq(project.path_with_namespace.downcase) }
end
- describe '#container_registry_repository' do
+ describe '#container_registry' do
let(:project) { create(:empty_project) }
before { stub_container_registry_config(enabled: true) }
- subject { project.container_registry_repository }
+ subject { project.container_registry }
it { is_expected.not_to be_nil }
end
- describe '#container_registry_repository_url' do
+ describe '#container_registry_url' do
let(:project) { create(:empty_project) }
- subject { project.container_registry_repository_url }
+ subject { project.container_registry_url }
before { stub_container_registry_config(**registry_settings) }
@@ -1434,34 +1437,6 @@ describe Project, models: true do
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
-
describe '#latest_successful_builds_for' do
def create_pipeline(status = 'success')
create(:ci_pipeline, project: project,
diff --git a/spec/services/container_images/destroy_service_spec.rb b/spec/services/container_images/destroy_service_spec.rb
new file mode 100644
index 00000000000..5b4dbaa7934
--- /dev/null
+++ b/spec/services/container_images/destroy_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe ContainerImages::DestroyService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:container_image) { create(:container_image, name: '') }
+ let(:project) { create(:project, path: 'test', namespace: user.namespace, container_images: [container_image]) }
+ let(:example_host) { 'example.com' }
+ let(:registry_url) { 'http://' + example_host }
+
+ it { expect(container_image).to be_valid }
+ it { expect(project.container_images).not_to be_empty }
+
+ context 'when container image has tags' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'removes all tags before destroy' do
+ service = described_class.new(project, user)
+
+ expect(container_image).to receive(:delete_tags).and_return(true)
+ expect { service.execute(container_image) }.to change(project.container_images, :count).by(-1)
+ end
+
+ it 'fails when tags are not removed' do
+ service = described_class.new(project, user)
+
+ expect(container_image).to receive(:delete_tags).and_return(false)
+ expect { service.execute(container_image) }.to raise_error(ActiveRecord::RecordNotDestroyed)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 74bfba44dfd..270e630e70e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -90,25 +90,30 @@ describe Projects::DestroyService, services: true do
end
context 'container registry' do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
+ project.container_images << container_image
end
- context 'tags deletion succeeds' do
+ context 'images deletion succeeds' do
it do
- expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
+ expect_any_instance_of(ContainerImage).to receive(:delete_tags).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) }
+ context 'images deletion fails' do
+ before do
+ expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(false)
+ end
subject { destroy_project(project, user, {}) }
- it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) }
+ it { expect{subject}.to raise_error(ActiveRecord::RecordNotDestroyed) }
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5c6fbea8d0e..5e56226ff91 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -29,9 +29,12 @@ describe Projects::TransferService, services: true do
end
context 'disallow transfering of project with tags' do
+ let(:container_image) { create(:container_image) }
+
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
+ project.container_images << container_image
end
subject { transfer_project(project, user, group) }