Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
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/projects/container_registry_controller.rb34
-rw-r--r--app/controllers/projects/registry/application_controller.rb16
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb43
-rw-r--r--app/controllers/projects/registry/tags_controller.rb28
-rw-r--r--app/models/container_repository.rb77
-rw-r--r--app/models/project.rb45
-rw-r--r--app/services/auth/container_registry_authentication_service.rb58
-rw-r--r--app/services/projects/destroy_service.rb18
-rw-r--r--app/views/projects/registry/repositories/_image.html.haml32
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml (renamed from app/views/projects/container_registry/_tag.html.haml)8
-rw-r--r--app/views/projects/registry/repositories/index.html.haml (renamed from app/views/projects/container_registry/index.html.haml)23
-rw-r--r--changelogs/unreleased/feature-multi-level-container-registry-images.yml4
-rw-r--r--config/routes/project.rb10
-rw-r--r--db/migrate/20170322013926_create_container_repository.rb16
-rw-r--r--db/schema.rb11
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/user/project/container_registry.md24
-rw-r--r--lib/container_registry/blob.rb4
-rw-r--r--lib/container_registry/path.rb70
-rw-r--r--lib/container_registry/registry.rb4
-rw-r--r--lib/container_registry/repository.rb48
-rw-r--r--lib/container_registry/tag.rb10
-rw-r--r--lib/gitlab/regex.rb7
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb84
-rw-r--r--spec/factories/container_repositories.rb33
-rw-r--r--spec/features/container_registry_spec.rb62
-rw-r--r--spec/features/security/project/internal_access_spec.rb5
-rw-r--r--spec/features/security/project/private_access_spec.rb5
-rw-r--r--spec/features/security/project/public_access_spec.rb5
-rw-r--r--spec/lib/container_registry/blob_spec.rb115
-rw-r--r--spec/lib/container_registry/path_spec.rb212
-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.rb86
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/container_repository_spec.rb209
-rw-r--r--spec/models/namespace_spec.rb12
-rw-r--r--spec/models/project_spec.rb97
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb94
-rw-r--r--spec/services/projects/destroy_service_spec.rb67
-rw-r--r--spec/services/projects/transfer_service_spec.rb5
-rw-r--r--spec/support/stub_gitlab_calls.rb39
45 files changed, 1378 insertions, 440 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..3266714396e
--- /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: 4em;
+}
+
+.table.tags {
+ margin-bottom: 0;
+}
diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb
deleted file mode 100644
index d1f46497207..00000000000
--- a/app/controllers/projects/container_registry_controller.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-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
diff --git a/app/controllers/projects/registry/application_controller.rb b/app/controllers/projects/registry/application_controller.rb
new file mode 100644
index 00000000000..a56f9c58726
--- /dev/null
+++ b/app/controllers/projects/registry/application_controller.rb
@@ -0,0 +1,16 @@
+module Projects
+ module Registry
+ class ApplicationController < Projects::ApplicationController
+ layout 'project'
+
+ before_action :verify_registry_enabled!
+ before_action :authorize_read_container_image!
+
+ private
+
+ def verify_registry_enabled!
+ render_404 unless Gitlab.config.registry.enabled
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
new file mode 100644
index 00000000000..17f391ba07f
--- /dev/null
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -0,0 +1,43 @@
+module Projects
+ module Registry
+ class RepositoriesController < ::Projects::Registry::ApplicationController
+ before_action :authorize_update_container_image!, only: [:destroy]
+ before_action :ensure_root_container_repository!, only: [:index]
+
+ def index
+ @images = project.container_repositories
+ end
+
+ def destroy
+ if image.destroy
+ redirect_to project_container_registry_path(@project),
+ notice: 'Image repository has been removed successfully!'
+ else
+ redirect_to project_container_registry_path(@project),
+ alert: 'Failed to remove image repository!'
+ end
+ end
+
+ private
+
+ def image
+ @image ||= project.container_repositories.find(params[:id])
+ end
+
+ ##
+ # Container repository object for root project path.
+ #
+ # Needed to maintain a backwards compatibility.
+ #
+ def ensure_root_container_repository!
+ ContainerRegistry::Path.new(@project.full_path).tap do |path|
+ break if path.has_repository?
+
+ ContainerRepository.build_from_path(path).tap do |repository|
+ repository.save! if repository.has_tags?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
new file mode 100644
index 00000000000..d689cade3ab
--- /dev/null
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -0,0 +1,28 @@
+module Projects
+ module Registry
+ class TagsController < ::Projects::Registry::ApplicationController
+ before_action :authorize_update_container_image!, only: [:destroy]
+
+ def destroy
+ if tag.delete
+ redirect_to project_container_registry_path(@project),
+ notice: 'Registry tag has been removed successfully!'
+ else
+ redirect_to project_container_registry_path(@project),
+ alert: 'Failed to remove registry tag!'
+ end
+ end
+
+ private
+
+ def image
+ @image ||= project.container_repositories
+ .find(params[:repository_id])
+ end
+
+ def tag
+ @tag ||= image.tag(params[:id])
+ end
+ end
+ end
+end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
new file mode 100644
index 00000000000..9682df3a586
--- /dev/null
+++ b/app/models/container_repository.rb
@@ -0,0 +1,77 @@
+class ContainerRepository < ActiveRecord::Base
+ belongs_to :project
+
+ validates :name, length: { minimum: 0, allow_nil: false }
+ validates :name, uniqueness: { scope: :project_id }
+
+ delegate :client, to: :registry
+
+ before_destroy :delete_tags!
+
+ def registry
+ @registry ||= begin
+ token = Auth::ContainerRegistryAuthenticationService.full_access_token(path)
+
+ url = Gitlab.config.registry.api_url
+ host_port = Gitlab.config.registry.host_port
+
+ ContainerRegistry::Registry.new(url, token: token, path: host_port)
+ end
+ end
+
+ def path
+ @path ||= [project.full_path, name].select(&:present?).join('/')
+ end
+
+ def tag(tag)
+ ContainerRegistry::Tag.new(self, tag)
+ end
+
+ def manifest
+ @manifest ||= client.repository_tags(path)
+ 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 has_tags?
+ tags.any?
+ end
+
+ def root_repository?
+ name.empty?
+ end
+
+ def delete_tags!
+ return unless has_tags?
+
+ digests = tags.map { |tag| tag.digest }.to_set
+
+ digests.all? do |digest|
+ client.delete_repository_tag(self.path, digest)
+ end
+ end
+
+ def self.build_from_path(path)
+ self.new(project: path.repository_project,
+ name: path.repository_name)
+ end
+
+ def self.create_from_path!(path)
+ build_from_path(path).tap(&:save!)
+ end
+
+ def self.build_root_repository(project)
+ self.new(project: project, name: '')
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 1f95d00baf8..f68714d09f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -159,6 +159,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_repositories, dependent: :destroy
has_many :commit_statuses, dependent: :destroy
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
@@ -406,32 +407,15 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self)
end
- def container_registry_path_with_namespace
- path_with_namespace.downcase
- end
-
- def container_registry_repository
- return unless Gitlab.config.registry.enabled
-
- @container_registry_repository ||= begin
- token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_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(container_registry_path_with_namespace)
- 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}"
+ "#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}"
end
end
def has_container_registry_tags?
- return unless container_registry_repository
-
- container_registry_repository.tags.any?
+ container_repositories.to_a.any?(&:has_tags?) ||
+ has_root_container_repository_tags?
end
def commit(ref = 'HEAD')
@@ -922,10 +906,10 @@ 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"
+ Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags 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)
@@ -1272,7 +1256,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
@@ -1405,4 +1389,15 @@ class Project < ActiveRecord::Base
Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
end
+
+ ##
+ # This method is here because of support for legacy container repository
+ # which has exactly the same path like project does, but which might not be
+ # persisted in `container_repositories` table.
+ #
+ def has_root_container_repository_tags?
+ return false unless Gitlab.config.registry.enabled
+
+ ContainerRepository.build_root_repository(self).has_tags?
+ end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index db82b8f6c30..5e151b0f044 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
@@ -37,13 +38,13 @@ module Auth
private
def authorized_token(*accesses)
- token = JSONWebToken::RSAToken.new(registry.key)
- token.issuer = registry.issuer
- token.audience = params[:service]
- token.subject = current_user.try(:username)
- token.expire_time = self.class.token_expire_at
- token[:access] = accesses.compact
- token
+ JSONWebToken::RSAToken.new(registry.key).tap do |token|
+ token.issuer = registry.issuer
+ token.audience = params[:service]
+ token.subject = current_user.try(:username)
+ token.expire_time = self.class.token_expire_at
+ token[:access] = accesses.compact
+ end
end
def scope
@@ -55,20 +56,43 @@ module Auth
def process_scope(scope)
type, name, actions = scope.split(':', 3)
actions = actions.split(',')
+ path = ContainerRegistry::Path.new(name)
+
return unless type == 'repository'
- process_repository_access(type, name, actions)
+ process_repository_access(type, path, actions)
end
- def process_repository_access(type, name, actions)
- requested_project = Project.find_by_full_path(name)
+ def process_repository_access(type, path, actions)
+ return unless path.valid?
+
+ requested_project = path.repository_project
+
return unless requested_project
actions = actions.select do |action|
can_access?(requested_project, action)
end
- { type: type, name: name, actions: actions } if actions.present?
+ return unless actions.present?
+
+ # At this point user/build is already authenticated.
+ #
+ ensure_container_repository!(path, actions)
+
+ { type: type, name: path.to_s, actions: actions }
+ end
+
+ ##
+ # Because we do not have two way communication with registry yet,
+ # we create a container repository image resource when push to the
+ # registry is successfuly authorized.
+ #
+ def ensure_container_repository!(path, actions)
+ return if path.has_repository?
+ return unless actions.include?('push')
+
+ ContainerRepository.create_from_path!(path)
end
def can_access?(requested_project, requested_action)
@@ -101,6 +125,11 @@ module Auth
can?(current_user, :read_container_image, requested_project)
end
+ ##
+ # We still support legacy pipeline triggers which do not have associated
+ # actor. New permissions model and new triggers are always associated with
+ # an actor, so this should be improved in 10.0 version of GitLab.
+ #
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
has_authentication_ability?(:build_create_container_image) &&
@@ -113,14 +142,11 @@ module Auth
end
def error(code, status:, message: '')
- {
- errors: [{ code: code, message: message }],
- http_status: status
- }
+ { errors: [{ code: code, message: message }], http_status: status }
end
def has_authentication_ability?(capability)
- (@authentication_abilities || []).include?(capability)
+ @authentication_abilities.to_a.include?(capability)
end
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a7142d5950e..06d8d143231 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -31,16 +31,16 @@ 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')
+ unless remove_legacy_registry_tags
+ raise_error('Failed to remove some tags in 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')
+ raise_error('Failed to remove project repository. Please try again or contact administrator.')
end
unless remove_repository(wiki_path)
- raise_error('Failed to remove wiki repository. Please try again or contact administrator')
+ raise_error('Failed to remove wiki repository. Please try again or contact administrator.')
end
end
@@ -68,10 +68,16 @@ module Projects
end
end
- def remove_registry_tags
+ ##
+ # This method makes sure that we correctly remove registry tags
+ # for legacy image repository (when repository path equals project path).
+ #
+ def remove_legacy_registry_tags
return true unless Gitlab.config.registry.enabled
- project.container_registry_repository.delete_tags
+ ContainerRepository.build_root_repository(project).tap do |repository|
+ return repository.has_tags? ? repository.delete_tags! : true
+ end
end
def raise_error(message)
diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml
new file mode 100644
index 00000000000..d183ce34a3a
--- /dev/null
+++ b/app/views/projects/registry/repositories/_image.html.haml
@@ -0,0 +1,32 @@
+.container-image.js-toggle-container
+ .container-image-head
+ = link_to "#", class: "js-toggle-button" do
+ = icon('chevron-down', 'aria-hidden': 'true')
+ = escape_once(image.path)
+
+ = clipboard_button(clipboard_text: "docker pull #{image.path}")
+
+ .controls.hidden-xs.pull-right
+ = link_to namespace_project_container_registry_path(@project.namespace, @project, image),
+ class: 'btn btn-remove has-tooltip',
+ title: 'Remove repository',
+ data: { confirm: 'Are you sure?' },
+ method: :delete do
+ = icon('trash cred', 'aria-hidden': 'true')
+
+ .container-image-tags.js-toggle-content.hide
+ - if image.has_tags?
+ .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
+ = render partial: 'tag', collection: image.tags
+ - else
+ .nothing-here-block No tags in Container Registry for this container image.
+
diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 10822b6184c..ee1ec0e8f9a 100644
--- a/app/views/projects/container_registry/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -25,5 +25,9 @@
- 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")
+ = link_to namespace_project_registry_repository_tag_path(@project.namespace, @project, tag.repository, tag.name),
+ method: :delete,
+ class: 'btn btn-remove has-tooltip',
+ title: 'Remove tag',
+ data: { confirm: 'Are you sure you want to delete this tag?' } do
+ = icon('trash cred')
diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 993da27310f..be128e92fa7 100644
--- a/app/views/projects/container_registry/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -15,25 +15,12 @@
%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 image repositories 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
+ = render partial: 'image', collection: @images
diff --git a/changelogs/unreleased/feature-multi-level-container-registry-images.yml b/changelogs/unreleased/feature-multi-level-container-registry-images.yml
new file mode 100644
index 00000000000..6d39a6c17c0
--- /dev/null
+++ b/changelogs/unreleased/feature-multi-level-container-registry-images.yml
@@ -0,0 +1,4 @@
+---
+title: Add support for multi-level container image repository names
+merge_request: 10109
+author: André Guede
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 62e2e6145fd..e38f0537143 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -221,7 +221,15 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+ resources :container_registry, only: [:index, :destroy],
+ controller: 'registry/repositories'
+
+ namespace :registry do
+ resources :repository, only: [] do
+ resources :tags, only: [:destroy],
+ constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+ end
+ end
resources :milestones, constraints: { id: /\d+/ } do
member do
diff --git a/db/migrate/20170322013926_create_container_repository.rb b/db/migrate/20170322013926_create_container_repository.rb
new file mode 100644
index 00000000000..91540bc88bd
--- /dev/null
+++ b/db/migrate/20170322013926_create_container_repository.rb
@@ -0,0 +1,16 @@
+class CreateContainerRepository < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :container_repositories do |t|
+ t.references :project, foreign_key: true, index: true, null: false
+ t.string :name, null: false
+
+ t.timestamps null: false
+ end
+
+ add_index :container_repositories, [:project_id, :name], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5ea307911f5..d8e116b7903 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -323,6 +323,16 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+ create_table "container_repositories", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "container_repositories", ["project_id", "name"], name: "index_container_repositories_on_project_id_and_name", unique: true, using: :btree
+ add_index "container_repositories", ["project_id"], name: "index_container_repositories_on_project_id", using: :btree
+
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
@@ -1304,6 +1314,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
+ add_foreign_key "container_repositories", "projects"
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index edb315d5b84..ffa0831290a 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..6a2ca7fb428 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 9.1
With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images.
@@ -54,18 +55,25 @@ 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.
+GitLab supports up to three levels of image repository names.
+
+Following examples of image tags are valid:
+
+```
+registry.example.com/group/project:some-tag
+registry.example.com/group/project/image:latest
+registry.example.com/group/project/my/image:rc1
+```
## Use images from GitLab Container Registry
@@ -73,7 +81,7 @@ 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
@@ -136,7 +144,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 +237,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/container_registry/blob.rb b/lib/container_registry/blob.rb
index eb5a2596177..d5f85f9fcad 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.path, digest)
end
def data
- @data ||= client.blob(repository.name, digest, type)
+ @data ||= client.blob(repository.path, digest, type)
end
end
end
diff --git a/lib/container_registry/path.rb b/lib/container_registry/path.rb
new file mode 100644
index 00000000000..a4b5f2aba6c
--- /dev/null
+++ b/lib/container_registry/path.rb
@@ -0,0 +1,70 @@
+module ContainerRegistry
+ ##
+ # Class responsible for extracting project and repository name from
+ # image repository path provided by a containers registry API response.
+ #
+ # Example:
+ #
+ # some/group/my_project/my/image ->
+ # project: some/group/my_project
+ # repository: my/image
+ #
+ class Path
+ InvalidRegistryPathError = Class.new(StandardError)
+
+ LEVELS_SUPPORTED = 3
+
+ def initialize(path)
+ @path = path
+ end
+
+ def valid?
+ @path =~ Gitlab::Regex.container_repository_name_regex &&
+ components.size > 1 &&
+ components.size < Namespace::NUMBER_OF_ANCESTORS_ALLOWED
+ end
+
+ def components
+ @components ||= @path.to_s.split('/')
+ end
+
+ def nodes
+ raise InvalidRegistryPathError unless valid?
+
+ @nodes ||= components.size.downto(2).map do |length|
+ components.take(length).join('/')
+ end
+ end
+
+ def has_project?
+ repository_project.present?
+ end
+
+ def has_repository?
+ return false unless has_project?
+
+ repository_project.container_repositories
+ .where(name: repository_name).any?
+ end
+
+ def root_repository?
+ @path == repository_project.full_path
+ end
+
+ def repository_project
+ @project ||= Project
+ .where_full_path_in(nodes.first(LEVELS_SUPPORTED))
+ .first
+ end
+
+ def repository_name
+ return unless has_project?
+
+ @path.remove(%r(^#{Regexp.escape(repository_project.full_path)}/?))
+ end
+
+ def to_s
+ @path
+ end
+ end
+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..d00e6191e7e 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.path, name)
end
def path
@@ -38,9 +36,7 @@ module ContainerRegistry
end
def digest
- return @digest if defined?(@digest)
-
- @digest = client.repository_tag_digest(repository.name, name)
+ @digest ||= client.repository_tag_digest(repository.path, name)
end
def config_blob
@@ -82,7 +78,7 @@ module ContainerRegistry
def delete
return unless digest
- client.delete_repository_tag(repository.name, digest)
+ client.delete_repository_tag(repository.path, digest)
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 5e5f5ff1589..e599dd4a656 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -121,6 +121,13 @@ module Gitlab
git_reference_regex
end
+ ##
+ # Docker Distribution Registry 2.4.1 repository name rules
+ #
+ def container_repository_name_regex
+ @container_repository_regex ||= %r{\A[a-z0-9]+(?:[-._/][a-z0-9]+)*\Z}
+ end
+
def environment_name_regex
@environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
end
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
new file mode 100644
index 00000000000..464302824a8
--- /dev/null
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Projects::Registry::RepositoriesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+
+ before do
+ sign_in(user)
+ stub_container_registry_config(enabled: true)
+ end
+
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
+
+ describe 'GET index' do
+ context 'when root container repository exists' do
+ before do
+ create(:container_repository, :root, project: project)
+ end
+
+ it 'does not create root container repository' do
+ expect { go_to_index }.not_to change { ContainerRepository.all.count }
+ end
+ end
+
+ context 'when root container repository is not created' do
+ context 'when there are tags for this repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: %w[rc1 latest])
+ end
+
+ it 'successfully renders container repositories' do
+ go_to_index
+
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'creates a root container repository' do
+ expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
+ expect(ContainerRepository.first).to be_root_repository
+ end
+ end
+
+ context 'when there are no tags for this repository' do
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
+ it 'successfully renders container repositories' do
+ go_to_index
+
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'does not ensure root container repository' do
+ expect { go_to_index }.not_to change { ContainerRepository.all.count }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user does not have access to registry' do
+ describe 'GET index' do
+ it 'responds with 404' do
+ go_to_index
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'does not ensure root container repository' do
+ expect { go_to_index }.not_to change { ContainerRepository.all.count }
+ end
+ end
+ end
+
+ def go_to_index
+ get :index, namespace_id: project.namespace,
+ project_id: project
+ end
+end
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
new file mode 100644
index 00000000000..3fcad9fd4b3
--- /dev/null
+++ b/spec/factories/container_repositories.rb
@@ -0,0 +1,33 @@
+FactoryGirl.define do
+ factory :container_repository do
+ name 'test_container_image'
+ project
+
+ transient do
+ tags []
+ end
+
+ trait :root do
+ name ''
+ end
+
+ after(:build) do |repository, evaluator|
+ next if evaluator.tags.to_a.none?
+
+ allow(repository.client)
+ .to receive(:repository_tags)
+ .and_return({
+ 'name' => repository.path,
+ 'tags' => evaluator.tags
+ })
+
+ evaluator.tags.each do |tag|
+ allow(repository.client)
+ .to receive(:repository_tag_digest)
+ .with(repository.path, tag)
+ .and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3' \
+ '72b088dac5b6d7ad7d49cd620d85cf72a15')
+ end
+ end
+ end
+end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 203e55a36f2..b86609e07c5 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -1,45 +1,61 @@
require 'spec_helper'
describe "Container Registry" do
+ let(:user) { create(:user) }
let(:project) { create(:empty_project) }
- let(:repository) { project.container_registry_repository }
- let(:tag_name) { 'latest' }
- let(:tags) { [tag_name] }
+
+ let(:container_repository) do
+ create(:container_repository, name: 'my/image')
+ end
before do
- login_as(:user)
- project.team << [@user, :developer]
- stub_container_registry_tags(*tags)
+ login_as(user)
+ project.add_developer(user)
stub_container_registry_config(enabled: true)
- allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ stub_container_registry_tags(repository: :any, tags: [])
end
- describe 'GET /:project/container_registry' do
+ context 'when there are no image repositories' do
+ scenario 'user visits container registry main page' do
+ visit_container_registry
+
+ expect(page).to have_content 'No container image repositories'
+ end
+ end
+
+ context 'when there are image repositories' do
before do
- visit namespace_project_container_registry_index_path(project.namespace, project)
+ stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest])
+ project.container_repositories << container_repository
end
- context 'when no tags' do
- let(:tags) { [] }
+ scenario 'user wants to see multi-level container repository' do
+ visit_container_registry
- it { expect(page).to have_content('No images in Container Registry for this project') }
+ expect(page).to have_content('my/image')
end
- context 'when there are tags' do
- it { expect(page).to have_content(tag_name) }
- it { expect(page).to have_content('d7a513a66') }
- end
- end
+ scenario 'user removes entire container repository' do
+ visit_container_registry
- describe 'DELETE /:project/container_registry/tag' do
- before do
- visit namespace_project_container_registry_index_path(project.namespace, project)
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(true)
+
+ click_on 'Remove repository'
end
- it do
- expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
+ scenario 'user removes a specific tag from container repository' do
+ visit_container_registry
- click_on 'Remove'
+ expect_any_instance_of(ContainerRegistry::Tag)
+ .to receive(:delete).and_return(true)
+
+ click_on 'Remove tag'
end
end
+
+ def visit_container_registry
+ visit namespace_project_container_registry_index_path(
+ project.namespace, project)
+ end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 1a66d1a6a1e..6ecdc8cbb71 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_repository) { create(:container_repository) }
+
before do
- stub_container_registry_tags('latest')
+ stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true)
+ project.container_repositories << container_repository
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..a8fc0624588 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_repository) { create(:container_repository) }
+
before do
- stub_container_registry_tags('latest')
+ stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true)
+ project.container_repositories << container_repository
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..c4d2f50ca14 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_repository) { create(:container_repository) }
+
before do
- stub_container_registry_tags('latest')
+ stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true)
+ project.container_repositories << container_repository
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..f06e5fd54a2 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -1,110 +1,121 @@
require 'spec_helper'
describe ContainerRegistry::Blob do
- let(:digest) { 'sha256:0123456789012345' }
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:empty_project, path: 'test', group: group) }
+
+ let(:repository) do
+ create(:container_repository, name: 'image',
+ tags: %w[latest rc1],
+ project: project)
+ end
+
let(:config) do
- {
- 'digest' => digest,
+ { 'digest' => 'sha256:0123456789012345',
'mediaType' => 'binary',
- 'size' => 1000
- }
+ 'size' => 1000 }
+ end
+
+ let(:blob) { described_class.new(repository, config) }
+
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab',
+ host_port: 'registry.gitlab')
end
- let(:token) { 'authorization-token' }
-
- let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) }
- 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') }
+ describe '#path' do
+ it 'returns a valid path to the blob' do
+ expect(blob.path).to eq('group/test/image@sha256:0123456789012345')
+ end
end
- context '#digest' do
- subject { blob.digest }
-
- it { is_expected.to eq(digest) }
+ describe '#digest' do
+ it 'return correct digest value' do
+ expect(blob.digest).to eq 'sha256:0123456789012345'
+ end
end
- context '#type' do
- subject { blob.type }
-
- it { is_expected.to eq('binary') }
+ describe '#type' do
+ it 'returns a correct type' do
+ expect(blob.type).to eq 'binary'
+ end
end
- context '#revision' do
- subject { blob.revision }
-
- it { is_expected.to eq('0123456789012345') }
+ describe '#revision' do
+ it 'returns a correct blob SHA' do
+ expect(blob.revision).to eq '0123456789012345'
+ end
end
- context '#short_revision' do
- subject { blob.short_revision }
-
- it { is_expected.to eq('012345678') }
+ describe '#short_revision' do
+ it 'return a short SHA' do
+ expect(blob.short_revision).to eq '012345678'
+ end
end
- context '#delete' do
+ describe '#delete' do
before do
- stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
- to_return(status: 200)
+ stub_request(:delete, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345')
+ .to_return(status: 200)
end
- subject { blob.delete }
-
- it { is_expected.to be_truthy }
+ it 'returns true when blob has been successfuly deleted' do
+ expect(blob.delete).to be_truthy
+ end
end
- context '#data' do
- let(:data) { '{"key":"value"}' }
-
- subject { blob.data }
-
+ describe '#data' do
context 'when locally stored' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345').
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
- body: data)
+ body: '{"key":"value"}')
end
- it { is_expected.to eq(data) }
+ it 'returns a correct blob data' do
+ expect(blob.data).to eq '{"key":"value"}'
+ end
end
context 'when externally stored' do
+ let(:location) { 'http://external.com/blob/file' }
+
before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
- with(headers: { 'Authorization' => "bearer #{token}" }).
- to_return(
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345')
+ .with(headers: { 'Authorization' => 'bearer token' })
+ .to_return(
status: 307,
headers: { 'Location' => location })
end
context 'for a valid address' do
- let(:location) { 'http://external.com/blob/file' }
-
before do
stub_request(:get, location).
with(headers: { 'Authorization' => nil }).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
- body: data)
+ body: '{"key":"value"}')
end
- it { is_expected.to eq(data) }
+ it 'returns correct data' do
+ expect(blob.data).to eq '{"key":"value"}'
+ end
end
context 'for invalid file' do
let(:location) { 'file:///etc/passwd' }
- it { expect{ subject }.to raise_error(ArgumentError, 'invalid address') }
+ it 'raises an error' do
+ expect { blob.data }.to raise_error(ArgumentError, 'invalid address')
+ end
end
end
end
diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb
new file mode 100644
index 00000000000..b9c4572c269
--- /dev/null
+++ b/spec/lib/container_registry/path_spec.rb
@@ -0,0 +1,212 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Path do
+ subject { described_class.new(path) }
+
+ describe '#components' do
+ let(:path) { 'path/to/some/project' }
+
+ it 'splits components by a forward slash' do
+ expect(subject.components).to eq %w[path to some project]
+ end
+ end
+
+ describe '#nodes' do
+ context 'when repository path is valid' do
+ let(:path) { 'path/to/some/project' }
+
+ it 'return all project path like node in reverse order' do
+ expect(subject.nodes).to eq %w[path/to/some/project
+ path/to/some
+ path/to]
+ end
+ end
+
+ context 'when repository path is invalid' do
+ let(:path) { '' }
+
+ it 'rasises en error' do
+ expect { subject.nodes }
+ .to raise_error described_class::InvalidRegistryPathError
+ end
+ end
+ end
+
+ describe '#to_s' do
+ let(:path) { 'some/image' }
+
+ it 'return a string with a repository path' do
+ expect(subject.to_s).to eq path
+ end
+ end
+
+ describe '#valid?' do
+ context 'when path has less than two components' do
+ let(:path) { 'something/' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when path has more than allowed number of components' do
+ let(:path) { 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/r/s/t/u/w/y/z' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when path has invalid characters' do
+ let(:path) { 'some\path' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when path has two or more components' do
+ let(:path) { 'some/path' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when path is related to multi-level image' do
+ let(:path) { 'some/path/my/image' }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ describe '#has_repository?' do
+ context 'when project exists' do
+ let(:project) { create(:empty_project) }
+ let(:path) { "#{project.full_path}/my/image" }
+
+ context 'when path already has matching repository' do
+ before do
+ create(:container_repository, project: project, name: 'my/image')
+ end
+
+ it { is_expected.to have_repository }
+ it { is_expected.to have_project }
+ end
+
+ context 'when path does not have matching repository' do
+ it { is_expected.not_to have_repository }
+ it { is_expected.to have_project }
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:path) { 'some/project/my/image' }
+
+ it { is_expected.not_to have_repository }
+ it { is_expected.not_to have_project }
+ end
+ end
+
+ describe '#repository_project' do
+ let(:group) { create(:group, path: 'some_group') }
+
+ context 'when project for given path exists' do
+ let(:path) { 'some_group/some_project' }
+
+ before do
+ create(:empty_project, group: group, name: 'some_project')
+ create(:empty_project, name: 'some_project')
+ end
+
+ it 'returns a correct project' do
+ expect(subject.repository_project.group).to eq group
+ end
+ end
+
+ context 'when project for given path does not exist' do
+ let(:path) { 'not/matching' }
+
+ it 'returns nil' do
+ expect(subject.repository_project).to be_nil
+ end
+ end
+
+ context 'when matching multi-level path' do
+ let(:project) do
+ create(:empty_project, group: group, name: 'some_project')
+ end
+
+ context 'when using the zero-level path' do
+ let(:path) { project.full_path }
+
+ it 'supports zero-level path' do
+ expect(subject.repository_project).to eq project
+ end
+ end
+
+ context 'when using first-level path' do
+ let(:path) { "#{project.full_path}/repository" }
+
+ it 'supports first-level path' do
+ expect(subject.repository_project).to eq project
+ end
+ end
+
+ context 'when using second-level path' do
+ let(:path) { "#{project.full_path}/repository/name" }
+
+ it 'supports second-level path' do
+ expect(subject.repository_project).to eq project
+ end
+ end
+
+ context 'when using too deep nesting in the path' do
+ let(:path) { "#{project.full_path}/repository/name/invalid" }
+
+ it 'does not support three-levels of nesting' do
+ expect(subject.repository_project).to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#repository_name' do
+ context 'when project does not exist' do
+ let(:path) { 'some/name' }
+
+ it 'returns nil' do
+ expect(subject.repository_name).to be_nil
+ end
+ end
+
+ context 'when project exists' do
+ let(:group) { create(:group, path: 'some_group') }
+
+ let(:project) do
+ create(:empty_project, group: group, name: 'some_project')
+ end
+
+ before do
+ allow(path).to receive(:repository_project)
+ .and_return(project)
+ end
+
+ context 'when project path equal repository path' do
+ let(:path) { 'some_group/some_project' }
+
+ it 'returns an empty string' do
+ expect(subject.repository_name).to eq ''
+ end
+ end
+
+ context 'when repository path has one additional level' do
+ let(:path) { 'some_group/some_project/repository' }
+
+ it 'returns a correct repository name' do
+ expect(subject.repository_name).to eq 'repository'
+ end
+ end
+
+ context 'when repository path has two additional levels' do
+ let(:path) { 'some_group/some_project/repository/image' }
+
+ it 'returns a correct repository name' do
+ expect(subject.repository_name).to eq 'repository/image'
+ end
+ end
+ end
+ end
+end
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..bc1912d8e6c 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -1,25 +1,59 @@
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' } }
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+
+ let(:repository) do
+ create(:container_repository, name: '', project: project)
+ end
+
+ let(:headers) do
+ { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }
+ end
+
+ let(:tag) { described_class.new(repository, 'tag') }
+
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab',
+ host_port: 'registry.gitlab')
+ 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) }
- context '#path' do
- subject { tag.path }
+ describe '#path' do
+ context 'when tag belongs to zero-level repository' do
+ let(:repository) do
+ create(:container_repository, name: '',
+ tags: %w[rc1],
+ project: project)
+ end
+
+ it 'returns path to the image' do
+ expect(tag.path).to eq('group/test:tag')
+ end
+ end
+
+ context 'when tag belongs to first-level repository' do
+ let(:repository) do
+ create(:container_repository, name: 'my_image',
+ tags: %w[tag],
+ project: project)
+ end
- it { is_expected.to eq('example.com/group/test:tag') }
+ it 'returns path to the image' do
+ expect(tag.path).to eq('group/test/my_image:tag')
+ end
+ end
end
context 'manifest processing' do
context 'schema v1' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag').
with(headers: headers).
to_return(
status: 200,
@@ -56,7 +90,7 @@ describe ContainerRegistry::Tag do
context 'schema v2' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag').
with(headers: headers).
to_return(
status: 200,
@@ -93,7 +127,7 @@ describe ContainerRegistry::Tag do
context 'when locally stored' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
with(headers: { 'Accept' => 'application/octet-stream' }).
to_return(
status: 200,
@@ -105,7 +139,7 @@ describe ContainerRegistry::Tag do
context 'when externally stored' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
with(headers: { 'Accept' => 'application/octet-stream' }).
to_return(
status: 307,
@@ -123,29 +157,29 @@ describe ContainerRegistry::Tag do
end
end
- context 'manifest digest' do
+ context 'with stubbed 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' })
+ stub_request(:head, 'http://registry.gitlab/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') }
+ describe '#digest' do
+ it 'returns a correct tag digest' do
+ expect(tag.digest).to eq 'sha256:digest'
+ end
end
- context '#delete' do
+ describe '#delete' do
before do
- stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest').
- with(headers: headers).
- to_return(status: 200)
+ stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest')
+ .with(headers: headers)
+ .to_return(status: 200)
end
- subject { tag.delete }
-
- it { is_expected.to be_truthy }
+ it 'correctly deletes the tag' do
+ expect(tag.delete).to be_truthy
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 24654bf6afd..a2e3b06989e 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -116,6 +116,9 @@ merge_access_levels:
- protected_branch
push_access_levels:
- protected_branch
+container_repositories:
+- project
+- name
project:
- taggings
- base_tags
@@ -202,6 +205,7 @@ project:
- project_authorizations
- route
- statistics
+- container_repositories
- uploads
award_emoji:
- awardable
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index f876cb00fc7..8601160561f 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1348,7 +1348,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_repository_spec.rb b/spec/models/container_repository_spec.rb
new file mode 100644
index 00000000000..f7ee0b57072
--- /dev/null
+++ b/spec/models/container_repository_spec.rb
@@ -0,0 +1,209 @@
+require 'spec_helper'
+
+describe ContainerRepository do
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+
+ let(:container_repository) do
+ create(:container_repository, name: 'my_image', project: project)
+ end
+
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab',
+ host_port: 'registry.gitlab')
+
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/my_image/tags/list')
+ .with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ .to_return(
+ status: 200,
+ body: JSON.dump(tags: ['test_tag']),
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ describe 'associations' do
+ it 'belongs to the project' do
+ expect(container_repository).to belong_to(:project)
+ end
+ end
+
+ describe '#tag' do
+ it 'has a test tag' do
+ expect(container_repository.tag('test')).not_to be_nil
+ end
+ end
+
+ describe '#path' do
+ it 'returns a full path to the repository' do
+ expect(container_repository.path).to eq('group/test/my_image')
+ end
+ end
+
+ describe '#manifest' do
+ subject { container_repository.manifest }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ describe '#valid?' do
+ subject { container_repository.valid? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#tags' do
+ subject { container_repository.tags }
+
+ it { is_expected.not_to be_empty }
+ end
+
+ describe '#has_tags?' do
+ it 'has tags' do
+ expect(container_repository).to have_tags
+ end
+ end
+
+ describe '#delete_tags!' do
+ let(:container_repository) do
+ create(:container_repository, name: 'my_image',
+ tags: %w[latest rc1],
+ project: project)
+ end
+
+ context 'when action succeeds' do
+ it 'returns status that indicates success' do
+ expect(container_repository.client)
+ .to receive(:delete_repository_tag)
+ .and_return(true)
+
+ expect(container_repository.delete_tags!).to be_truthy
+ end
+ end
+
+ context 'when action fails' do
+ it 'returns status that indicates failure' do
+ expect(container_repository.client)
+ .to receive(:delete_repository_tag)
+ .and_return(false)
+
+ expect(container_repository.delete_tags!).to be_falsey
+ end
+ end
+ end
+
+ describe '#root_repository?' do
+ context 'when repository is a root repository' do
+ let(:repository) { create(:container_repository, :root) }
+
+ it 'returns true' do
+ expect(repository).to be_root_repository
+ end
+ end
+
+ context 'when repository is not a root repository' do
+ it 'returns false' do
+ expect(container_repository).not_to be_root_repository
+ end
+ end
+ end
+
+ describe '.build_from_path' do
+ let(:registry_path) do
+ ContainerRegistry::Path.new(project.full_path + '/some/image')
+ end
+
+ let(:repository) do
+ described_class.build_from_path(registry_path)
+ end
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+
+ it 'is not persisted' do
+ expect(repository).not_to be_persisted
+ end
+ end
+
+ describe '.create_from_path!' do
+ let(:repository) do
+ described_class.create_from_path!(ContainerRegistry::Path.new(path))
+ end
+
+ let(:repository_path) { ContainerRegistry::Path.new(path) }
+
+ context 'when received multi-level repository path' do
+ let(:path) { project.full_path + '/some/image' }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+ end
+
+ context 'when path is too long' do
+ let(:path) do
+ project.full_path + '/a/b/c/d/e/f/g/h/i/j/k/l/n/o/p/s/t/u/x/y/z'
+ end
+
+ it 'does not create repository and raises error' do
+ expect { repository }.to raise_error(
+ ContainerRegistry::Path::InvalidRegistryPathError)
+ end
+ end
+
+ context 'when received multi-level repository with nested groups' do
+ let(:group) { create(:group, :nested, name: 'nested') }
+ let(:path) { project.full_path + '/some/image' }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+
+ it 'has path including a nested group' do
+ expect(repository.path).to include 'nested/test/some/image'
+ end
+ end
+
+ context 'when received root repository path' do
+ let(:path) { project.full_path }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with an empty name' do
+ expect(repository.name).to be_empty
+ end
+ end
+ end
+
+ describe '.build_root_repository' do
+ let(:repository) do
+ described_class.build_root_repository(project)
+ end
+
+ it 'fabricates a root repository object' do
+ expect(repository).to be_root_repository
+ end
+
+ it 'assignes it to the correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'does not persist it' do
+ expect(repository).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index d9216112259..e406d0a16bd 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -148,18 +148,22 @@ 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_repository) { create(:container_repository) }
+
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
- create(:empty_project, namespace: @namespace)
+ create(:empty_project, namespace: @namespace, container_repositories: [container_repository])
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 'raises an error about not movable project' do
+ expect { @namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
+ end
end
context 'with subgroups' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 59a2560ca06..da1372fe761 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1157,11 +1157,12 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
-
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end
it 'renames a repository' do
+ stub_container_registry_config(enabled: false)
+
expect(gitlab_shell).to receive(:mv_repository).
ordered.
with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
@@ -1185,10 +1186,13 @@ describe Project, models: true do
project.rename_repo
end
- context 'container registry with tags' do
+ context 'container registry with images' do
+ let(:container_repository) { create(:container_repository) }
+
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+ project.container_repositories << container_repository
end
subject { project.rename_repo }
@@ -1386,38 +1390,17 @@ describe Project, models: true do
end
end
- describe '#container_registry_path_with_namespace' do
- let(:project) { create(:empty_project, path: 'PROJECT') }
-
- subject { project.container_registry_path_with_namespace }
-
- it { is_expected.not_to eq(project.path_with_namespace) }
- it { is_expected.to eq(project.path_with_namespace.downcase) }
- 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.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) }
context 'for enabled registry' do
let(:registry_settings) do
- {
- enabled: true,
- host_port: 'example.com',
- }
+ { enabled: true,
+ host_port: 'example.com' }
end
it { is_expected.not_to be_nil }
@@ -1425,9 +1408,7 @@ describe Project, models: true do
context 'for disabled registry' do
let(:registry_settings) do
- {
- enabled: false
- }
+ { enabled: false }
end
it { is_expected.to be_nil }
@@ -1437,28 +1418,60 @@ describe Project, models: true do
describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) }
- subject { project.has_container_registry_tags? }
-
- context 'for enabled registry' do
+ context 'when container registry is enabled' do
before { stub_container_registry_config(enabled: true) }
- context 'with tags' do
- before { stub_container_registry_tags('test', 'test2') }
+ context 'when tags are present for multi-level registries' do
+ before do
+ create(:container_repository, project: project, name: 'image')
+
+ stub_container_registry_tags(repository: /image/,
+ tags: %w[latest rc1])
+ end
- it { is_expected.to be_truthy }
+ it 'should have image tags' do
+ expect(project).to have_container_registry_tags
+ end
end
- context 'when no tags' do
- before { stub_container_registry_tags }
+ context 'when tags are present for root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: %w[latest rc1 pre1])
+ end
- it { is_expected.to be_falsey }
+ it 'should have image tags' do
+ expect(project).to have_container_registry_tags
+ end
+ end
+
+ context 'when there are no tags at all' do
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
+ it 'should not have image tags' do
+ expect(project).not_to have_container_registry_tags
+ end
end
end
- context 'for disabled registry' do
+ context 'when container registry is disabled' do
before { stub_container_registry_config(enabled: false) }
- it { is_expected.to be_falsey }
+ it 'should not have image tags' do
+ expect(project).not_to have_container_registry_tags
+ end
+
+ it 'should not check root repository tags' do
+ expect(project).not_to receive(:full_path)
+ expect(project).not_to have_container_registry_tags
+ end
+
+ it 'should iterate through container repositories' do
+ expect(project).to receive(:container_repositories)
+ expect(project).not_to have_container_registry_tags
+ end
end
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index b91234ddb1e..e273dfe1552 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -6,14 +6,15 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:payload) { JWT.decode(subject[:token], rsa_key).first }
+
let(:authentication_abilities) do
- [
- :read_container_image,
- :create_container_image
- ]
+ [:read_container_image, :create_container_image]
end
- subject { described_class.new(current_project, current_user, current_params).execute(authentication_abilities: authentication_abilities) }
+ subject do
+ described_class.new(current_project, current_user, current_params)
+ .execute(authentication_abilities: authentication_abilities)
+ end
before do
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
@@ -40,13 +41,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
end
- shared_examples 'a accessible' do
+ shared_examples 'an accessible' do
let(:access) do
- [{
- 'type' => 'repository',
+ [{ 'type' => 'repository',
'name' => project.path_with_namespace,
- 'actions' => actions,
- }]
+ 'actions' => actions }]
end
it_behaves_like 'a valid token'
@@ -59,19 +58,19 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
shared_examples 'a pullable' do
- it_behaves_like 'a accessible' do
+ it_behaves_like 'an accessible' do
let(:actions) { ['pull'] }
end
end
shared_examples 'a pushable' do
- it_behaves_like 'a accessible' do
+ it_behaves_like 'an accessible' do
let(:actions) { ['push'] }
end
end
shared_examples 'a pullable and pushable' do
- it_behaves_like 'a accessible' do
+ it_behaves_like 'an accessible' do
let(:actions) { %w(pull push) }
end
end
@@ -81,15 +80,30 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it { is_expected.not_to include(:token) }
end
+ shared_examples 'container repository factory' do
+ it 'creates a new container repository resource' do
+ expect { subject }
+ .to change { project.container_repositories.count }.by(1)
+ end
+ end
+
+ shared_examples 'not a container repository factory' do
+ it 'does not create a new container repository resource' do
+ expect { subject }.not_to change { ContainerRepository.count }
+ end
+ end
+
describe '#full_access_token' do
let(:project) { create(:empty_project) }
let(:token) { described_class.full_access_token(project.path_with_namespace) }
subject { { token: token } }
- it_behaves_like 'a accessible' do
+ it_behaves_like 'an accessible' do
let(:actions) { ['*'] }
end
+
+ it_behaves_like 'not a container repository factory'
end
context 'user authorization' do
@@ -110,16 +124,20 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pushable'
+ it_behaves_like 'container repository factory'
end
context 'allow reporter to pull images' do
before { project.team << [current_user, :reporter] }
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull" }
- end
+ context 'when pulling from root level repository' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
- it_behaves_like 'a pullable'
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
end
context 'return a least of privileges' do
@@ -130,6 +148,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'disallow guest to pull or push images' do
@@ -140,6 +159,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
@@ -152,6 +172,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'disallow anyone to push images' do
@@ -160,6 +181,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when repository name is invalid' do
+ let(:current_params) do
+ { scope: 'repository:invalid:push' }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
@@ -173,6 +204,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'disallow anyone to push images' do
@@ -181,6 +213,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
@@ -191,6 +224,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
end
@@ -198,11 +232,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'build authorized as user' do
let(:current_project) { create(:empty_project) }
let(:current_user) { create(:user) }
+
let(:authentication_abilities) do
- [
- :build_read_container_image,
- :build_create_container_image
- ]
+ [:build_read_container_image, :build_create_container_image]
end
before do
@@ -219,6 +251,10 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'a pullable and pushable' do
let(:project) { current_project }
end
+
+ it_behaves_like 'container repository factory' do
+ let(:project) { current_project }
+ end
end
context 'for other projects' do
@@ -231,11 +267,13 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:project) { create(:empty_project, :public) }
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
shared_examples 'pullable for being team member' do
context 'when you are not member' do
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
context 'when you are member' do
@@ -244,12 +282,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'when you are owner' do
let(:project) { create(:empty_project, namespace: current_user.namespace) }
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
end
@@ -263,6 +303,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'when you are not member' do
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
context 'when you are member' do
@@ -271,12 +312,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'when you are owner' do
let(:project) { create(:empty_project, namespace: current_user.namespace) }
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
end
end
@@ -296,12 +339,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
context 'when you are owner' do
let(:project) { create(:empty_project, :public, namespace: current_user.namespace) }
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
end
@@ -318,6 +363,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
end
end
@@ -325,6 +371,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'unauthorized' do
context 'disallow to use scope-less authentication' do
it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
end
context 'for invalid scope' do
@@ -333,6 +380,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
end
context 'for private project' do
@@ -354,6 +402,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
end
context 'when pushing' do
@@ -362,6 +411,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
end
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index b1e10f4562e..4b8589b2736 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -7,6 +7,11 @@ describe Projects::DestroyService, services: true do
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
shared_examples 'deleting the project' do
it 'deletes the project' do
expect(Project.unscoped.all).not_to include(project)
@@ -89,30 +94,64 @@ describe Projects::DestroyService, services: true do
it_behaves_like 'deleting the project with pipeline and build'
end
- context 'container registry' do
- before do
- stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
- end
+ describe 'container registry' do
+ context 'when there are regular container repositories' do
+ let(:container_repository) { create(:container_repository) }
+
+ before do
+ stub_container_registry_tags(repository: project.full_path + '/image',
+ tags: ['tag'])
+ project.container_repositories << container_repository
+ end
+
+ context 'when image repository deletion succeeds' do
+ it 'removes tags' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(true)
+
+ destroy_project(project, user)
+ end
+ end
- context 'tags deletion succeeds' do
- it do
- expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
+ context 'when image repository deletion fails' do
+ it 'raises an exception' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(false)
- destroy_project(project, user, {})
+ expect{ destroy_project(project, user) }
+ .to raise_error(ActiveRecord::RecordNotDestroyed)
+ end
end
end
- context 'tags deletion fails' do
- before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) }
+ context 'when there are tags for legacy root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: ['tag'])
+ end
+
+ context 'when image repository tags deletion succeeds' do
+ it 'removes tags' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(true)
- subject { destroy_project(project, user, {}) }
+ destroy_project(project, user)
+ end
+ end
+
+ context 'when image repository tags deletion fails' do
+ it 'raises an exception' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(false)
- it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) }
+ expect { destroy_project(project, user) }
+ .to raise_error(Projects::DestroyService::DestroyError)
+ end
+ end
end
end
- def destroy_project(project, user, params)
+ def destroy_project(project, user, params = {})
if async
Projects::DestroyService.new(project, user, params).async_execute
else
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index f8187fefc14..29ccce59c53 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_repository) { create(:container_repository) }
+
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+ project.container_repositories << container_repository
end
subject { transfer_project(project, user, group) }
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index a01ef576234..ded2d593059 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -27,23 +27,40 @@ module StubGitlabCalls
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')
+ 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.parse(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')
- )
+ def stub_container_registry_tags(repository: :any, tags:)
+ repository = any_args if repository == :any
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_tags).with(repository)
+ .and_return({ 'tags' => tags })
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_manifest).with(repository)
+ .and_return(stub_container_registry_tag_manifest)
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:blob).with(repository)
+ .and_return(stub_container_registry_blob)
end
private
+ def stub_container_registry_tag_manifest
+ fixture_path = 'spec/fixtures/container_registry/tag_manifest.json'
+
+ JSON.parse(File.read(Rails.root + fixture_path))
+ end
+
+ def stub_container_registry_blob
+ fixture_path = 'spec/fixtures/container_registry/config_blob.json'
+
+ File.read(Rails.root + fixture_path)
+ end
+
def gitlab_url
Gitlab.config.gitlab.url
end