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
/config/unicorn.rb
/config/secrets.yml
/config/sidekiq.yml
/config/registry.key
/coverage/*
/coverage-javascript/
/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
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
......@@ -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)
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
token
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
......@@ -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)
......
.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 @@
- 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')
......@@ -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
---
title: Add support for multi-level container image repository names
merge_request: 10109
author: André Guede
......@@ -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
......
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
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
......
......@@ -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
......
......@@ -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:
......
......@@ -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
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
@client = ContainerRegistry::Client.new(uri, options)
end
def repository(name)
ContainerRegistry::Repository.new(self, name)
end
private
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
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
......@@ -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
......
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'
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] }
before do
login_as(:user)
project.team << [@user, :developer]
stub_container_registry_tags(*tags)
stub_container_registry_config(enabled: true)
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
let(:container_repository) do
create(:container_repository, name: 'my/image')
end
describe 'GET /:project/container_registry' 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
context 'when no tags' do
let(:tags) { [] }
context 'when there are no image repositories' do
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
context 'when there are tags' do
it { expect(page).to have_content(tag_name) }
it { expect(page).to have_content('d7a513a66') }
context 'when there are image repositories' do
before do
stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest])
project.container_repositories << container_repository
end
scenario 'user wants to see multi-level container repository' do
visit_container_registry
expect(page).to have_content('my/image')
end
describe 'DELETE /:project/container_registry/tag' do
before do
visit namespace_project_container_registry_index_path(project.namespace, project)
scenario 'user removes entire container repository' do
visit_container_registry
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
......@@ -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) }
......
......@@ -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) }
......
......@@ -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) }
......
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(:token) { 'authorization-token' }
let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) }
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
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
context '#digest' do
subject { blob.digest }
it { is_expected.to eq(digest) }
end
context '#type' do
subject { blob.type }
it { is_expected.to eq('binary') }
describe '#digest' do
it 'return correct digest value' do
expect(blob.digest).to eq 'sha256:0123456789012345'
end
end
context '#revision' do
subject { blob.revision }
it { is_expected.to eq('0123456789012345') }
describe '#type' do
it 'returns a correct type' do
expect(blob.type).to eq 'binary'
end
end
context '#short_revision' do
subject { blob.short_revision }
describe '#revision' do
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
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
......
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
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 }
......
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'
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
......@@ -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
......
......@@ -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
......
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
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
......
......@@ -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
describe '#container_registry_url' 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
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')
it { is_expected.to be_truthy }
stub_container_registry_tags(repository: /image/,
tags: %w[latest rc1])
end
context 'when no tags' do
before { stub_container_registry_tags }
it 'should have image tags' do
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
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) }
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
......
......@@ -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] }
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 '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
......
......@@ -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
describe 'container registry' do
context 'when there are regular container repositories' 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: project.full_path + '/image',
tags: ['tag'])
project.container_repositories << container_repository
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 succeeds' do
it 'removes tags' do
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
context 'when there are tags for legacy root repository' do
before do
stub_container_registry_tags(repository: project.full_path,
tags: ['tag'])
end
context 'tags deletion fails' do
before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) }
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
......
......@@ -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) }
......
......@@ -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
......
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