Commit d6caa9d7 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/multi-level-container-registry-images' into 'master'

Multi-level container registry images

Closes #17801

See merge request !10109
parents 173384f8 9362f593
...@@ -30,6 +30,7 @@ eslint-report.html ...@@ -30,6 +30,7 @@ eslint-report.html
/config/unicorn.rb /config/unicorn.rb
/config/secrets.yml /config/secrets.yml
/config/sidekiq.yml /config/sidekiq.yml
/config/registry.key
/coverage/* /coverage/*
/coverage-javascript/ /coverage-javascript/
/db/*.sqlite3 /db/*.sqlite3
......
/**
* Container Registry
*/
.container-image {
border-bottom: 1px solid $white-normal;
}
.container-image-head {
padding: 0 16px;
line-height: 4em;
}
.table.tags {
margin-bottom: 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
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
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
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
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
...@@ -159,6 +159,7 @@ class Project < ActiveRecord::Base ...@@ -159,6 +159,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy
has_many :commit_statuses, dependent: :destroy has_many :commit_statuses, dependent: :destroy
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline' has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
...@@ -406,32 +407,15 @@ class Project < ActiveRecord::Base ...@@ -406,32 +407,15 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self) @repository ||= Repository.new(path_with_namespace, self)
end end
def container_registry_path_with_namespace def container_registry_url
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
if Gitlab.config.registry.enabled 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
end end
def has_container_registry_tags? def has_container_registry_tags?
return unless container_registry_repository container_repositories.to_a.any?(&:has_tags?) ||
has_root_container_repository_tags?
container_registry_repository.tags.any?
end end
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
...@@ -922,10 +906,10 @@ class Project < ActiveRecord::Base ...@@ -922,10 +906,10 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace) expire_caches_before_rename(old_path_with_namespace)
if has_container_registry_tags? 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 # we currently doesn't support renaming repository if it contains images in container registry
raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
end end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
...@@ -1272,7 +1256,7 @@ class Project < ActiveRecord::Base ...@@ -1272,7 +1256,7 @@ class Project < ActiveRecord::Base
] ]
if container_registry_enabled? 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 end
variables variables
...@@ -1405,4 +1389,15 @@ class Project < ActiveRecord::Base ...@@ -1405,4 +1389,15 @@ class Project < ActiveRecord::Base
Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
end 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 end
...@@ -17,6 +17,7 @@ module Auth ...@@ -17,6 +17,7 @@ module Auth
end end
def self.full_access_token(*names) def self.full_access_token(*names)
names = names.flatten
registry = Gitlab.config.registry registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key) token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer token.issuer = registry.issuer
...@@ -37,13 +38,13 @@ module Auth ...@@ -37,13 +38,13 @@ module Auth
private private
def authorized_token(*accesses) def authorized_token(*accesses)
token = JSONWebToken::RSAToken.new(registry.key) JSONWebToken::RSAToken.new(registry.key).tap do |token|
token.issuer = registry.issuer token.issuer = registry.issuer
token.audience = params[:service] token.audience = params[:service]
token.subject = current_user.try(:username) token.subject = current_user.try(:username)
token.expire_time = self.class.token_expire_at token.expire_time = self.class.token_expire_at
token[:access] = accesses.compact token[:access] = accesses.compact
token end
end end
def scope def scope
...@@ -55,20 +56,43 @@ module Auth ...@@ -55,20 +56,43 @@ module Auth
def process_scope(scope) def process_scope(scope)
type, name, actions = scope.split(':', 3) type, name, actions = scope.split(':', 3)
actions = actions.split(',') actions = actions.split(',')
path = ContainerRegistry::Path.new(name)
return unless type == 'repository' return unless type == 'repository'
process_repository_access(type, name, actions) process_repository_access(type, path, actions)
end end
def process_repository_access(type, name, actions) def process_repository_access(type, path, actions)
requested_project = Project.find_by_full_path(name) return unless path.valid?
requested_project = path.repository_project
return unless requested_project return unless requested_project
actions = actions.select do |action| actions = actions.select do |action|
can_access?(requested_project, action) can_access?(requested_project, action)
end 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 end
def can_access?(requested_project, requested_action) def can_access?(requested_project, requested_action)
...@@ -101,6 +125,11 @@ module Auth ...@@ -101,6 +125,11 @@ module Auth
can?(current_user, :read_container_image, requested_project) can?(current_user, :read_container_image, requested_project)
end 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) def build_can_push?(requested_project)
# Build can push only to the project from which it originates # Build can push only to the project from which it originates
has_authentication_ability?(:build_create_container_image) && has_authentication_ability?(:build_create_container_image) &&
...@@ -113,14 +142,11 @@ module Auth ...@@ -113,14 +142,11 @@ module Auth
end end
def error(code, status:, message: '') def error(code, status:, message: '')
{ { errors: [{ code: code, message: message }], http_status: status }
errors: [{ code: code, message: message }],
http_status: status
}
end end
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
(@authentication_abilities || []).include?(capability) @authentication_abilities.to_a.include?(capability)
end end
end end
end end
...@@ -31,16 +31,16 @@ module Projects ...@@ -31,16 +31,16 @@ module Projects
project.team.truncate project.team.truncate
project.destroy! project.destroy!
unless remove_registry_tags unless remove_legacy_registry_tags
raise_error('Failed to remove project container registry. Please try again or contact administrator') raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end end
unless remove_repository(repo_path) unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator') raise_error('Failed to remove project repository. Please try again or contact administrator.')
end end
unless remove_repository(wiki_path) 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
end end
...@@ -68,10 +68,16 @@ module Projects ...@@ -68,10 +68,16 @@ module Projects
end end
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 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 end
def raise_error(message) def raise_error(message)
......
.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.
...@@ -25,5 +25,9 @@ ...@@ -25,5 +25,9 @@
- if can?(current_user, :update_container_image, @project) - if can?(current_user, :update_container_image, @project)
%td.content %td.content
.controls.hidden-xs.pull-right .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_registry_repository_tag_path(@project.namespace, @project, tag.repository, tag.name),
= icon("trash cred") 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')
...@@ -15,25 +15,12 @@ ...@@ -15,25 +15,12 @@
%br %br
Then you are free to create and upload a container image with build and push commands: Then you are free to create and upload a container image with build and push commands:
%pre %pre
docker build -t #{escape_once(@project.container_registry_repository_url)} . docker build -t #{escape_once(@project.container_registry_url)}/image .
%br %br
docker push #{escape_once(@project.container_registry_repository_url)} docker push #{escape_once(@project.container_registry_url)}/image
- if @tags.blank? - if @images.blank?
%li .nothing-here-block No container image repositories in Container Registry for this project.
.nothing-here-block No images in Container Registry for this project.
- else - else
.table-holder = render partial: 'image', collection: @images
%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
---
title: Add support for multi-level container image repository names
merge_request: 10109
author: André Guede
...@@ -221,7 +221,15 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -221,7 +221,15 @@ constraints(ProjectUrlConstrainer.new) do
end end
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 resources :milestones, constraints: { id: /\d+/ } do
member do member do
......
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
...@@ -323,6 +323,16 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -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 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| create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false t.integer "deploy_key_id", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
...@@ -1304,6 +1314,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -1304,6 +1314,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_foreign_key "boards", "projects" add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade 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 "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 "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
...@@ -299,8 +299,8 @@ could look like: ...@@ -299,8 +299,8 @@ could look like:
stage: build stage: build
script: script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- docker build -t registry.example.com/group/project:latest . - docker build -t registry.example.com/group/project/image:latest .
- docker push registry.example.com/group/project: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 You have to use the special `gitlab-ci-token` user created for you in order to
...@@ -350,8 +350,8 @@ stages: ...@@ -350,8 +350,8 @@ stages:
- deploy - deploy
variables: variables:
CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_COMMIT_REF_NAME 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:latest CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
before_script: before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need - 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 to pass a personal access token instead of your password in order to login to
GitLab's Container Registry. 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 With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images. 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 ...@@ -54,18 +55,25 @@ sure that you are using the Registry URL with the namespace and project name
that is hosted on GitLab: that is hosted on GitLab:
``` ```
docker build -t registry.example.com/group/project . docker build -t registry.example.com/group/project/image .
docker push registry.example.com/group/project docker push registry.example.com/group/project/image
``` ```
Your image will be named after the following scheme: 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 GitLab supports up to three levels of image repository names.
using tags.
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 ## Use images from GitLab Container Registry
...@@ -73,7 +81,7 @@ To download and run a container from images hosted in GitLab Container Registry, ...@@ -73,7 +81,7 @@ To download and run a container from images hosted in GitLab Container Registry,
use `docker run`: 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 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 ...@@ -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: 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 dc5e59c14160: Pushing [==================================================>] 14.85 kB
03c20c1a019a: Pushing [==================================================>] 2.048 kB 03c20c1a019a: Pushing [==================================================>] 2.048 kB
a08f14ef632e: 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: ...@@ -229,7 +237,7 @@ a container image. You may need to run as root to do this. For example:
```sh ```sh
docker login s3-testing.myregistry.com:4567 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: In the example above, we see the following trace on the mitmproxy window:
......
...@@ -38,11 +38,11 @@ module ContainerRegistry ...@@ -38,11 +38,11 @@ module ContainerRegistry
end end
def delete def delete
client.delete_blob(repository.name, digest) client.delete_blob(repository.path, digest)
end end
def data def data
@data ||= client.blob(repository.name, digest, type) @data ||= client.blob(repository.path, digest, type)
end end
end end
end end
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
...@@ -8,10 +8,6 @@ module ContainerRegistry ...@@ -8,10 +8,6 @@ module ContainerRegistry
@client = ContainerRegistry::Client.new(uri, options) @client = ContainerRegistry::Client.new(uri, options)
end end
def repository(name)
ContainerRegistry::Repository.new(self, name)
end
private private
def default_path def default_path
......
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
...@@ -22,9 +22,7 @@ module ContainerRegistry ...@@ -22,9 +22,7 @@ module ContainerRegistry
end end
def manifest def manifest
return @manifest if defined?(@manifest) @manifest ||= client.repository_manifest(repository.path, name)
@manifest = client.repository_manifest(repository.name, name)
end end
def path def path
...@@ -38,9 +36,7 @@ module ContainerRegistry ...@@ -38,9 +36,7 @@ module ContainerRegistry
end end
def digest def digest
return @digest if defined?(@digest) @digest ||= client.repository_tag_digest(repository.path, name)
@digest = client.repository_tag_digest(repository.name, name)
end end
def config_blob def config_blob
...@@ -82,7 +78,7 @@ module ContainerRegistry ...@@ -82,7 +78,7 @@ module ContainerRegistry
def delete def delete
return unless digest return unless digest
client.delete_repository_tag(repository.name, digest) client.delete_repository_tag(repository.path, digest)
end end
end end
end end
...@@ -121,6 +121,13 @@ module Gitlab ...@@ -121,6 +121,13 @@ module Gitlab
git_reference_regex git_reference_regex
end 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 def environment_name_regex
@environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze @environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
end end
......
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
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
require 'spec_helper' require 'spec_helper'
describe "Container Registry" do describe "Container Registry" do
let(:user) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:repository) { project.container_registry_repository }
let(:tag_name) { 'latest' }
let(:tags) { [tag_name] }
before do let(:container_repository) do
login_as(:user) create(:container_repository, name: 'my/image')
project.team << [@user, :developer]
stub_container_registry_tags(*tags)
stub_container_registry_config(enabled: true)
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
end end
describe 'GET /:project/container_registry' do
before do before do
visit namespace_project_container_registry_index_path(project.namespace, project) login_as(user)
project.add_developer(user)
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: [])
end end
context 'when no tags' do context 'when there are no image repositories' do
let(:tags) { [] } scenario 'user visits container registry main page' do
visit_container_registry
it { expect(page).to have_content('No images in Container Registry for this project') } expect(page).to have_content 'No container image repositories'
end
end end
context 'when there are tags' do context 'when there are image repositories' do
it { expect(page).to have_content(tag_name) } before do
it { expect(page).to have_content('d7a513a66') } stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest])
project.container_repositories << container_repository
end end
scenario 'user wants to see multi-level container repository' do
visit_container_registry
expect(page).to have_content('my/image')
end end
describe 'DELETE /:project/container_registry/tag' do scenario 'user removes entire container repository' do
before do visit_container_registry
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 end
it do scenario 'user removes a specific tag from container repository' do
expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) visit_container_registry
click_on 'Remove' expect_any_instance_of(ContainerRegistry::Tag)
.to receive(:delete).and_return(true)
click_on 'Remove tag'
end
end end
def visit_container_registry
visit namespace_project_container_registry_index_path(
project.namespace, project)
end end
end end
...@@ -443,9 +443,12 @@ describe "Internal Project Access", feature: true do ...@@ -443,9 +443,12 @@ describe "Internal Project Access", feature: true do
end end
describe "GET /:project_path/container_registry" do describe "GET /:project_path/container_registry" do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_tags('latest') stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
project.container_repositories << container_repository
end end
subject { namespace_project_container_registry_index_path(project.namespace, project) } subject { namespace_project_container_registry_index_path(project.namespace, project) }
......
...@@ -432,9 +432,12 @@ describe "Private Project Access", feature: true do ...@@ -432,9 +432,12 @@ describe "Private Project Access", feature: true do
end end
describe "GET /:project_path/container_registry" do describe "GET /:project_path/container_registry" do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_tags('latest') stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
project.container_repositories << container_repository
end end
subject { namespace_project_container_registry_index_path(project.namespace, project) } subject { namespace_project_container_registry_index_path(project.namespace, project) }
......
...@@ -443,9 +443,12 @@ describe "Public Project Access", feature: true do ...@@ -443,9 +443,12 @@ describe "Public Project Access", feature: true do
end end
describe "GET /:project_path/container_registry" do describe "GET /:project_path/container_registry" do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_tags('latest') stub_container_registry_tags(repository: :any, tags: ['latest'])
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
project.container_repositories << container_repository
end end
subject { namespace_project_container_registry_index_path(project.namespace, project) } subject { namespace_project_container_registry_index_path(project.namespace, project) }
......
require 'spec_helper' require 'spec_helper'
describe ContainerRegistry::Blob do 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 let(:config) do
{ { 'digest' => 'sha256:0123456789012345',
'digest' => digest,
'mediaType' => 'binary', 'mediaType' => 'binary',
'size' => 1000 'size' => 1000 }
}
end end
let(:token) { 'authorization-token' }
let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) } let(:blob) { described_class.new(repository, config) }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) } before do
stub_container_registry_config(enabled: true,
api_url: 'http://registry.gitlab',
host_port: 'registry.gitlab')
end
it { expect(blob).to respond_to(:repository) } it { expect(blob).to respond_to(:repository) }
it { expect(blob).to delegate_method(:registry).to(:repository) } it { expect(blob).to delegate_method(:registry).to(:repository) }
it { expect(blob).to delegate_method(:client).to(:repository) } it { expect(blob).to delegate_method(:client).to(:repository) }
context '#path' do describe '#path' do
subject { blob.path } it 'returns a valid path to the blob' do
expect(blob.path).to eq('group/test/image@sha256:0123456789012345')
it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') }
end end
context '#digest' do
subject { blob.digest }
it { is_expected.to eq(digest) }
end end
context '#type' do describe '#digest' do
subject { blob.type } it 'return correct digest value' do
expect(blob.digest).to eq 'sha256:0123456789012345'
it { is_expected.to eq('binary') } end
end end
context '#revision' do describe '#type' do
subject { blob.revision } it 'returns a correct type' do
expect(blob.type).to eq 'binary'
it { is_expected.to eq('0123456789012345') } end
end end
context '#short_revision' do describe '#revision' do
subject { blob.short_revision } it 'returns a correct blob SHA' do
expect(blob.revision).to eq '0123456789012345'
end
end
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 end
context '#delete' do describe '#delete' do
before do before do
stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). stub_request(:delete, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345')
to_return(status: 200) .to_return(status: 200)
end end
subject { blob.delete } it 'returns true when blob has been successfuly deleted' do
expect(blob.delete).to be_truthy
it { is_expected.to be_truthy } end
end end
context '#data' do describe '#data' do
let(:data) { '{"key":"value"}' }
subject { blob.data }
context 'when locally stored' do context 'when locally stored' do
before 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( to_return(
status: 200, status: 200,
headers: { 'Content-Type' => 'application/json' }, headers: { 'Content-Type' => 'application/json' },
body: data) body: '{"key":"value"}')
end end
it { is_expected.to eq(data) } it 'returns a correct blob data' do
expect(blob.data).to eq '{"key":"value"}'
end
end end
context 'when externally stored' do context 'when externally stored' do
let(:location) { 'http://external.com/blob/file' }
before 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')
with(headers: { 'Authorization' => "bearer #{token}" }). .with(headers: { 'Authorization' => 'bearer token' })
to_return( .to_return(
status: 307, status: 307,
headers: { 'Location' => location }) headers: { 'Location' => location })
end end
context 'for a valid address' do context 'for a valid address' do
let(:location) { 'http://external.com/blob/file' }
before do before do
stub_request(:get, location). stub_request(:get, location).
with(headers: { 'Authorization' => nil }). with(headers: { 'Authorization' => nil }).
to_return( to_return(
status: 200, status: 200,
headers: { 'Content-Type' => 'application/json' }, headers: { 'Content-Type' => 'application/json' },
body: data) body: '{"key":"value"}')
end end
it { is_expected.to eq(data) } it 'returns correct data' do
expect(blob.data).to eq '{"key":"value"}'
end
end end
context 'for invalid file' do context 'for invalid file' do
let(:location) { 'file:///etc/passwd' } 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 end
end end
......
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
...@@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do ...@@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do
it { is_expected.to respond_to(:uri) } it { is_expected.to respond_to(:uri) }
it { is_expected.to respond_to(:path) } 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 context '#path' do
subject { registry.path } subject { registry.path }
......
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
require 'spec_helper' require 'spec_helper'
describe ContainerRegistry::Tag do describe ContainerRegistry::Tag do
let(:registry) { ContainerRegistry::Registry.new('http://example.com') } let(:group) { create(:group, name: 'group') }
let(:repository) { registry.repository('group/test') } let(:project) { create(:project, path: 'test', group: group) }
let(:tag) { repository.tag('tag') }
let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } 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 respond_to(:repository) }
it { expect(tag).to delegate_method(:registry).to(:repository) } it { expect(tag).to delegate_method(:registry).to(:repository) }
it { expect(tag).to delegate_method(:client).to(:repository) } it { expect(tag).to delegate_method(:client).to(:repository) }
context '#path' do describe '#path' do
subject { tag.path } 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 end
context 'manifest processing' do context 'manifest processing' do
context 'schema v1' do context 'schema v1' do
before 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). with(headers: headers).
to_return( to_return(
status: 200, status: 200,
...@@ -56,7 +90,7 @@ describe ContainerRegistry::Tag do ...@@ -56,7 +90,7 @@ describe ContainerRegistry::Tag do
context 'schema v2' do context 'schema v2' do
before 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). with(headers: headers).
to_return( to_return(
status: 200, status: 200,
...@@ -93,7 +127,7 @@ describe ContainerRegistry::Tag do ...@@ -93,7 +127,7 @@ describe ContainerRegistry::Tag do
context 'when locally stored' do context 'when locally stored' do
before 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' }). with(headers: { 'Accept' => 'application/octet-stream' }).
to_return( to_return(
status: 200, status: 200,
...@@ -105,7 +139,7 @@ describe ContainerRegistry::Tag do ...@@ -105,7 +139,7 @@ describe ContainerRegistry::Tag do
context 'when externally stored' do context 'when externally stored' do
before 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' }). with(headers: { 'Accept' => 'application/octet-stream' }).
to_return( to_return(
status: 307, status: 307,
...@@ -123,29 +157,29 @@ describe ContainerRegistry::Tag do ...@@ -123,29 +157,29 @@ describe ContainerRegistry::Tag do
end end
end end
context 'manifest digest' do context 'with stubbed digest' do
before do before do
stub_request(:head, 'http://example.com/v2/group/test/manifests/tag'). stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
with(headers: headers). .with(headers: headers)
to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) .to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
end end
context '#digest' do describe '#digest' do
subject { tag.digest } it 'returns a correct tag digest' do
expect(tag.digest).to eq 'sha256:digest'
it { is_expected.to eq('sha256:digest') } end
end end
context '#delete' do describe '#delete' do
before do before do
stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest'). stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest')
with(headers: headers). .with(headers: headers)
to_return(status: 200) .to_return(status: 200)
end end
subject { tag.delete } it 'correctly deletes the tag' do
expect(tag.delete).to be_truthy
it { is_expected.to be_truthy } end
end end
end end
end end
...@@ -116,6 +116,9 @@ merge_access_levels: ...@@ -116,6 +116,9 @@ merge_access_levels:
- protected_branch - protected_branch
push_access_levels: push_access_levels:
- protected_branch - protected_branch
container_repositories:
- project
- name
project: project:
- taggings - taggings
- base_tags - base_tags
...@@ -202,6 +205,7 @@ project: ...@@ -202,6 +205,7 @@ project:
- project_authorizations - project_authorizations
- route - route
- statistics - statistics
- container_repositories
- uploads - uploads
award_emoji: award_emoji:
- awardable - awardable
......
...@@ -1348,7 +1348,7 @@ describe Ci::Build, :models do ...@@ -1348,7 +1348,7 @@ describe Ci::Build, :models do
{ key: 'CI_REGISTRY', value: 'registry.example.com', public: true } { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
end end
let(:ci_registry_image) do 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 end
context 'and is disabled for project' do context 'and is disabled for project' do
......
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
...@@ -148,18 +148,22 @@ describe Namespace, models: true do ...@@ -148,18 +148,22 @@ describe Namespace, models: true do
expect(@namespace.move_dir).to be_truthy expect(@namespace.move_dir).to be_truthy
end 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 before do
stub_container_registry_config(enabled: true) 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_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return('new_path') allow(@namespace).to receive(:path).and_return('new_path')
end 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 end
context 'with subgroups' do context 'with subgroups' do
......
...@@ -1157,11 +1157,12 @@ describe Project, models: true do ...@@ -1157,11 +1157,12 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier. # call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(project).to receive(:previous_changes).and_return('path' => ['foo']) allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end end
it 'renames a repository' do it 'renames a repository' do
stub_container_registry_config(enabled: false)
expect(gitlab_shell).to receive(:mv_repository). expect(gitlab_shell).to receive(:mv_repository).
ordered. ordered.
with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}"). with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
...@@ -1185,10 +1186,13 @@ describe Project, models: true do ...@@ -1185,10 +1186,13 @@ describe Project, models: true do
project.rename_repo project.rename_repo
end end
context 'container registry with tags' do context 'container registry with images' do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_config(enabled: true) 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 end
subject { project.rename_repo } subject { project.rename_repo }
...@@ -1386,38 +1390,17 @@ describe Project, models: true do ...@@ -1386,38 +1390,17 @@ describe Project, models: true do
end end
end end
describe '#container_registry_path_with_namespace' do describe '#container_registry_url' 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) } let(:project) { create(:empty_project) }
before { stub_container_registry_config(enabled: true) } subject { project.container_registry_url }
subject { project.container_registry_repository }
it { is_expected.not_to be_nil }
end
describe '#container_registry_repository_url' do
let(:project) { create(:empty_project) }
subject { project.container_registry_repository_url }
before { stub_container_registry_config(**registry_settings) } before { stub_container_registry_config(**registry_settings) }
context 'for enabled registry' do context 'for enabled registry' do
let(:registry_settings) do let(:registry_settings) do
{ { enabled: true,
enabled: true, host_port: 'example.com' }
host_port: 'example.com',
}
end end
it { is_expected.not_to be_nil } it { is_expected.not_to be_nil }
...@@ -1425,9 +1408,7 @@ describe Project, models: true do ...@@ -1425,9 +1408,7 @@ describe Project, models: true do
context 'for disabled registry' do context 'for disabled registry' do
let(:registry_settings) do let(:registry_settings) do
{ { enabled: false }
enabled: false
}
end end
it { is_expected.to be_nil } it { is_expected.to be_nil }
...@@ -1437,28 +1418,60 @@ describe Project, models: true do ...@@ -1437,28 +1418,60 @@ describe Project, models: true do
describe '#has_container_registry_tags?' do describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
subject { project.has_container_registry_tags? } context 'when container registry is enabled' do
context 'for enabled registry' do
before { stub_container_registry_config(enabled: true) } before { stub_container_registry_config(enabled: true) }
context 'with tags' do context 'when tags are present for multi-level registries' do
before { stub_container_registry_tags('test', 'test2') } before do
create(:container_repository, project: project, name: 'image')
it { is_expected.to be_truthy } stub_container_registry_tags(repository: /image/,
tags: %w[latest rc1])
end end
context 'when no tags' do it 'should have image tags' do
before { stub_container_registry_tags } expect(project).to have_container_registry_tags
end
end
it { is_expected.to be_falsey } 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 'should have image tags' do
expect(project).to have_container_registry_tags
end end
end end
context 'for disabled registry' do 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 'when container registry is disabled' do
before { stub_container_registry_config(enabled: false) } 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
end end
......
...@@ -7,6 +7,11 @@ describe Projects::DestroyService, services: true do ...@@ -7,6 +7,11 @@ describe Projects::DestroyService, services: true do
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute 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 shared_examples 'deleting the project' do
it 'deletes the project' do it 'deletes the project' do
expect(Project.unscoped.all).not_to include(project) expect(Project.unscoped.all).not_to include(project)
...@@ -89,30 +94,64 @@ describe Projects::DestroyService, services: true do ...@@ -89,30 +94,64 @@ describe Projects::DestroyService, services: true do
it_behaves_like 'deleting the project with pipeline and build' it_behaves_like 'deleting the project with pipeline and build'
end end
context 'container registry' do describe 'container registry' do
context 'when there are regular container repositories' do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_config(enabled: true) stub_container_registry_tags(repository: project.full_path + '/image',
stub_container_registry_tags('tag') tags: ['tag'])
project.container_repositories << container_repository
end end
context 'tags deletion succeeds' do context 'when image repository deletion succeeds' do
it do it 'removes tags' do
expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(true)
destroy_project(project, user, {}) destroy_project(project, user)
end
end
context 'when image repository deletion fails' do
it 'raises an exception' do
expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(false)
expect{ destroy_project(project, user) }
.to raise_error(ActiveRecord::RecordNotDestroyed)
end
end
end end
context 'when there are tags for legacy root repository' do
before do
stub_container_registry_tags(repository: project.full_path,
tags: ['tag'])
end end
context 'tags deletion fails' do context 'when image repository tags deletion succeeds' do
before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) } 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
end end
def destroy_project(project, user, params) def destroy_project(project, user, params = {})
if async if async
Projects::DestroyService.new(project, user, params).async_execute Projects::DestroyService.new(project, user, params).async_execute
else else
......
...@@ -29,9 +29,12 @@ describe Projects::TransferService, services: true do ...@@ -29,9 +29,12 @@ describe Projects::TransferService, services: true do
end end
context 'disallow transfering of project with tags' do context 'disallow transfering of project with tags' do
let(:container_repository) { create(:container_repository) }
before do before do
stub_container_registry_config(enabled: true) 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 end
subject { transfer_project(project, user, group) } subject { transfer_project(project, user, group) }
......
...@@ -27,23 +27,40 @@ module StubGitlabCalls ...@@ -27,23 +27,40 @@ module StubGitlabCalls
def stub_container_registry_config(registry_settings) def stub_container_registry_config(registry_settings)
allow(Gitlab.config.registry).to receive_messages(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 end
def stub_container_registry_tags(*tags) def stub_container_registry_tags(repository: :any, tags:)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return( repository = any_args if repository == :any
{ "tags" => tags }
) allow_any_instance_of(ContainerRegistry::Client)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return( .to receive(:repository_tags).with(repository)
JSON.parse(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json')) .and_return({ 'tags' => tags })
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return( allow_any_instance_of(ContainerRegistry::Client)
File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json') .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 end
private 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 def gitlab_url
Gitlab.config.gitlab.url Gitlab.config.gitlab.url
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment