Commit 1c8fa70f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 736d36d8
......@@ -374,6 +374,7 @@ linters:
- 'app/views/shared/boards/components/sidebar/_due_date.html.haml'
- 'app/views/shared/boards/components/sidebar/_labels.html.haml'
- 'app/views/shared/boards/components/sidebar/_milestone.html.haml'
- 'app/views/shared/deploy_tokens/_revoke_modal.html.haml'
- 'app/views/shared/empty_states/_priority_labels.html.haml'
- 'app/views/shared/hook_logs/_content.html.haml'
- 'app/views/shared/issuable/_assignees.html.haml'
......
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import initVariableList from '~/ci_variable_list';
import DueDateSelectors from '~/due_date_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
// eslint-disable-next-line no-new
new DueDateSelectors();
if (gon.features.newVariablesUi) {
initVariableList();
......
......@@ -3,6 +3,7 @@ import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list';
import DueDateSelectors from '~/due_date_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
......@@ -39,5 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
// eslint-disable-next-line no-new
new DueDateSelectors();
registrySettingsApp();
});
<script>
import { mapState, mapActions } from 'vuex';
import {
GlLoadingIcon,
GlEmptyState,
GlPagination,
GlTooltipDirective,
......@@ -10,6 +9,7 @@ import {
GlModal,
GlSprintf,
GlLink,
GlSkeletonLoader,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
......@@ -20,7 +20,6 @@ export default {
name: 'RegistryListApp',
components: {
GlEmptyState,
GlLoadingIcon,
GlPagination,
ProjectEmptyState,
GroupEmptyState,
......@@ -30,11 +29,17 @@ export default {
GlModal,
GlSprintf,
GlLink,
GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
loader: {
repeat: 10,
width: 1000,
height: 40,
},
data() {
return {
itemToDelete: {},
......@@ -104,74 +109,81 @@ export default {
</gl-empty-state>
<template v-else>
<gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
<template v-else>
<div v-if="images.length" ref="imagesList">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
<div>
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="d-flex flex-column">
<div v-if="isLoading" class="mt-2">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
:width="$options.loader.width"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="500" x="10" y="10" height="20" rx="4" />
<circle cx="525" cy="20" r="10" />
<rect x="960" y="0" width="40" height="40" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<div v-if="images.length" ref="imagesList" class="d-flex flex-column">
<div
v-for="(listItem, index) in images"
:key="index"
ref="rowItem"
:class="{ 'border-top': index === 0 }"
class="d-flex justify-content-between align-items-center py-2 border-bottom"
>
<div>
<router-link
ref="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div
v-for="(listItem, index) in images"
:key="index"
ref="rowItem"
:class="[
'd-flex justify-content-between align-items-center py-2 border-bottom',
{ 'border-top': index === 0 },
]"
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__('ContainerRegistry|Missing or insufficient permission, delete button disabled')
"
>
<div>
<router-link
ref="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
)
"
<gl-button
ref="deleteImageButton"
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
>
<gl-button
ref="deleteImageButton"
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
>
<gl-icon name="remove" />
</gl-button>
</div>
<gl-icon name="remove" />
</gl-button>
</div>
</div>
<gl-pagination
......@@ -182,6 +194,7 @@ export default {
class="w-100 mt-2"
/>
</div>
<template v-else>
<project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else />
......
......@@ -68,31 +68,28 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
.delete(tag.destroy_path)
.then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, params });
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => {
commit(types.SET_MAIN_LOADING, true);
const { id } = decodeAndParse(params);
const url = `/${state.config.projectPath}/registry/repository/${id}/tags/bulk_destroy`;
const { tags_path } = decodeAndParse(params);
const url = tags_path.replace('?format=json', '/bulk_destroy');
return axios
.delete(url, { params: { ids } })
.then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, params });
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
......
// eslint-disable-next-line import/prefer-default-export
export const tags = state => {
// to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading
// this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
return state.isLoading ? [] : state.tags;
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
......@@ -9,6 +10,7 @@ Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
state,
getters,
actions,
mutations,
});
......
......@@ -54,7 +54,7 @@
.mh-50vh { max-height: 50vh; }
.font-size-inherit { font-size: inherit; }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
......
# frozen_string_literal: true
class Groups::DeployTokensController < Groups::ApplicationController
before_action :authorize_admin_group!
def revoke
@token = @group.deploy_tokens.find(params[:id])
@token.revoke!
redirect_to group_settings_ci_cd_path(@group, anchor: 'js-deploy-tokens')
end
end
......@@ -7,11 +7,11 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
push_frontend_feature_flag(:new_variables_ui, @group)
end
before_action :define_variables, only: [:show, :create_deploy_token]
def show
define_ci_variables
end
def update
......@@ -41,8 +41,23 @@ module Groups
redirect_to group_settings_ci_cd_path
end
def create_deploy_token
@new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
end
render 'show'
end
private
def define_variables
define_ci_variables
define_deploy_token_variables
end
def define_ci_variables
@variable = Ci::GroupVariable.new(group: group)
.present(current_user: current_user)
......@@ -50,6 +65,12 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
def define_deploy_token_variables
@deploy_tokens = @group.deploy_tokens.active
@new_deploy_token = DeployToken.new
end
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
......@@ -73,6 +94,10 @@ module Groups
def update_group_params
params.require(:group).permit(:max_artifacts_size)
end
def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end
end
end
end
......@@ -7,6 +7,6 @@ class Projects::DeployTokensController < Projects::ApplicationController
@token = @project.deploy_tokens.find(params[:id])
@token.revoke!
redirect_to project_settings_repository_path(project, anchor: 'js-deploy-tokens')
redirect_to project_settings_ci_cd_path(project, anchor: 'js-deploy-tokens')
end
end
......@@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :define_variables
before_action do
push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
push_frontend_feature_flag(:new_variables_ui, @project)
end
def show
......@@ -46,6 +46,16 @@ module Projects
redirect_to namespace_project_settings_ci_cd_path
end
def create_deploy_token
@new_deploy_token = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
end
render 'show'
end
private
def update_params
......@@ -64,6 +74,10 @@ module Projects
end
end
def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end
def run_autodevops_pipeline(service)
return unless service.run_auto_devops_pipeline?
......@@ -83,6 +97,7 @@ module Projects
def define_variables
define_runners_variables
define_ci_variables
define_deploy_token_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
......@@ -132,6 +147,12 @@ module Projects
def define_auto_devops_variables
@auto_devops = @project.auto_devops || ProjectAutoDevops.new
end
def define_deploy_token_variables
@deploy_tokens = @project.deploy_tokens.active
@new_deploy_token = DeployToken.new
end
end
end
end
......
......@@ -10,16 +10,6 @@ module Projects
render_show
end
def create_deploy_token
@new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
end
render_show
end
def cleanup
cleanup_params = params.require(:project).permit(:bfg_object_map)
result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
......@@ -38,9 +28,7 @@ module Projects
def render_show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
@deploy_tokens = @project.deploy_tokens.active
define_deploy_token
define_protected_refs
remote_mirror
......@@ -93,14 +81,6 @@ module Projects
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end
def define_deploy_token
@new_deploy_token ||= DeployToken.new
end
def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end
end
end
end
......
......@@ -5,6 +5,22 @@ module CiVariablesHelper
Gitlab::CurrentSettings.current_application_settings.protected_ci_variables
end
def create_deploy_token_path(entity, opts = {})
if entity.is_a?(Group)
create_deploy_token_group_settings_ci_cd_path(entity, opts)
else
create_deploy_token_project_settings_repository_path(entity, opts)
end
end
def revoke_deploy_token_path(entity, token)
if entity.is_a?(Group)
revoke_group_deploy_token_path(entity, token)
else
revoke_project_deploy_token_path(entity, token)
end
end
def ci_variable_protected?(variable, only_key_value)
if variable && !only_key_value
variable.protected
......
......@@ -59,6 +59,9 @@ class Group < Namespace
has_many :import_failures, inverse_of: :group
has_many :group_deploy_tokens
has_many :deploy_tokens, through: :group_deploy_tokens
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
......
......@@ -9,7 +9,7 @@ class GroupDeployToken < ApplicationRecord
validates :deploy_token_id, uniqueness: { scope: [:group_id] }
def has_access_to?(requested_project)
return false unless Feature.enabled?(:allow_group_deploy_token, default: true)
return false unless Feature.enabled?(:allow_group_deploy_token, default_enabled: true)
requested_project_group = requested_project&.group
return false unless requested_project_group
......
......@@ -2343,6 +2343,14 @@ class Project < ApplicationRecord
Gitlab::CurrentSettings.self_monitoring_project_id == id
end
def deploy_token_create_url(opts = {})
Gitlab::Routing.url_helpers.create_deploy_token_project_settings_ci_cd_path(self, opts)
end
def deploy_token_revoke_url_for(token)
Gitlab::Routing.url_helpers.revoke_project_deploy_token_path(self, token)
end
private
def closest_namespace_setting(name)
......
......@@ -3,11 +3,76 @@
class SnippetRepository < ApplicationRecord
include Shardable
DEFAULT_EMPTY_FILE_NAME = 'snippetfile'
EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d)\.txt$/.freeze
CommitError = Class.new(StandardError)
belongs_to :snippet, inverse_of: :snippet_repository
delegate :repository, to: :snippet
class << self
def find_snippet(disk_path)
find_by(disk_path: disk_path)&.snippet
end
end
def multi_files_action(user, files = [], **options)
return if files.nil? || files.empty?
lease_key = "multi_files_action:#{snippet_id}"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 120)
raise CommitError, 'Snippet is already being updated' unless uuid = lease.try_obtain
options[:actions] = transform_file_entries(files)
capture_git_error { repository.multi_action(user, **options) }
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
private
def capture_git_error(&block)
yield block
rescue Gitlab::Git::Index::IndexError,
Gitlab::Git::CommitError,
Gitlab::Git::PreReceiveError,
Gitlab::Git::CommandError => e
raise CommitError, e.message
end
def transform_file_entries(files)
last_index = get_last_empty_file_index
files.each do |file_entry|
file_entry[:action] = infer_action(file_entry) unless file_entry[:action]
if file_entry[:file_path].blank?
file_entry[:file_path] = build_empty_file_name(last_index)
last_index += 1
end
end
end
def infer_action(file_entry)
return :create if file_entry[:previous_path].blank?
file_entry[:previous_path] != file_entry[:file_path] ? :move : :update
end
def get_last_empty_file_index
last_file = repository.ls_files(nil)
.map! { |file| file.match(EMPTY_FILE_PATTERN) }
.compact
.max_by { |element| element[1] }
last_file ? (last_file[1].to_i + 1) : 1
end
def build_empty_file_name(index)
"#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
end
end
# frozen_string_literal: true
module DeployTokenMethods
def create_deploy_token_for(entity, params)
params[:deploy_token_type] = DeployToken.deploy_token_types["#{entity.class.name.downcase}_type".to_sym]
entity.deploy_tokens.create(params) do |deploy_token|
deploy_token.username = params[:username].presence
end
end
end
# frozen_string_literal: true
module DeployTokens
class CreateService < BaseService
def execute
@project.deploy_tokens.create(params) do |deploy_token|
deploy_token.username = params[:username].presence
end
end
end
end
# frozen_string_literal: true
module Groups
module DeployTokens
class CreateService < BaseService
include DeployTokenMethods
def execute
create_deploy_token_for(@group, params)
end
end
end
end
# frozen_string_literal: true
module Projects
module DeployTokens
class CreateService < BaseService
include DeployTokenMethods
def execute
create_deploy_token_for(@project, params)
end
end
end
end
......@@ -4,6 +4,8 @@ module Snippets
class CreateService < Snippets::BaseService
include SpamCheckMethods
CreateRepositoryError = Class.new(StandardError)
def execute
filter_spam_check_params
......@@ -23,13 +25,7 @@ module Snippets
spam_check(snippet, current_user)
snippet_saved = snippet.with_transaction_returning_status do
(snippet.save && snippet.store_mentions!).tap do |saved|
create_repository_for(snippet, current_user) if saved
end
end
if snippet_saved
if save_and_commit(snippet)
UserAgentDetailService.new(snippet, @request).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create)
......@@ -41,8 +37,45 @@ module Snippets
private
def create_repository_for(snippet, user)
snippet.create_repository if Feature.enabled?(:version_snippets, user)
def save_and_commit(snippet)
snippet.with_transaction_returning_status do
(snippet.save && snippet.store_mentions!).tap do |saved|
break false unless saved
if Feature.enabled?(:version_snippets, current_user)
create_repository_for(snippet)
create_commit(snippet)
end
end
rescue => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ...
snippet.errors.add(:base, e.message)
# If the commit action failed we need to remove the repository if exists
if snippet.repository_exists?
Repositories::DestroyService.new(snippet.repository).execute
end
false
end
end
def create_repository_for(snippet)
snippet.create_repository
raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
end
def create_commit(snippet)
commit_attrs = {
branch_name: 'master',
message: 'Initial commit'
}
snippet.snippet_repository.multi_files_action(current_user, snippet_files, commit_attrs)
end
def snippet_files
[{ file_path: params[:file_name], content: params[:content] }]
end
end
end
......@@ -5,7 +5,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
- if Feature.enabled?(:new_variables_ui, @project || @group)
- is_group = !@group.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }
......
......@@ -3,6 +3,7 @@
- expanded = expanded_by_default?
- general_expanded = @group.errors.empty? ? expanded : true
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group.')
-# Given we only have one field in this form which is also admin-only,
-# we don't want to show an empty section to non-admin users,
......@@ -24,6 +25,8 @@
.settings-content
= render 'ci/variables/index', save_endpoint: group_variables_path
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
......
......@@ -15,7 +15,7 @@
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
.input-group-text.border-0
#{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
= hidden_field_tag :namespace_id, current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
......@@ -6,7 +6,6 @@
.col-12
- if Feature.enabled?(:vue_container_registry_explorer, @project.group)
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
project_path: @project.full_path,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
......
......@@ -4,6 +4,7 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
- deploy_token_description = s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
......@@ -51,6 +52,8 @@
.settings-content
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
= render "shared/deploy_tokens/index", group_or_project: @project, description: deploy_token_description
%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4
......
......@@ -13,7 +13,6 @@
= render "projects/settings/repository/protected_branches"
= render @deploy_keys
= render "projects/deploy_tokens/index"
= render "projects/cleanup/show"
= render_if_exists 'shared/promotions/promote_repository_features'
%p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
= form_for token, url: create_deploy_token_namespace_project_settings_repository_path(project.namespace, project, anchor: 'js-deploy-tokens'), method: :post do |f|
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post do |f|
= form_errors(token)
.form-group
......@@ -24,7 +24,7 @@
= label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to the repository')
- if container_registry_enabled?(project)
- if container_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry'
= label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-bold form-check-label'
......
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) }
%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings' } }
.settings-header
%h4= s_('DeployTokens|Deploy Tokens')
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
= description
.settings-content
- if @new_deploy_token.persisted?
= render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
= render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
%h5.prepend-top-0
= s_('DeployTokens|Add a deploy token')
= render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
= render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
%hr
= render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
= render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
......@@ -3,15 +3,13 @@
.modal-content
.modal-header
%h4.modal-title
= s_('DeployTokens|Revoke')
%b #{token.name}?
= s_('DeployTokens|Revoke %{b_start}%{name}%{b_end}?').html_safe % { b_start: '<b>'.html_safe, name: token.name, b_end: '</b>'.html_safe }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
= s_('DeployTokens|You are about to revoke')
%b #{token.name}.
= s_('DeployTokens|You are about to revoke %{b_start}%{name}%{b_end}.').html_safe % { b_start: '<b>'.html_safe, name: token.name, b_end: '</b>'.html_safe }
= s_('DeployTokens|This action cannot be undone.')
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
= link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger'
= link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_deploy_token_path(group_or_project, token), method: :put, class: 'btn btn-danger'
......@@ -22,10 +22,10 @@
%span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%span.token-never-expires-label= _('Never')
%td= token.scopes.present? ? token.scopes.join(", ") : _('<no scopes selected>')
%td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
= render 'projects/deploy_tokens/revoke_modal', token: token, project: project
= render 'shared/deploy_tokens/revoke_modal', token: token, group_or_project: group_or_project
- else
.settings-message.text-center
= s_('DeployTokens|This project has no active Deploy Tokens.')
= s_('DeployTokens|This %{entity_type} has no active Deploy Tokens.') % { entity_type: group_or_project.class.name.downcase }
---
title: Addition of the Group Deploy Token interface
merge_request: 24102
author:
type: added
---
title: Fixes project import failures when user is not part of any groups
merge_request: 26038
author:
type: fixed
---
title: Commit file when snippet is created
merge_request: 23953
author:
type: added
---
title: Allow chart descriptions for Insights
merge_request: 25686
author:
type: added
---
title: Remove unreachable link from embded dashboard context menu
merge_request: 25892
author:
type: fixed
---
title: Remove Puma notices from AdminArea banner
merge_request: 26137
author:
type: changed
......@@ -29,6 +29,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :ci_cd, only: [:show, :update], controller: 'ci_cd' do
put :reset_registration_token
patch :update_auto_devops
post :create_deploy_token, path: 'deploy_token/create'
end
end
......@@ -49,6 +50,12 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
resources :deploy_tokens, constraints: { id: /\d+/ }, only: [] do
member do
put :revoke
end
end
resource :avatar, only: [:destroy]
concerns :clusterable
......
......@@ -79,7 +79,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository do
post :create_deploy_token, path: 'deploy_token/create'
# TODO: Move 'create_deploy_token' here to the ':ci_cd' resource above during 12.9.
# More details here: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24102#note_287572556
post :create_deploy_token, path: 'deploy_token/create', to: 'ci_cd#create_deploy_token'
post :cleanup
end
end
......
......@@ -64,6 +64,63 @@ the extra jobs will take resources away from jobs from workers that were already
there, if the resources available to the Sidekiq process handling the namespace
are not adjusted appropriately.
## Idempotent Jobs
It's known that a job can fail for multiple reasons, for example, network outages or bugs.
In order to address this, Sidekiq has a built-in retry mechanism that is
used by default by most workers within GitLab.
It's expected that a job can run again after a failure without major side-effects for the
application or users, which is why Sidekiq encourages
jobs to be [idempotent and transactional](https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional).
As a general rule, a worker can be considered idempotent if:
- It can safely run multiple times with the same arguments.
- Application side-effects are expected to happen only once
(or side-effects of a second run are not impactful).
A good example of that would be a cache expiration worker.
### Ensuring a worker is idempotent
Make sure the worker tests pass using the following shared example:
```ruby
include_examples 'an idempotent worker' do
it 'marks the MR as merged' do
# Using subject inside this block will process the job multiple times
subject
expect(merge_request.state).to eq('merged')
end
end
```
Use the `perform_multiple` method directly instead of `job.perform` (this
helper method is automatically included for workers).
### Declaring a worker as idempotent
```ruby
class IdempotentWorker
include ApplicationWorker
# Declares a worker is idempotent and can
# safely run multiple times.
idempotent!
# ...
end
```
It's encouraged to only have the `idempotent!` call in the top-most worker class, even if
the `perform` method is defined in another class or module.
NOTE: **Note:**
Note that a cop will fail if the worker class is not marked as idempotent.
Consider skipping the cop if you're not confident your job can safely run multiple times.
## Latency Sensitive Jobs
If a large number of background jobs get scheduled at once, queueing of jobs may
......
......@@ -61,6 +61,7 @@ bugsCharts:
title: "Charts for bugs"
charts:
- title: "Monthly bugs created"
description: "Open bugs created per month"
type: bar
query:
issuable_type: issue
......@@ -77,6 +78,7 @@ For example, here's single chart definition:
```yaml
- title: "Monthly bugs created"
description: "Open bugs created per month"
type: bar
query:
issuable_type: issue
......@@ -96,6 +98,7 @@ The following table lists available parameters for charts:
| Keyword | Description |
|:---------------------------------------------------|:------------|
| [`title`](#title) | The title of the chart. This will displayed on the Insights page. |
| [`description`](#description) | A description for the individual chart. This will be displayed above the relevant chart. |
| [`type`](#type) | The type of chart: `bar`, `line` or `stacked-bar`. |
| [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. |
......@@ -114,6 +117,17 @@ monthlyBugsCreated:
title: "Monthly bugs created"
```
### `description`
The `description` text is displayed above the chart, but below the title. It's used
to give extra details regarding the chart, for example:
```yaml
monthlyBugsCreated:
title: "Monthly bugs created"
description: "Open bugs created per month"
```
### `type`
`type` is the chart type.
......@@ -145,6 +159,7 @@ Example:
```yaml
monthlyBugsCreated:
title: "Monthly bugs created"
description: "Open bugs created per month"
type: bar
query:
issuable_type: issue
......@@ -283,6 +298,7 @@ a group's insights:
```yaml
monthlyBugsCreated:
title: "Monthly bugs created"
description: "Open bugs created per month"
type: bar
query:
issuable_type: issue
......@@ -311,6 +327,7 @@ bugsCharts:
title: "Charts for bugs"
charts:
- title: "Monthly bugs created"
description: "Open bugs created per month"
type: bar
<<: *projectsOnly
query:
......
......@@ -33,17 +33,19 @@ defined in that template.
Add the following to your `.gitlab-ci.yml` file:
```yaml
include:
template: Verify/Accessibility.gitlab-ci.yml
variables:
a11y_urls: "https://about.gitlab.com"
a11y:
variables:
a11y_urls: https://example.com https://example.com/another-page
include:
- remote: "https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml"
```
The example above will create an `a11y` job in your CI/CD pipeline and will run
Pa11y against the webpage you defined in `a11y_urls` to build a report.
NOTE: **Note:**
Only one URL may be currently passed into `a11y_urls`.
The full HTML Pa11y report will be saved as an artifact that can be [viewed directly in your browser](../pipelines/job_artifacts.md#browsing-artifacts).
NOTE: **Note:**
......
......@@ -7,36 +7,14 @@ module Gitlab
extend Gitlab::Git::RuggedImpl::UseRugged
def check
return [] unless Gitlab::Runtime.puma?
notices = []
link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html">'
link_end = '</a>'
notices << {
type: 'info',
message: _('You are running Puma, which is currently experimental. '\
'More information is available in our '\
'%{link_start}documentation%{link_end}.') % { link_start: link_start, link_end: link_end }
}
if running_puma_with_multiple_threads?
link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html">'
link_end = '</a>'
notices << {
type: 'info',
message: _('Puma is running with a thread count above 1. '\
'Information on deprecated GitLab features in this configuration is available in the '\
'%{link_start}documentation%{link_end}.') % { link_start: link_start, link_end: link_end }
}
end
if running_puma_with_multiple_threads? && rugged_enabled_through_feature_flag?
link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">'
link_end = '</a>'
notices << {
type: 'warning',
message: _('Puma is running with a thread count above 1 and the rugged '\
message: _('Puma is running with a thread count above 1 and the Rugged '\
'service is enabled. This may decrease performance in some environments. '\
'See our %{link_start}documentation%{link_end} '\
'for details of this issue.') % { link_start: link_start, link_end: link_end }
......
......@@ -6507,6 +6507,9 @@ msgstr ""
msgid "DeployTokens|Expires"
msgstr ""
msgid "DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group."
msgstr ""
msgid "DeployTokens|Name"
msgstr ""
......@@ -6516,16 +6519,19 @@ msgstr ""
msgid "DeployTokens|Revoke"
msgstr ""
msgid "DeployTokens|Revoke %{b_start}%{name}%{b_end}?"
msgstr ""
msgid "DeployTokens|Revoke %{name}"
msgstr ""
msgid "DeployTokens|Scopes"
msgstr ""
msgid "DeployTokens|This action cannot be undone."
msgid "DeployTokens|This %{entity_type} has no active Deploy Tokens."
msgstr ""
msgid "DeployTokens|This project has no active Deploy Tokens."
msgid "DeployTokens|This action cannot be undone."
msgstr ""
msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
......@@ -6537,12 +6543,15 @@ msgstr ""
msgid "DeployTokens|Username"
msgstr ""
msgid "DeployTokens|You are about to revoke"
msgid "DeployTokens|You are about to revoke %{b_start}%{name}%{b_end}."
msgstr ""
msgid "DeployTokens|Your New Deploy Token"
msgstr ""
msgid "DeployTokens|Your new group deploy token has been created."
msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
......@@ -15735,10 +15744,7 @@ msgstr ""
msgid "Pull"
msgstr ""
msgid "Puma is running with a thread count above 1 and the rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
msgstr ""
msgid "Puma is running with a thread count above 1. Information on deprecated GitLab features in this configuration is available in the %{link_start}documentation%{link_end}."
msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
msgstr ""
msgid "Purchase more minutes"
......@@ -22324,9 +22330,6 @@ msgstr ""
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
msgstr ""
msgid "You are running Puma, which is currently experimental. More information is available in our %{link_start}documentation%{link_end}."
msgstr ""
msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr ""
......
......@@ -13,6 +13,16 @@ module QA
element :variables_settings_content
end
view 'app/views/shared/deploy_tokens/_index.html.haml' do
element :deploy_tokens_settings
end
def expand_deploy_tokens(&block)
expand_section(:deploy_tokens_settings) do
Settings::DeployTokens.perform(&block)
end
end
def expand_runners_settings(&block)
expand_section(:runners_settings_content) do
Settings::Runners.perform(&block)
......
......@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class DeployTokens < Page::Base
view 'app/views/projects/deploy_tokens/_form.html.haml' do
view 'app/views/shared/deploy_tokens/_form.html.haml' do
element :deploy_token_name
element :deploy_token_expires_at
element :deploy_token_read_repository
......@@ -13,7 +13,7 @@ module QA
element :create_deploy_token
end
view 'app/views/projects/deploy_tokens/_new_deploy_token.html.haml' do
view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
element :created_deploy_token_section
element :deploy_token_user
element :deploy_token
......
......@@ -31,12 +31,6 @@ module QA
end
end
def expand_deploy_tokens(&block)
expand_section(:deploy_tokens_settings) do
DeployTokens.perform(&block)
end
end
def expand_mirroring_repositories(&block)
expand_section(:mirroring_repositories_settings_section) do
MirroringRepositories.perform(&block)
......
......@@ -6,16 +6,16 @@ module QA
attr_accessor :name, :expires_at
attribute :username do
Page::Project::Settings::Repository.perform do |repository_page|
repository_page.expand_deploy_tokens do |token|
Page::Project::Settings::CICD.perform do |cicd_page|
cicd_page.expand_deploy_tokens do |token|
token.token_username
end
end
end
attribute :password do
Page::Project::Settings::Repository.perform do |repository_page|
repository_page.expand_deploy_tokens do |token|
Page::Project::Settings::CICD.perform do |cicd_page|
cicd_page.expand_deploy_tokens do |token|
token.token_password
end
end
......@@ -31,12 +31,10 @@ module QA
def fabricate!
project.visit!
Page::Project::Menu.act do
go_to_repository_settings
end
Page::Project::Menu.perform(&:go_to_ci_cd_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_tokens do |page|
Page::Project::Settings::CICD.perform do |cicd|
cicd.expand_deploy_tokens do |page|
page.fill_token_name(name)
page.fill_token_expires_at(expires_at)
page.fill_scopes(read_repository: true, read_registry: false)
......
......@@ -210,4 +210,16 @@ describe Groups::Settings::CiCdController do
end
end
end
describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do
let(:entity) { group }
let(:create_entity_params) { { group_id: group } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
before do
entity.add_owner(user)
end
end
end
end
......@@ -247,4 +247,12 @@ describe Projects::Settings::CiCdController do
end
end
end
describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do
let(:entity) { project }
let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
end
end
end
......@@ -32,24 +32,4 @@ describe Projects::Settings::RepositoryController do
expect(RepositoryCleanupWorker).to have_received(:perform_async).once
end
end
describe 'POST create_deploy_token' do
let(:deploy_token_params) do
{
name: 'deployer_token',
expires_at: 1.month.from_now.to_date.to_s,
username: 'deployer',
read_repository: '1'
}
end
subject(:create_deploy_token) { post :create_deploy_token, params: { namespace_id: project.namespace, project_id: project, deploy_token: deploy_token_params } }
it 'creates deploy token' do
expect { create_deploy_token }.to change { DeployToken.active.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
end
......@@ -37,6 +37,19 @@ describe 'Group CI/CD settings' do
end
end
context 'Deploy tokens' do
let!(:deploy_token) { create(:deploy_token, :group, groups: [group]) }
before do
stub_container_registry_config(enabled: true)
visit group_settings_ci_cd_path(group)
end
it_behaves_like 'a deploy token in ci/cd settings' do
let(:entity_type) { 'group' }
end
end
describe 'Auto DevOps form' do
before do
stub_application_setting(auto_devops_enabled: true)
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Projects > Settings > CI/CD settings' do
let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) }
let(:role) { :maintainer }
context 'Deploy tokens' do
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
before do
project.add_role(user, role)
sign_in(user)
stub_container_registry_config(enabled: true)
visit project_settings_ci_cd_path(project)
end
it_behaves_like 'a deploy token in ci/cd settings' do
let(:entity_type) { 'project' }
end
end
end
......@@ -108,39 +108,6 @@ describe 'Projects > Settings > Repository settings' do
end
end
context 'Deploy tokens' do
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
before do
stub_container_registry_config(enabled: true)
visit project_settings_repository_path(project)
end
it 'view deploy tokens' do
within('.deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
expect(page).to have_content('read_registry')
end
end
it 'add a new deploy token' do
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
fill_in 'deploy_token_username', with: 'deployer'
check 'deploy_token_read_repository'
check 'deploy_token_read_registry'
click_button 'Create deploy token'
expect(page).to have_content('Your new project deploy token has been created')
within('.created-deploy-token-container') do
expect(page).to have_selector("input[name='deploy-token-user'][value='deployer']")
expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']")
end
end
end
context 'remote mirror settings' do
let(:user2) { create(:user) }
......
......@@ -11,7 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do
project.add_role(user, role)
sign_in(user)
visit(project_settings_repository_path(project))
visit(project_settings_ci_cd_path(project))
click_link('Revoke')
end
......
import $ from 'jquery';
import '~/behaviors/quick_submit';
describe('Quick Submit behavior', function() {
describe('Quick Submit behavior', () => {
let testContext;
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
preloadFixtures('snippets/show.html');
beforeEach(() => {
loadFixtures('snippets/show.html');
testContext = {};
testContext.spies = {
submit: jest.fn(),
};
$('form').submit(e => {
// Prevent a form submit from moving us off the testing page
e.preventDefault();
// Explicitly call the spie to know this function get's not called
testContext.spies.submit();
});
this.spies = {
submit: spyOnEvent('form', 'submit'),
};
this.textarea = $('.js-quick-submit textarea').first();
});
afterEach(() => {
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');
testContext.textarea = $('.js-quick-submit textarea').first();
});
it('does not respond to other keyCodes', () => {
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
keyCode: 32,
}),
);
expect(this.spies.submit).not.toHaveBeenTriggered();
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
it('does not respond to Enter alone', () => {
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
ctrlKey: false,
metaKey: false,
}),
);
expect(this.spies.submit).not.toHaveBeenTriggered();
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
it('does not respond to repeated events', () => {
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
repeat: true,
}),
);
expect(this.spies.submit).not.toHaveBeenTriggered();
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
it('disables input of type submit', () => {
const submitButton = $('.js-quick-submit input[type=submit]');
this.textarea.trigger(keydownEvent());
testContext.textarea.trigger(keydownEvent());
expect(submitButton).toBeDisabled();
});
it('disables button of type submit', () => {
const submitButton = $('.js-quick-submit input[type=submit]');
this.textarea.trigger(keydownEvent());
testContext.textarea.trigger(keydownEvent());
expect(submitButton).toBeDisabled();
});
......@@ -73,71 +75,79 @@ describe('Quick Submit behavior', function() {
const existingSubmit = $('.js-quick-submit input[type=submit]');
// Add an extra submit button
const newSubmit = $('<button type="submit">Submit it</button>');
newSubmit.insertAfter(this.textarea);
newSubmit.insertAfter(testContext.textarea);
const oldClick = spyOnEvent(existingSubmit, 'click');
const newClick = spyOnEvent(newSubmit, 'click');
const spies = {
oldClickSpy: jest.fn(),
newClickSpy: jest.fn(),
};
existingSubmit.on('click', () => {
spies.oldClickSpy();
});
newSubmit.on('click', () => {
spies.newClickSpy();
});
this.textarea.trigger(keydownEvent());
testContext.textarea.trigger(keydownEvent());
expect(oldClick).not.toHaveBeenTriggered();
expect(newClick).toHaveBeenTriggered();
expect(spies.oldClickSpy).not.toHaveBeenCalled();
expect(spies.newClickSpy).toHaveBeenCalled();
});
// We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
// only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) {
describe('In Macintosh', () => {
it('responds to Meta+Enter', () => {
this.textarea.trigger(keydownEvent());
testContext.textarea.trigger(keydownEvent());
expect(this.spies.submit).toHaveBeenTriggered();
expect(testContext.spies.submit).toHaveBeenCalled();
});
it('excludes other modifier keys', () => {
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
altKey: true,
}),
);
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
ctrlKey: true,
}),
);
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
shiftKey: true,
}),
);
expect(this.spies.submit).not.toHaveBeenTriggered();
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
});
} else {
it('responds to Ctrl+Enter', () => {
this.textarea.trigger(keydownEvent());
testContext.textarea.trigger(keydownEvent());
expect(this.spies.submit).toHaveBeenTriggered();
expect(testContext.spies.submit).toHaveBeenCalled();
});
it('excludes other modifier keys', () => {
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
altKey: true,
}),
);
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
metaKey: true,
}),
);
this.textarea.trigger(
testContext.textarea.trigger(
keydownEvent({
shiftKey: true,
}),
);
expect(this.spies.submit).not.toHaveBeenTriggered();
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
}
});
import { mount } from '@vue/test-utils';
import { GlTable, GlPagination, GlLoadingIcon } from '@gitlab/ui';
import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue';
......@@ -14,8 +14,7 @@ describe('Details Page', () => {
const findDeleteModal = () => wrapper.find(GlModal);
const findPagination = () => wrapper.find(GlPagination);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findTagsTable = () => wrapper.find(GlTable);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
const findFirstRowItem = ref => wrapper.find({ ref });
const findBulkDeleteButton = () => wrapper.find({ ref: 'bulkDeleteButton' });
......@@ -33,7 +32,7 @@ describe('Details Page', () => {
...stubChildren(component),
GlModal,
GlSprintf: false,
GlTable: false,
GlTable,
},
mocks: {
$route: {
......@@ -53,18 +52,19 @@ describe('Details Page', () => {
});
describe('when isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
beforeEach(() => {
store.dispatch('receiveTagsListSuccess', { ...tagsListResponse, data: [] });
store.commit(SET_MAIN_LOADING, true);
});
afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('has a loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
it('has a skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
it('does not have a main content', () => {
expect(findTagsTable().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
expect(findDeleteModal().exists()).toBe(false);
it('does not have list items', () => {
expect(findFirstRowItem('rowCheckbox').exists()).toBe(false);
});
});
......
import VueRouter from 'vue-router';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
import store from '~/registry/explorer/stores/';
......@@ -17,7 +17,7 @@ describe('List Page', () => {
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
const findEmptyState = () => wrapper.find(GlEmptyState);
......@@ -71,7 +71,7 @@ describe('List Page', () => {
});
it('should not show the loading or default state', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findSkeletonLoader().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false);
});
});
......@@ -81,8 +81,8 @@ describe('List Page', () => {
afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('shows the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
it('imagesList is not visible', () => {
......
......@@ -180,10 +180,7 @@ describe('Actions RegistryExplorer Store', () => {
{
tagsPagination: {},
},
[
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[{ type: types.SET_MAIN_LOADING, payload: true }],
[
{
type: 'requestTagsList',
......@@ -220,13 +217,11 @@ describe('Actions RegistryExplorer Store', () => {
});
describe('request delete multiple tags', () => {
const id = 1;
const params = window.btoa(JSON.stringify({ id }));
const projectPath = 'project-path';
const url = `${projectPath}/registry/repository/${id}/tags/bulk_destroy`;
const url = `project-path/registry/repository/foo/tags`;
const params = window.btoa(JSON.stringify({ tags_path: `${url}?format=json` }));
it('successfully performs the delete request', done => {
mock.onDelete(url).replyOnce(200);
mock.onDelete(`${url}/bulk_destroy`).replyOnce(200);
testAction(
actions.requestDeleteTags,
......@@ -235,15 +230,9 @@ describe('Actions RegistryExplorer Store', () => {
params,
},
{
config: {
projectPath,
},
tagsPagination: {},
},
[
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[{ type: types.SET_MAIN_LOADING, payload: true }],
[
{
type: 'requestTagsList',
......@@ -267,9 +256,6 @@ describe('Actions RegistryExplorer Store', () => {
params,
},
{
config: {
projectPath,
},
tagsPagination: {},
},
[
......
import * as getters from '~/registry/explorer/stores/getters';
describe('Getters RegistryExplorer store', () => {
let state;
const tags = ['foo', 'bar'];
describe('tags', () => {
describe('when isLoading is false', () => {
beforeEach(() => {
state = {
tags,
isLoading: false,
};
});
it('returns tags', () => {
expect(getters.tags(state)).toEqual(state.tags);
});
});
describe('when isLoading is true', () => {
beforeEach(() => {
state = {
tags,
isLoading: true,
};
});
it('returns empty array', () => {
expect(getters.tags(state)).toEqual([]);
});
});
});
});
......@@ -15,26 +15,10 @@ describe Gitlab::ConfigChecker::PumaRuggedChecker do
end
context 'application is puma' do
let(:notice_running_puma) do
{
type: 'info',
message: 'You are running Puma, which is currently experimental. '\
'More information is available in our '\
'<a href="https://docs.gitlab.com/ee/administration/operations/puma.html">documentation</a>.'
}
end
let(:notice_multi_threaded_puma) do
{
type: 'info',
message: 'Puma is running with a thread count above 1. '\
'Information on deprecated GitLab features in this configuration is available in the '\
'<a href="https://docs.gitlab.com/ee/administration/operations/puma.html">documentation</a>.'\
}
end
let(:notice_multi_threaded_puma_with_rugged) do
{
type: 'warning',
message: 'Puma is running with a thread count above 1 and the rugged '\
message: 'Puma is running with a thread count above 1 and the Rugged '\
'service is enabled. This may decrease performance in some environments. '\
'See our <a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">documentation</a> '\
'for details of this issue.'
......@@ -51,35 +35,29 @@ describe Gitlab::ConfigChecker::PumaRuggedChecker do
let(:multithreaded_puma) { false }
let(:rugged_enabled) { true }
it 'report running puma notice' do
is_expected.to contain_exactly(notice_running_puma)
end
it { is_expected.to be_empty }
end
context 'not multithreaded_puma and rugged API is not enabled' do
let(:multithreaded_puma) { false }
let(:rugged_enabled) { false }
it 'report running puma notice' do
is_expected.to contain_exactly(notice_running_puma)
end
it { is_expected.to be_empty }
end
context 'multithreaded_puma and rugged API is not enabled' do
let(:multithreaded_puma) { true }
let(:rugged_enabled) { false }
it 'report running puma notice and multi-thread puma notice' do
is_expected.to contain_exactly(notice_running_puma, notice_multi_threaded_puma)
end
it { is_expected.to be_empty }
end
context 'multithreaded_puma and rugged API is enabled' do
let(:multithreaded_puma) { true }
let(:rugged_enabled) { true }
it 'report puma/multi_threaded_puma/multi_threaded_puma_with_rugged notices' do
is_expected.to contain_exactly(notice_running_puma, notice_multi_threaded_puma, notice_multi_threaded_puma_with_rugged)
it 'report multi_threaded_puma_with_rugged notices' do
is_expected.to contain_exactly(notice_multi_threaded_puma_with_rugged)
end
end
end
......
......@@ -3,6 +3,11 @@
require 'spec_helper'
describe SnippetRepository do
let_it_be(:user) { create(:user) }
let(:snippet) { create(:personal_snippet, :repository, author: user) }
let(:snippet_repository) { snippet.snippet_repository }
let(:commit_opts) { { branch_name: 'master', message: 'whatever' } }
describe 'associations' do
it { is_expected.to belong_to(:shard) }
it { is_expected.to belong_to(:snippet) }
......@@ -10,7 +15,7 @@ describe SnippetRepository do
describe '.find_snippet' do
it 'finds snippet by disk path' do
snippet = create(:snippet)
snippet = create(:snippet, author: user)
snippet.track_snippet_repository
expect(described_class.find_snippet(snippet.disk_path)).to eq(snippet)
......@@ -20,4 +25,147 @@ describe SnippetRepository do
expect(described_class.find_snippet('@@unexisting/path/to/snippet')).to be_nil
end
end
describe '#multi_files_action' do
let(:new_file) { { file_path: 'new_file_test', content: 'bar' } }
let(:move_file) { { previous_path: 'CHANGELOG', file_path: 'CHANGELOG_new', content: 'bar' } }
let(:update_file) { { previous_path: 'README', file_path: 'README', content: 'bar' } }
let(:data) { [new_file, move_file, update_file] }
it 'returns nil when files argument is empty' do
expect(snippet.repository).not_to receive(:multi_action)
operation = snippet_repository.multi_files_action(user, [], commit_opts)
expect(operation).to be_nil
end
it 'returns nil when files argument is nil' do
expect(snippet.repository).not_to receive(:multi_action)
operation = snippet_repository.multi_files_action(user, nil, commit_opts)
expect(operation).to be_nil
end
it 'performs the operation accordingly to the files data' do
new_file_blob = blob_at(snippet, new_file[:file_path])
move_file_blob = blob_at(snippet, move_file[:previous_path])
update_file_blob = blob_at(snippet, update_file[:previous_path])
aggregate_failures do
expect(new_file_blob).to be_nil
expect(move_file_blob).not_to be_nil
expect(update_file_blob).not_to be_nil
end
expect do
snippet_repository.multi_files_action(user, data, commit_opts)
end.not_to raise_error
aggregate_failures do
data.each do |entry|
blob = blob_at(snippet, entry[:file_path])
expect(blob).not_to be_nil
expect(blob.path).to eq entry[:file_path]
expect(blob.data).to eq entry[:content]
end
end
end
it 'tries to obtain an exclusive lease' do
expect(Gitlab::ExclusiveLease).to receive(:new).with("multi_files_action:#{snippet.id}", anything).and_call_original
snippet_repository.multi_files_action(user, data, commit_opts)
end
it 'cancels the lease when the method has finished' do
expect(Gitlab::ExclusiveLease).to receive(:cancel).with("multi_files_action:#{snippet.id}", anything).and_call_original
snippet_repository.multi_files_action(user, data, commit_opts)
end
it 'raises an error if the lease cannot be obtained' do
allow_next_instance_of(Gitlab::ExclusiveLease) do |instance|
allow(instance).to receive(:try_obtain).and_return false
end
expect do
snippet_repository.multi_files_action(user, data, commit_opts)
end.to raise_error(described_class::CommitError)
end
context 'with commit actions' do
let(:result) do
[{ action: :create }.merge(new_file),
{ action: :move }.merge(move_file),
{ action: :update }.merge(update_file)]
end
let(:repo) { double }
before do
allow(snippet).to receive(:repository).and_return(repo)
allow(repo).to receive(:ls_files).and_return([])
end
it 'infers the commit action based on the parameters if not present' do
expect(repo).to receive(:multi_action).with(user, hash_including(actions: result))
snippet_repository.multi_files_action(user, data, commit_opts)
end
context 'when commit actions are present' do
let(:file_action) { { file_path: 'foo.txt', content: 'foo', action: :foobar } }
let(:data) { [file_action] }
it 'does not change commit action' do
expect(repo).to(
receive(:multi_action).with(
user,
hash_including(actions: array_including(hash_including(action: :foobar)))))
snippet_repository.multi_files_action(user, data, commit_opts)
end
end
end
context 'when files are not named' do
let(:data) do
[
{
file_path: '',
content: 'foo',
action: :create
},
{
file_path: '',
content: 'bar',
action: :create
},
{
file_path: 'foo.txt',
content: 'bar',
action: :create
}
]
end
it 'sets a name for non named files' do
expect do
snippet_repository.multi_files_action(user, data, commit_opts)
end.not_to raise_error
expect(snippet.repository.ls_files(nil)).to include('snippetfile1.txt', 'snippetfile2.txt', 'foo.txt')
end
end
end
def blob_at(snippet, path)
snippet.repository.blob_at('master', path)
end
def first_blob(snippet)
snippet.repository.blob_at('master', snippet.repository.ls_files(nil).first)
end
end
......@@ -98,6 +98,36 @@ describe API::ProjectSnippets do
}
end
shared_examples 'project snippet repository actions' do
let(:snippet) { ProjectSnippet.find(json_response['id']) }
it 'creates repository' do
subject
expect(snippet.repository.exists?).to be_truthy
end
it 'commit the files to the repository' do
subject
blob = snippet.repository.blob_at('master', params[:file_name])
expect(blob.data).to eq params[:code]
end
context 'when feature flag :version_snippets is disabled' do
it 'does not create snippet repository' do
stub_feature_flags(version_snippets: false)
expect do
subject
end.to change { ProjectSnippet.count }.by(1)
expect(snippet.repository_exists?).to be_falsey
end
end
end
context 'with a regular user' do
let(:user) { create(:user) }
......@@ -118,6 +148,10 @@ describe API::ProjectSnippets do
expect(snippet.file_name).to eq(params[:file_name])
expect(snippet.visibility_level).to eq(Snippet::INTERNAL)
end
it_behaves_like 'project snippet repository actions' do
subject { post api("/projects/#{project.id}/snippets/", user), params: params }
end
end
it 'creates a new snippet' do
......@@ -132,6 +166,10 @@ describe API::ProjectSnippets do
expect(snippet.visibility_level).to eq(Snippet::PUBLIC)
end
it_behaves_like 'project snippet repository actions' do
subject { post api("/projects/#{project.id}/snippets/", admin), params: params }
end
it 'creates a new snippet with content parameter' do
params[:content] = params.delete(:code)
......
......@@ -199,9 +199,13 @@ describe API::Snippets do
end
shared_examples 'snippet creation' do
let(:snippet) { Snippet.find(json_response["id"]) }
subject { post api("/snippets/", user), params: params }
it 'creates a new snippet' do
expect do
post api("/snippets/", user), params: params
subject
end.to change { PersonalSnippet.count }.by(1)
expect(response).to have_gitlab_http_status(201)
......@@ -210,6 +214,32 @@ describe API::Snippets do
expect(json_response['file_name']).to eq(params[:file_name])
expect(json_response['visibility']).to eq(params[:visibility])
end
it 'creates repository' do
subject
expect(snippet.repository.exists?).to be_truthy
end
it 'commit the files to the repository' do
subject
blob = snippet.repository.blob_at('master', params[:file_name])
expect(blob.data).to eq params[:content]
end
context 'when feature flag :version_snippets is disabled' do
it 'does not create snippet repository' do
stub_feature_flags(version_snippets: false)
expect do
subject
end.to change { PersonalSnippet.count }.by(1)
expect(snippet.repository_exists?).to be_falsey
end
end
end
context 'with restricted visibility settings' do
......
......@@ -798,6 +798,11 @@ describe 'project routing' do
end
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository"
# TODO: remove this test as part of https://gitlab.com/gitlab-org/gitlab/issues/207079 (12.9)
it 'to ci_cd#create_deploy_token' do
expect(post('gitlab/gitlabhq/-/settings/repository/deploy_token/create')).to route_to('projects/settings/ci_cd#create_deploy_token', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
describe Projects::TemplatesController, 'routing' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::DeployTokens::CreateService do
it_behaves_like 'a deploy token creation service' do
let(:entity) { create(:group) }
let(:deploy_token_class) { GroupDeployToken }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::DeployTokens::CreateService do
it_behaves_like 'a deploy token creation service' do
let(:entity) { create(:project) }
let(:deploy_token_class) { ProjectDeployToken }
end
end
......@@ -143,37 +143,102 @@ describe Snippets::CreateService do
end
end
shared_examples 'creates repository' do
it do
shared_examples 'creates repository and files' do
it 'creates repository' do
subject
expect(snippet.repository_exists?).to be_truthy
expect(snippet.repository.exists?).to be_truthy
end
it 'commit the files to the repository' do
subject
blob = snippet.repository.blob_at('master', base_opts[:file_name])
expect(blob.data).to eq base_opts[:content]
end
context 'when repository creation action fails' do
before do
allow_next_instance_of(Snippet) do |instance|
allow(instance).to receive(:create_repository).and_return(nil)
end
end
it 'does not create the snippet' do
expect { subject }.not_to change { Snippet.count }
end
it 'returns the error' do
expect(snippet.errors.full_messages).to include('Repository could not be created')
end
end
context 'when the commit action fails' do
before do
allow_next_instance_of(SnippetRepository) do |instance|
allow(instance).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError.new('foobar'))
end
end
it 'does not create the snippet' do
expect { subject }.not_to change { Snippet.count }
end
it 'does not create the repository' do
expect(snippet.repository_exists?).to be_falsey
end
it 'destroys the existing repository' do
expect(Repositories::DestroyService).to receive(:new).and_call_original
subject
end
it 'returns the error' do
response = subject
expect(response).to be_error
expect(response.payload[:snippet].errors.full_messages).to eq ['foobar']
end
end
context 'when snippet creation fails' do
let(:extra_opts) { { content: nil } }
it 'does not create repository' do
subject
expect do
subject
end.not_to change(Snippet, :count)
expect(snippet.repository_exists?).to be_falsey
end
end
context 'when feature flag :version_snippets is disabled' do
it 'does not create snippet repository' do
before do
stub_feature_flags(version_snippets: false)
end
it 'does not create snippet repository' do
expect do
subject
end.to change(Snippet, :count).by(1)
expect(snippet.repository_exists?).to be_falsey
end
it 'does not try to commit files' do
expect_next_instance_of(described_class) do |instance|
expect(instance).not_to receive(:create_commit)
end
subject
end
end
end
context 'when Project Snippet' do
context 'when ProjectSnippet' do
let_it_be(:project) { create(:project) }
before do
......@@ -185,7 +250,7 @@ describe Snippets::CreateService do
it_behaves_like 'spam check is performed'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository'
it_behaves_like 'creates repository and files'
end
context 'when PersonalSnippet' do
......@@ -196,7 +261,7 @@ describe Snippets::CreateService do
it_behaves_like 'spam check is performed'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository'
it_behaves_like 'creates repository and files'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DeployTokens::CreateService do
let(:project) { create(:project) }
RSpec.shared_examples 'a deploy token creation service' do
let(:user) { create(:user) }
let(:deploy_token_params) { attributes_for(:deploy_token) }
describe '#execute' do
subject { described_class.new(project, user, deploy_token_params).execute }
subject { described_class.new(entity, user, deploy_token_params).execute }
context 'when the deploy token is valid' do
it 'creates a new DeployToken' do
......@@ -16,7 +13,7 @@ describe DeployTokens::CreateService do
end
it 'creates a new ProjectDeployToken' do
expect { subject }.to change { ProjectDeployToken.count }.by(1)
expect { subject }.to change { deploy_token_class.count }.by(1)
end
it 'returns a DeployToken' do
......@@ -56,7 +53,7 @@ describe DeployTokens::CreateService do
end
it 'does not create a new ProjectDeployToken' do
expect { subject }.not_to change { ProjectDeployToken.count }
expect { subject }.not_to change { deploy_token_class.count }
end
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'a created deploy token' do
let(:deploy_token_params) do
{
name: 'deployer_token',
expires_at: 1.month.from_now.to_date.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: deploy_token_type
}
end
subject(:create_deploy_token) { post :create_deploy_token, params: create_entity_params.merge({ deploy_token: deploy_token_params }) }
it 'creates deploy token' do
expect { create_deploy_token }.to change { DeployToken.active.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
# frozen_string_literal: true
RSpec.shared_examples 'a deploy token in ci/cd settings' do
it 'view deploy tokens' do
within('.deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
expect(page).to have_content('read_registry')
end
end
it 'add a new deploy token' do
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
fill_in 'deploy_token_username', with: 'deployer'
check 'deploy_token_read_repository'
check 'deploy_token_read_registry'
click_button 'Create deploy token'
expect(page).to have_content("Your new #{entity_type} deploy token has been created")
within('.created-deploy-token-container') do
expect(page).to have_selector("input[name='deploy-token-user'][value='deployer']")
expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']")
end
end
end
......@@ -5,17 +5,18 @@ require 'spec_helper'
describe 'import/gitlab_projects/new.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { build_stubbed(:user, namespace: build_stubbed(:namespace)) }
let(:namespace) { build_stubbed(:namespace) }
let(:user) { build_stubbed(:user, namespace: namespace) }
before do
allow(view).to receive(:current_user).and_return(user)
end
context 'when the user has no other namespaces' do
it 'shows a namespace_id hidden field tag' do
it 'adds a namespace_id hidden field tag with the namespace id as value' do
render
expect(rendered).to have_css('input[name="namespace_id"]', count: 1, visible: false)
expect(rendered).to have_css("input[name='namespace_id'][value='#{namespace.id}']", count: 1, visible: false)
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment