Commit 6cd5b7db authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b3e0658c
...@@ -350,7 +350,9 @@ quick-rspec geo pg-10 ee: ...@@ -350,7 +350,9 @@ quick-rspec geo pg-10 ee:
- $CI_COMMIT_REF_NAME =~ /(^geo[\/-].*|.*-geo$)/ - $CI_COMMIT_REF_NAME =~ /(^geo[\/-].*|.*-geo$)/
rspec quarantine pg ee: rspec quarantine pg ee:
extends: rspec quarantine pg extends:
- rspec quarantine pg
- .only-ee
script: script:
- export NO_KNAPSACK=1 CACHE_CLASSES=true - export NO_KNAPSACK=1 CACHE_CLASSES=true
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
......
...@@ -22,7 +22,6 @@ import Board from 'ee_else_ce/boards/components/board'; ...@@ -22,7 +22,6 @@ import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue'; import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor';
import { import {
NavigationType, NavigationType,
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
......
import Vue from 'vue'; import Vue from 'vue';
import '../vue_shared/vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false; Vue.config.productionTip = false;
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
import { __ } from '~/locale'; import { __ } from '~/locale';
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -2,7 +2,6 @@ import Vue from 'vue'; ...@@ -2,7 +2,6 @@ import Vue from 'vue';
import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar'; import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data'; import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
export default function initIssueableApp() { export default function initIssueableApp() {
return new Vue({ return new Vue({
......
...@@ -8,19 +8,19 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; ...@@ -8,19 +8,19 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Maintain a global counter for active requests // Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb // see: spec/support/wait_for_requests.rb
axios.interceptors.request.use(config => { axios.interceptors.request.use(config => {
window.activeVueResources = window.activeVueResources || 0; window.pendingRequests = window.pendingRequests || 0;
window.activeVueResources += 1; window.pendingRequests += 1;
return config; return config;
}); });
// Remove the global counter // Remove the global counter
axios.interceptors.response.use( axios.interceptors.response.use(
response => { response => {
window.activeVueResources -= 1; window.pendingRequests -= 1;
return response; return response;
}, },
err => { err => {
window.activeVueResources -= 1; window.pendingRequests -= 1;
return Promise.reject(err); return Promise.reject(err);
}, },
); );
......
...@@ -27,8 +27,7 @@ export default { ...@@ -27,8 +27,7 @@ export default {
/** /**
pageInfo will come from the headers of the API call pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call there should be a function that constructs the pageInfo for this component
there should be a function that contructs the pageInfo for this component
This is an example: This is an example:
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import csrf from '../lib/utils/csrf';
Vue.use(VueResource);
// Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb
Vue.http.interceptors.push((request, next) => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;
next(() => {
window.activeVueResources -= 1;
});
});
// Inject CSRF token and parse headers.
// New Vue Resource version uses Headers, we are expecting a plain object to render pagination
// and polling.
Vue.http.interceptors.push((request, next) => {
request.headers.set(csrf.headerKey, csrf.token);
next(response => {
// Headers object has a `forEach` property that iterates through all values.
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// eslint-disable-next-line no-param-reassign
response.headers = headers;
});
});
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
} }
.sidebar-top-level-items > li > a { .sidebar-top-level-items > li > a {
min-height: 44px; min-height: 45px;
} }
.fly-out-top-item { .fly-out-top-item {
......
...@@ -19,14 +19,12 @@ module Projects ...@@ -19,14 +19,12 @@ module Projects
end end
def destroy def destroy
if tag.delete result = Projects::ContainerRepository::DeleteTagsService
respond_to do |format| .new(image.project, current_user, tags: [params[:id]])
format.json { head :no_content } .execute(image)
end
else respond_to do |format|
respond_to do |format| format.json { head(result[:status] == :success ? :ok : bad_request) }
format.json { head :bad_request }
end
end end
end end
...@@ -42,21 +40,12 @@ module Projects ...@@ -42,21 +40,12 @@ module Projects
return return
end end
@tags = tag_names.map { |tag_name| image.tag(tag_name) } result = Projects::ContainerRepository::DeleteTagsService
unless @tags.all? { |tag| tag.valid_name? } .new(image.project, current_user, tags: tag_names)
head :bad_request .execute(image)
return
end
success_count = 0
@tags.each do |tag|
if tag.delete
success_count += 1
end
end
respond_to do |format| respond_to do |format|
format.json { head(success_count == @tags.size ? :no_content : :bad_request) } format.json { head(result[:status] == :success ? :no_content : :bad_request) }
end end
end end
...@@ -70,10 +59,6 @@ module Projects ...@@ -70,10 +59,6 @@ module Projects
@image ||= project.container_repositories @image ||= project.container_repositories
.find(params[:repository_id]) .find(params[:repository_id])
end end
def tag
@tag ||= image.tag(params[:id])
end
end end
end end
end end
...@@ -40,7 +40,7 @@ module Projects ...@@ -40,7 +40,7 @@ module Projects
return unless tags.count == other_tags.count return unless tags.count == other_tags.count
# delete all tags # delete all tags
tags.map(&:delete) tags.map(&:unsafe_delete)
end end
def group_by_digest(tags) def group_by_digest(tags)
......
# frozen_string_literal: true
module Projects
module ContainerRepository
class DeleteTagsService < BaseService
def execute(container_repository)
return error('access denied') unless can?(current_user, :destroy_container_image, project)
tag_names = params[:tags]
return error('not tags specified') if tag_names.blank?
if can_use?
smart_delete(container_repository, tag_names)
else
unsafe_delete(container_repository, tag_names)
end
end
private
def unsafe_delete(container_repository, tag_names)
deleted_tags = tag_names.select do |tag_name|
container_repository.tag(tag_name).unsafe_delete
end
return error('could not delete tags') if deleted_tags.empty?
success(deleted: deleted_tags)
end
# Replace a tag on the registry with a dummy tag.
# This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it.
# This way when the tag is deleted only the dummy image is affected.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/21405 for a discussion
def smart_delete(container_repository, tag_names)
# generates the blobs for the dummy image
dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
# update the manifests of the tags with the new dummy image
tag_digests = tag_names.map do |name|
container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
end
# make sure the digests are the same (it should always be)
tag_digests.uniq!
# rubocop: disable CodeReuse/ActiveRecord
Gitlab::Sentry.track_exception(ArgumentError.new('multiple tag digests')) if tag_digests.many?
# deletes the dummy image
# all created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
if container_repository.client.delete_repository_tag(container_repository.path, tag_digests.first)
success(deleted: tag_names)
else
error('could not delete tags')
end
end
def can_use?
Feature.enabled?(:container_registry_smart_delete, project, default_enabled: true)
end
end
end
end
---
title: 'Adds the ability to delete single tags from the docker registry. Fix the issue that caused all related tags and image to be deleted at the same time.'
merge_request: 16886
author:
type: fixed
---
title: Stop sidebar icons from jumping when expanded & collapsed
merge_request: 16971
author:
type: fixed
...@@ -252,8 +252,8 @@ This action does not delete blobs. In order to delete them and recycle disk spac ...@@ -252,8 +252,8 @@ This action does not delete blobs. In order to delete them and recycle disk spac
[run the garbage collection](https://docs.gitlab.com/omnibus/maintenance/README.html#removing-unused-layers-not-referenced-by-manifests). [run the garbage collection](https://docs.gitlab.com/omnibus/maintenance/README.html#removing-unused-layers-not-referenced-by-manifests).
NOTE: **Note:** NOTE: **Note:**
Due to a [Docker Distribution deficiency](https://gitlab.com/gitlab-org/gitlab-foss/issues/21405), Since GitLab 12.4, individual tags are deleted.
it doesn't remove tags whose manifest is shared by multiple tags. For more details, see the [discussion](https://gitlab.com/gitlab-org/gitlab/issues/15737).
Examples: Examples:
......
...@@ -253,13 +253,13 @@ table_display_block: true ...@@ -253,13 +253,13 @@ table_display_block: true
--- ---
``` ```
## Emphasis ### Emphasis
- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`). - Use double asterisks (`**`) to mark a word or text in bold (`**bold**`).
- Use underscore (`_`) for text in italics (`_italic_`). - Use underscore (`_`) for text in italics (`_italic_`).
- Use greater than (`>`) for blockquotes. - Use greater than (`>`) for blockquotes.
## Punctuation ### Punctuation
Check the general punctuation rules for the GitLab documentation on the table below. Check the general punctuation rules for the GitLab documentation on the table below.
Check specific punctuation rules for [lists](#lists) below. Check specific punctuation rules for [lists](#lists) below.
...@@ -274,6 +274,20 @@ Check specific punctuation rules for [lists](#lists) below. ...@@ -274,6 +274,20 @@ Check specific punctuation rules for [lists](#lists) below.
| Always add a space before and after dashes when using it in a sentence (for replacing a comma, for example). | _You should try this - or not._ | | Always add a space before and after dashes when using it in a sentence (for replacing a comma, for example). | _You should try this - or not._ |
| Always use lowercase after a colon. | _Related Issues: a way to create a relationship between issues._ | | Always use lowercase after a colon. | _Related Issues: a way to create a relationship between issues._ |
### Placeholder text
Often in examples, a writer will provide a command or configuration that is complete apart from
a value specific to the reader.
In these cases, use [`<` and `>`](https://en.wikipedia.org/wiki/Usage_message#Pattern) to call out
where a reader must replace text with their own value.
For example:
```sh
cp <your_source_directory> <your_destination_directory>
```
## Lists ## Lists
- Always start list items with a capital letter, unless they are parameters or commands - Always start list items with a capital letter, unless they are parameters or commands
......
...@@ -106,9 +106,15 @@ module API ...@@ -106,9 +106,15 @@ module API
authorize_destroy_container_image! authorize_destroy_container_image!
validate_tag! validate_tag!
tag.delete result = ::Projects::ContainerRepository::DeleteTagsService
.new(repository.project, current_user, tags: [declared_params[:tag_name]])
status :ok .execute(repository)
if result[:status] == :success
status :ok
else
status :bad_request
end
end end
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
require 'faraday' require 'faraday'
require 'faraday_middleware' require 'faraday_middleware'
require 'digest'
module ContainerRegistry module ContainerRegistry
class Client class Client
...@@ -9,6 +10,8 @@ module ContainerRegistry ...@@ -9,6 +10,8 @@ module ContainerRegistry
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json' DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json' OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
# Taken from: FaradayMiddleware::FollowRedirects # Taken from: FaradayMiddleware::FollowRedirects
...@@ -36,6 +39,45 @@ module ContainerRegistry ...@@ -36,6 +39,45 @@ module ContainerRegistry
faraday.delete("/v2/#{name}/manifests/#{reference}").success? faraday.delete("/v2/#{name}/manifests/#{reference}").success?
end end
def upload_raw_blob(path, blob)
digest = "sha256:#{Digest::SHA256.hexdigest(blob)}"
if upload_blob(path, blob, digest).success?
[blob, digest]
end
end
def upload_blob(name, content, digest)
upload = faraday.post("/v2/#{name}/blobs/uploads/")
return unless upload.success?
location = URI(upload.headers['location'])
faraday.put("#{location.path}?#{location.query}") do |req|
req.params['digest'] = digest
req.headers['Content-Type'] = 'application/octet-stream'
req.body = content
end
end
def generate_empty_manifest(path)
image = {
config: {}
}
image, image_digest = upload_raw_blob(path, JSON.pretty_generate(image))
return unless image
{
schemaVersion: 2,
mediaType: DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE,
config: {
mediaType: CONTAINER_IMAGE_V1_TYPE,
size: image.size,
digest: image_digest
}
}
end
def blob(name, digest, type = nil) def blob(name, digest, type = nil)
type ||= 'application/octet-stream' type ||= 'application/octet-stream'
response_body faraday_blob.get("/v2/#{name}/blobs/#{digest}", nil, 'Accept' => type), allow_redirect: true response_body faraday_blob.get("/v2/#{name}/blobs/#{digest}", nil, 'Accept' => type), allow_redirect: true
...@@ -45,6 +87,15 @@ module ContainerRegistry ...@@ -45,6 +87,15 @@ module ContainerRegistry
faraday.delete("/v2/#{name}/blobs/#{digest}").success? faraday.delete("/v2/#{name}/blobs/#{digest}").success?
end end
def put_tag(name, reference, manifest)
response = faraday.put("/v2/#{name}/manifests/#{reference}") do |req|
req.headers['Content-Type'] = DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE
req.body = JSON.pretty_generate(manifest)
end
response.headers['docker-content-digest'] if response.success?
end
private private
def initialize_connection(conn, options) def initialize_connection(conn, options)
......
...@@ -98,6 +98,10 @@ module ContainerRegistry ...@@ -98,6 +98,10 @@ module ContainerRegistry
end end
end end
def put(digests)
repository.client.put_tag(repository.path, name, digests)
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def total_size def total_size
return unless layers return unless layers
...@@ -106,7 +110,10 @@ module ContainerRegistry ...@@ -106,7 +110,10 @@ module ContainerRegistry
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def delete # Deletes the image associated with this tag
# Note this will delete the image and all tags associated with it.
# Consider using DeleteTagsService instead.
def unsafe_delete
return unless digest return unless digest
client.delete_repository_tag(repository.path, digest) client.delete_repository_tag(repository.path, digest)
......
This diff is collapsed.
desc 'Checks if the branch would apply cleanly to EE'
task ee_compat_check: :environment do
Rake::Task['gitlab:dev:ee_compat_check'].invoke
end
namespace :gitlab do
namespace :dev do
desc 'Checks if the branch would apply cleanly to EE'
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
{
ce_project_url: ENV['CI_PROJECT_URL'],
branch: ENV['CI_COMMIT_REF_NAME'],
job_id: ENV['CI_JOB_ID']
}
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
exit 1
end
args
end
if File.basename(Rails.root) == 'gitlab'
puts "Skipping EE projects"
exit 0
elsif Gitlab::EeCompatCheck.new(opts || {}).check
exit 0
else
exit 1
end
end
end
end
...@@ -28,14 +28,23 @@ unless Rails.env.production? ...@@ -28,14 +28,23 @@ unless Rails.env.production?
task :all do task :all do
status = 0 status = 0
%w[ tasks = %w[
config_lint config_lint
lint:haml lint:haml
scss_lint scss_lint
gettext:lint gettext:lint
gettext:updated_check
lint:static_verification lint:static_verification
].each do |task| ]
if Gitlab.ee?
# This task will fail on CE installations (e.g. gitlab-org/gitlab-foss)
# since it will detect strings in the locale files that do not exist in
# the source files. To work around this we will only enable this task on
# EE installations.
tasks << 'gettext:updated_check'
end
tasks.each do |task|
pid = Process.fork do pid = Process.fork do
puts "*** Running rake task: #{task} ***" puts "*** Running rake task: #{task} ***"
......
This diff is collapsed.
...@@ -37,10 +37,6 @@ tasks = [ ...@@ -37,10 +37,6 @@ tasks = [
%w[scripts/lint-rugged] %w[scripts/lint-rugged]
] ]
if Gitlab.ee?
tasks.unshift(%w[ruby -rbundler/setup scripts/ee_specific_check/ee_specific_check.rb])
end
static_analysis = Gitlab::Popen::Runner.new static_analysis = Gitlab::Popen::Runner.new
static_analysis.run(tasks) do |cmd, &run| static_analysis.run(tasks) do |cmd, &run|
......
...@@ -10,6 +10,8 @@ describe Projects::Registry::TagsController do ...@@ -10,6 +10,8 @@ describe Projects::Registry::TagsController do
create(:container_repository, name: 'image', project: project) create(:container_repository, name: 'image', project: project)
end end
let(:service) { double('service') }
before do before do
sign_in(user) sign_in(user)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
...@@ -84,17 +86,17 @@ describe Projects::Registry::TagsController do ...@@ -84,17 +86,17 @@ describe Projects::Registry::TagsController do
context 'when there is matching tag present' do context 'when there is matching tag present' do
before do before do
stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) stub_container_registry_tags(repository: repository.path, tags: %w[rc1], with_manifest: true)
end end
it 'makes it possible to delete regular tag' do it 'makes it possible to delete regular tag' do
expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete) expect_delete_tags(%w[rc1])
destroy_tag('rc1') destroy_tag('rc1')
end end
it 'makes it possible to delete a tag that ends with a dot' do it 'makes it possible to delete a tag that ends with a dot' do
expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete) expect_delete_tags(%w[test.])
destroy_tag('test.') destroy_tag('test.')
end end
...@@ -125,11 +127,12 @@ describe Projects::Registry::TagsController do ...@@ -125,11 +127,12 @@ describe Projects::Registry::TagsController do
stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end end
let(:tags) { %w[tc1 test.] }
it 'makes it possible to delete tags in bulk' do it 'makes it possible to delete tags in bulk' do
allow_any_instance_of(ContainerRegistry::Tag).to receive(:delete) { |*args| ContainerRegistry::Tag.delete(*args) } expect_delete_tags(tags)
expect(ContainerRegistry::Tag).to receive(:delete).exactly(2).times
bulk_destroy_tags(['rc1', 'test.']) bulk_destroy_tags(tags)
end end
end end
end end
...@@ -146,4 +149,9 @@ describe Projects::Registry::TagsController do ...@@ -146,4 +149,9 @@ describe Projects::Registry::TagsController do
format: :json format: :json
end end
end end
def expect_delete_tags(tags, status = :success)
expect(service).to receive(:execute).with(repository) { { status: status } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(repository.project, user, tags: tags) { service }
end
end end
...@@ -53,7 +53,9 @@ describe 'Container Registry', :js do ...@@ -53,7 +53,9 @@ describe 'Container Registry', :js do
find('.js-toggle-repo').click find('.js-toggle-repo').click
wait_for_requests wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) service = double('service')
expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
click_on(class: 'js-delete-registry-row', visible: false) click_on(class: 'js-delete-registry-row', visible: false)
expect(find('.modal .modal-title')).to have_content 'Remove image' expect(find('.modal .modal-title')).to have_content 'Remove image'
......
...@@ -470,9 +470,6 @@ describe 'Issues' do ...@@ -470,9 +470,6 @@ describe 'Issues' do
expect(page).to have_content 'None' expect(page).to have_content 'None'
end end
# wait_for_requests does not work with vue-resource at the moment
sleep 1
expect(issue.reload.assignees).to be_empty expect(issue.reload.assignees).to be_empty
end end
......
// eslint-disable-next-line import/prefer-default-export
export const headersInterceptor = (request, next) => {
next(response => {
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// eslint-disable-next-line no-param-reassign
response.headers = headers;
});
};
// eslint-disable-next-line import/prefer-default-export
export const headersInterceptor = (request, next) => {
next(response => {
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// eslint-disable-next-line no-param-reassign
response.headers = headers;
});
};
...@@ -7,7 +7,6 @@ import 'core-js/features/set-immediate'; ...@@ -7,7 +7,6 @@ import 'core-js/features/set-immediate';
import 'vendor/jasmine-jquery'; import 'vendor/jasmine-jquery';
import '~/commons'; import '~/commons';
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import jasmineDiff from 'jasmine-diff'; import jasmineDiff from 'jasmine-diff';
import { config as testUtilsConfig } from '@vue/test-utils'; import { config as testUtilsConfig } from '@vue/test-utils';
...@@ -46,7 +45,6 @@ Vue.config.errorHandler = function(err) { ...@@ -46,7 +45,6 @@ Vue.config.errorHandler = function(err) {
fail(err); fail(err);
}; };
Vue.use(VueResource);
Vue.use(Translate); Vue.use(Translate);
// enable test fixtures // enable test fixtures
...@@ -102,13 +100,6 @@ afterEach(__rewire_reset_all__); // eslint-disable-line ...@@ -102,13 +100,6 @@ afterEach(__rewire_reset_all__); // eslint-disable-line
// to run our unit tests. // to run our unit tests.
beforeEach(done => done()); beforeEach(done => done());
const builtinVueHttpInterceptors = Vue.http.interceptors.slice();
beforeEach(() => {
// restore interceptors so we have no remaining ones from previous tests
Vue.http.interceptors = builtinVueHttpInterceptors.slice();
});
let longRunningTestTimeoutHandle; let longRunningTestTimeoutHandle;
beforeEach(done => { beforeEach(done => {
......
...@@ -73,4 +73,69 @@ describe ContainerRegistry::Client do ...@@ -73,4 +73,69 @@ describe ContainerRegistry::Client do
expect(response).to eq('Successfully redirected') expect(response).to eq('Successfully redirected')
end end
end end
def stub_upload(path, content, digest, status = 200)
stub_request(:post, "http://container-registry/v2/#{path}/blobs/uploads/")
.to_return(status: status, body: "", headers: { 'location' => 'http://container-registry/next_upload?id=someid' })
stub_request(:put, "http://container-registry/next_upload?digest=#{digest}&id=someid")
.with(body: content)
.to_return(status: status, body: "", headers: {})
end
describe '#upload_blob' do
subject { client.upload_blob('path', 'content', 'sha256:123') }
context 'with successful uploads' do
it 'starts the upload and posts the blob' do
stub_upload('path', 'content', 'sha256:123')
expect(subject).to be_success
end
end
context 'with a failed upload' do
before do
stub_upload('path', 'content', 'sha256:123', 400)
end
it 'returns nil' do
expect(subject).to be nil
end
end
end
describe '#generate_empty_manifest' do
subject { client.generate_empty_manifest('path') }
let(:result_manifest) do
{
schemaVersion: 2,
mediaType: 'application/vnd.docker.distribution.manifest.v2+json',
config: {
mediaType: 'application/vnd.docker.container.image.v1+json',
size: 21,
digest: 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3'
}
}
end
it 'uploads a random image and returns the manifest' do
stub_upload('path', "{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
expect(subject).to eq(result_manifest)
end
end
describe '#put_tag' do
subject { client.put_tag('path', 'tagA', { foo: :bar }) }
it 'uploads the manifest and returns the digest' do
stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
.with(body: "{\n \"foo\": \"bar\"\n}")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' })
expect(subject).to eq 'sha256:123'
end
end
end end
...@@ -179,7 +179,7 @@ describe ContainerRegistry::Tag do ...@@ -179,7 +179,7 @@ describe ContainerRegistry::Tag do
end end
end end
describe '#delete' do describe '#unsafe_delete' do
before do before do
stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest') stub_request(:delete, 'http://registry.gitlab/v2/group/test/manifests/sha256:digest')
.with(headers: headers) .with(headers: headers)
...@@ -187,7 +187,7 @@ describe ContainerRegistry::Tag do ...@@ -187,7 +187,7 @@ describe ContainerRegistry::Tag do
end end
it 'correctly deletes the tag' do it 'correctly deletes the tag' do
expect(tag.delete).to be_truthy expect(tag.unsafe_delete).to be_truthy
end end
end end
end end
......
...@@ -150,7 +150,7 @@ describe API::ProjectContainerRepositories do ...@@ -150,7 +150,7 @@ describe API::ProjectContainerRepositories do
expect(response).to have_gitlab_http_status(:accepted) expect(response).to have_gitlab_http_status(:accepted)
end end
context 'called multiple times in one hour' do context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
it 'returns 400 with an error message' do it 'returns 400 with an error message' do
stub_exclusive_lease_taken(lease_key, timeout: 1.hour) stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
subject subject
...@@ -202,6 +202,8 @@ describe API::ProjectContainerRepositories do ...@@ -202,6 +202,8 @@ describe API::ProjectContainerRepositories do
end end
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
let(:service) { double('service') }
subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) } subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
it_behaves_like 'rejected container repository access', :reporter, :forbidden it_behaves_like 'rejected container repository access', :reporter, :forbidden
...@@ -210,18 +212,34 @@ describe API::ProjectContainerRepositories do ...@@ -210,18 +212,34 @@ describe API::ProjectContainerRepositories do
context 'for developer' do context 'for developer' do
let(:api_user) { developer } let(:api_user) { developer }
before do context 'when there are multiple tags' do
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true) before do
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
end
it 'properly removes tag' do
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
subject
expect(response).to have_gitlab_http_status(:ok)
end
end end
it 'properly removes tag' do context 'when there\'s only one tag' do
expect_any_instance_of(ContainerRegistry::Client) before do
.to receive(:delete_repository_tag).with(root_repository.path, stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15') end
subject it 'properly removes tag' do
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
expect(response).to have_gitlab_http_status(:ok) subject
expect(response).to have_gitlab_http_status(:ok)
end
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ContainerRepository::DeleteTagsService do
set(:user) { create(:user) }
set(:project) { create(:project, :private) }
set(:repository) { create(:container_repository, :root, project: project) }
let(:params) { { tags: tags } }
let(:service) { described_class.new(project, user, params) }
before do
stub_container_registry_config(enabled: true,
api_url: 'http://registry.gitlab',
host_port: 'registry.gitlab')
stub_container_registry_tags(
repository: repository.path,
tags: %w(latest A Ba Bb C D E))
stub_tag_digest('latest', 'sha256:configA')
stub_tag_digest('A', 'sha256:configA')
stub_tag_digest('Ba', 'sha256:configB')
end
describe '#execute' do
let(:tags) { %w[A] }
subject { service.execute(repository) }
context 'without permissions' do
it { is_expected.to include(status: :error) }
end
context 'with permissions' do
before do
project.add_developer(user)
end
context 'when no params are specified' do
let(:params) { {} }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag)
is_expected.to include(status: :error)
end
end
context 'with empty tags' do
let(:tags) { [] }
it 'does not remove anything' do
expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag)
is_expected.to include(status: :error)
end
end
context 'with dummy tags disabled' do
let(:tags) { %w[A Ba] }
before do
stub_feature_flags(container_registry_smart_delete: false)
end
it 'deletes tags one by one' do
expect_delete_tag('sha256:configA')
expect_delete_tag('sha256:configB')
is_expected.to include(status: :success)
end
end
context 'with dummy tags enabled' do
let(:tags) { %w[A Ba] }
it 'deletes the tags using a dummy image' do
stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
expect_delete_tag('sha256:dummy')
is_expected.to include(status: :success)
end
end
end
end
private
def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => digest })
end
def stub_digest_config(digest, created_at)
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:blob)
.with(repository.path, digest, nil) do
{ 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
end
end
def stub_upload(content, digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:upload_blob)
.with(repository.path, content, digest) { double(success?: true ) }
end
def expect_delete_tag(digest)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:delete_repository_tag)
.with(repository.path, digest) { true }
end
end
...@@ -49,11 +49,11 @@ module WaitForRequests ...@@ -49,11 +49,11 @@ module WaitForRequests
return true unless javascript_test? return true unless javascript_test?
finished_all_ajax_requests? && finished_all_ajax_requests? &&
finished_all_vue_resource_requests? finished_all_axios_requests?
end end
def finished_all_vue_resource_requests? def finished_all_axios_requests?
Capybara.page.evaluate_script('window.activeVueResources || 0').zero? Capybara.page.evaluate_script('window.pendingRequests || 0').zero?
end end
def finished_all_ajax_requests? def finished_all_ajax_requests?
......
...@@ -1131,7 +1131,6 @@ vue-apollo,3.0.0-beta.25,ISC ...@@ -1131,7 +1131,6 @@ vue-apollo,3.0.0-beta.25,ISC
vue-functional-data-merge,2.0.6,MIT vue-functional-data-merge,2.0.6,MIT
vue-hot-reload-api,2.3.0,MIT vue-hot-reload-api,2.3.0,MIT
vue-loader,15.4.2,MIT vue-loader,15.4.2,MIT
vue-resource,1.5.0,MIT
vue-router,3.0.1,MIT vue-router,3.0.1,MIT
vue-style-loader,4.1.0,MIT vue-style-loader,4.1.0,MIT
vue-template-compiler,2.5.17,MIT vue-template-compiler,2.5.17,MIT
......
This diff is collapsed.
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