Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-10 03:08:56 +00:00
parent 237ead18b9
commit 981fb44c36
53 changed files with 990 additions and 430 deletions

View File

@ -69,7 +69,6 @@ export default {
editButtonAttrs() { editButtonAttrs() {
return { return {
'data-testid': 'edit', 'data-testid': 'edit',
icon: 'pencil-square',
href: this.userPaths.edit, href: this.userPaths.edit,
}; };
}, },
@ -101,6 +100,7 @@ export default {
<gl-button <gl-button
v-else v-else
v-gl-tooltip="$options.i18n.edit" v-gl-tooltip="$options.i18n.edit"
icon="pencil-square"
v-bind="editButtonAttrs" v-bind="editButtonAttrs"
:aria-label="$options.i18n.edit" :aria-label="$options.i18n.edit"
/> />
@ -108,10 +108,9 @@ export default {
<div v-if="hasDropdownActions" class="gl-p-2"> <div v-if="hasDropdownActions" class="gl-p-2">
<gl-dropdown <gl-dropdown
v-gl-tooltip="$options.i18n.userAdministration"
data-testid="dropdown-toggle" data-testid="dropdown-toggle"
:text="$options.i18n.userAdministration" icon="ellipsis_v"
:text-sr-only="!showButtonLabels"
icon="ellipsis_h"
data-qa-selector="user_actions_dropdown_toggle" data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username" :data-qa-username="user.username"
no-caret no-caret

View File

@ -23,11 +23,21 @@ export default {
GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'], inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
computed: { computed: {
tooltip() { tooltip() {
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n; const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
if (!this.canAddCluster) {
return dropdownDisabledHint;
} else if (this.displayClusterAgents) {
return connectWithAgent;
}
return connectExistingCluster;
},
shouldTriggerModal() {
return this.canAddCluster && this.displayClusterAgents;
}, },
}, },
}; };
@ -37,15 +47,16 @@ export default {
<div class="nav-controls gl-ml-auto"> <div class="nav-controls gl-ml-auto">
<gl-dropdown <gl-dropdown
ref="dropdown" ref="dropdown"
v-gl-modal-directive="canAddCluster && $options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip" v-gl-tooltip="tooltip"
category="primary" category="primary"
variant="confirm" variant="confirm"
:text="$options.i18n.actionsButton" :text="$options.i18n.actionsButton"
:disabled="!canAddCluster" :disabled="!canAddCluster"
split :split="displayClusterAgents"
right right
> >
<template v-if="displayClusterAgents">
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item <gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
@ -55,6 +66,8 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
</template>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop> <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createNewCluster }} {{ $options.i18n.createNewCluster }}
</gl-dropdown-item> </gl-dropdown-item>

View File

@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import {
CLUSTERS_TABS, CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST, MAX_CLUSTERS_LIST,
MAX_LIST_COUNT, MAX_LIST_COUNT,
AGENT, AGENT,
@ -29,6 +30,7 @@ export default {
}, },
CLUSTERS_TABS, CLUSTERS_TABS,
mixins: [trackingMixin], mixins: [trackingMixin],
inject: ['displayClusterAgents'],
props: { props: {
defaultBranchName: { defaultBranchName: {
default: '.noBranch', default: '.noBranch',
@ -42,6 +44,11 @@ export default {
maxAgents: MAX_CLUSTERS_LIST, maxAgents: MAX_CLUSTERS_LIST,
}; };
}, },
computed: {
clusterTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
},
},
watch: { watch: {
selectedTabIndex(val) { selectedTabIndex(val) {
this.onTabChange(val); this.onTabChange(val);
@ -49,10 +56,10 @@ export default {
}, },
methods: { methods: {
setSelectedTab(tabName) { setSelectedTab(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName); this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
}, },
onTabChange(tab) { onTabChange(tab) {
const tabName = CLUSTERS_TABS[tab].queryParamValue; const tabName = this.clusterTabs[tab].queryParamValue;
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST; this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
this.track(EVENT_ACTIONS_CHANGE, { property: tabName }); this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
@ -69,7 +76,7 @@ export default {
lazy lazy
> >
<gl-tab <gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS" v-for="(tab, idx) in clusterTabs"
:key="idx" :key="idx"
:title="tab.title" :title="tab.title"
:query-param-value="tab.queryParamValue" :query-param-value="tab.queryParamValue"

View File

@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6; export const MAX_CLUSTERS_LIST = 6;
export const CERTIFICATE_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [ export const CLUSTERS_TABS = [
{ {
title: s__('ClusterAgents|All'), title: s__('ClusterAgents|All'),
@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [
component: 'agents', component: 'agents',
queryParamValue: 'agent', queryParamValue: 'agent',
}, },
{ CERTIFICATE_TAB,
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
]; ];
export const CLUSTERS_ACTIONS = { export const CLUSTERS_ACTIONS = {

View File

@ -1,13 +1,61 @@
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters'; import { parseBoolean } from '~/lib/utils/common_utils';
import loadMainView from './load_main_view'; import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(GlToast); Vue.use(GlToast);
Vue.use(VueApollo); Vue.use(VueApollo);
export default () => { export default () => {
loadClusters(Vue); const el = document.querySelector('.js-clusters-main-view');
loadMainView(Vue, VueApollo);
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
displayClusterAgents,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
displayClusterAgents: parseBoolean(displayClusterAgents),
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
}; };

View File

@ -1,25 +0,0 @@
import Clusters from './components/clusters.vue';
import { createStore } from './store';
export default (Vue) => {
const el = document.querySelector('#js-clusters-list-app');
if (!el) {
return null;
}
const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
return new Vue({
el,
provide: {
emptyStateHelpText,
newClusterPath,
clustersEmptyStateImage,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
},
});
};

View File

@ -1,57 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(VueApollo);
export default () => {
const el = document.querySelector('.js-clusters-main-view');
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
};

View File

@ -5,7 +5,7 @@ import createStore from './store';
Vue.use(Translate); Vue.use(Translate);
export const initHeaderSearchApp = () => { export const initHeaderSearchApp = (search = '') => {
const el = document.getElementById('js-header-search'); const el = document.getElementById('js-header-search');
if (!el) { if (!el) {
@ -18,7 +18,7 @@ export const initHeaderSearchApp = () => {
return new Vue({ return new Vue({
el, el,
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }), store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
render(createElement) { render(createElement) {
return createElement(HeaderSearchApp); return createElement(HeaderSearchApp);
}, },

View File

@ -13,11 +13,12 @@ export const getStoreConfig = ({
mrPath, mrPath,
autocompletePath, autocompletePath,
searchContext, searchContext,
search,
}) => ({ }) => ({
actions, actions,
getters, getters,
mutations, mutations,
state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }), state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
}); });
const createStore = (config) => new Vuex.Store(getStoreConfig(config)); const createStore = (config) => new Vuex.Store(getStoreConfig(config));

View File

@ -1,10 +1,17 @@
const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }) => ({ const createState = ({
searchPath, searchPath,
issuesPath, issuesPath,
mrPath, mrPath,
autocompletePath, autocompletePath,
searchContext, searchContext,
search: '', search,
}) => ({
searchPath,
issuesPath,
mrPath,
autocompletePath,
searchContext,
search,
autocompleteOptions: [], autocompleteOptions: [],
autocompleteError: false, autocompleteError: false,
loading: false, loading: false,

View File

@ -31,7 +31,10 @@ export default {
computed: { computed: {
actionPrimary() { actionPrimary() {
return { return {
attributes: { variant: 'danger' }, attributes: {
variant: 'danger',
'data-qa-selector': 'confirm_delete_issue_button',
},
text: this.title, text: this.title,
}; };
}, },

View File

@ -290,6 +290,7 @@ export default {
class="gl-display-none gl-sm-display-inline-flex! gl-ml-3" class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
icon="ellipsis_v" icon="ellipsis_v"
category="tertiary" category="tertiary"
data-qa-selector="issue_actions_ellipsis_dropdown"
:text="dropdownText" :text="dropdownText"
:text-sr-only="true" :text-sr-only="true"
data-testid="desktop-dropdown" data-testid="desktop-dropdown"
@ -323,6 +324,7 @@ export default {
<gl-dropdown-item <gl-dropdown-item
v-gl-modal="$options.deleteModalId" v-gl-modal="$options.deleteModalId"
variant="danger" variant="danger"
data-qa-selector="delete_issue_button"
@click="track('click_dropdown')" @click="track('click_dropdown')"
> >
{{ deleteButtonText }} {{ deleteButtonText }}

View File

@ -116,16 +116,18 @@ function deferredInitialisation() {
); );
} }
const search = document.querySelector('#search'); const searchInputBox = document.querySelector('#search');
if (search) { if (searchInputBox) {
search.addEventListener( searchInputBox.addEventListener(
'focus', 'focus',
() => { () => {
if (gon.features?.newHeaderSearch) { if (gon.features?.newHeaderSearch) {
import(/* webpackChunkName: 'globalSearch' */ '~/header_search') import(/* webpackChunkName: 'globalSearch' */ '~/header_search')
.then(async ({ initHeaderSearchApp }) => { .then(async ({ initHeaderSearchApp }) => {
await initHeaderSearchApp(); // In case the user started searching before we bootstrapped, let's pass the search along.
document.querySelector('#search').focus(); const initialSearchValue = searchInputBox.value;
await initHeaderSearchApp(initialSearchValue);
searchInputBox.focus();
}) })
.catch(() => {}); .catch(() => {});
} else { } else {

View File

@ -360,27 +360,10 @@
} }
> li { > li {
// TODO: Remove this block once all sidebar badges use gl_badge_tag
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
.badge.badge-pill:not(.gl-badge) {
@include gl-rounded-lg;
@include gl-py-1;
@include gl-px-3;
background-color: $blue-100;
color: $blue-700;
}
&.active { &.active {
.sidebar-sub-level-items:not(.is-fly-out-only) { .sidebar-sub-level-items:not(.is-fly-out-only) {
display: block; display: block;
} }
// TODO: Remove this block once all sidebar badges use gl_badge_tag
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
.badge.badge-pill:not(.gl-badge) {
@include gl-font-weight-normal;
color: $blue-700;
}
} }
} }

View File

@ -1380,24 +1380,11 @@ input {
border-radius: 4px; border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
border-radius: 0.5rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
background-color: #064787;
color: #9dc7f1;
}
.sidebar-top-level-items .sidebar-top-level-items
> li.active > li.active
.sidebar-sub-level-items:not(.is-fly-out-only) { .sidebar-sub-level-items:not(.is-fly-out-only) {
display: block; display: block;
} }
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
font-weight: 400;
color: #9dc7f1;
}
.sidebar-top-level-items li > a.gl-link { .sidebar-top-level-items li > a.gl-link {
color: #fafafa; color: #fafafa;
} }

View File

@ -1365,24 +1365,11 @@ input {
border-radius: 4px; border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
} }
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
border-radius: 0.5rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
background-color: #cbe2f9;
color: #0b5cad;
}
.sidebar-top-level-items .sidebar-top-level-items
> li.active > li.active
.sidebar-sub-level-items:not(.is-fly-out-only) { .sidebar-sub-level-items:not(.is-fly-out-only) {
display: block; display: block;
} }
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
font-weight: 400;
color: #0b5cad;
}
.sidebar-top-level-items li > a.gl-link { .sidebar-top-level-items li > a.gl-link {
color: #303030; color: #303030;
} }

View File

@ -12,10 +12,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# Overridden from Doorkeeper::AuthorizationsController to # Overridden from Doorkeeper::AuthorizationsController to
# include the call to session.delete # include the call to session.delete
def new def new
logger.info("#{self.class.name}#new: pre_auth_params['scope'] = #{pre_auth_params['scope'].inspect}")
if pre_auth.authorizable? if pre_auth.authorizable?
logger.info("#{self.class.name}#new: pre_auth.scopes = #{pre_auth.scopes.to_a.inspect}")
if skip_authorization? || matching_token? if skip_authorization? || matching_token?
auth = authorization.authorize auth = authorization.authorize
parsed_redirect_uri = URI.parse(auth.redirect_uri) parsed_redirect_uri = URI.parse(auth.redirect_uri)
@ -46,15 +43,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
auth_type = params.delete('gl_auth_type') auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login' return unless auth_type == 'login'
logger.info("#{self.class.name}: BEFORE application has read_user: #{application_has_read_user_scope?}")
logger.info("#{self.class.name}: BEFORE scope = #{params['scope'].inspect}")
ensure_read_user_scope! ensure_read_user_scope!
params['scope'] = Gitlab::Auth::READ_USER_SCOPE.to_s if application_has_read_user_scope? params['scope'] = Gitlab::Auth::READ_USER_SCOPE.to_s if application_has_read_user_scope?
logger.info("#{self.class.name}: AFTER application has read_user: #{application_has_read_user_scope?}")
logger.info("#{self.class.name}: AFTER scope = #{params['scope'].inspect}")
end end
# Configure the application to support read_user scope, if it already # Configure the application to support read_user scope, if it already

View File

@ -28,8 +28,10 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'), clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_help_text: clusterable.empty_state_help_text, empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path(tab: 'create'), new_cluster_path: clusterable.new_path(tab: 'create'),
add_cluster_path: clusterable.new_path(tab: 'add'),
can_add_cluster: clusterable.can_add_cluster?.to_s, can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s
} }
end end
@ -38,7 +40,6 @@ module ClustersHelper
default_branch_name: clusterable.default_branch, default_branch_name: clusterable.default_branch,
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'), empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable.full_path, project_path: clusterable.full_path,
add_cluster_path: clusterable.new_path(tab: 'add'),
kas_address: Gitlab::Kas.external_url, kas_address: Gitlab::Kas.external_url,
gitlab_version: Gitlab.version_info gitlab_version: Gitlab.version_info
}.merge(js_clusters_list_data(clusterable)) }.merge(js_clusters_list_data(clusterable))

View File

@ -10,11 +10,41 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
scope :priority_order, -> { order('last_incremental_run_at ASC NULLS FIRST') } scope :priority_order, -> { order('last_incremental_run_at ASC NULLS FIRST') }
scope :enabled, -> { where('enabled IS TRUE') } scope :enabled, -> { where('enabled IS TRUE') }
def estimated_next_run_at
return unless enabled
return if last_incremental_run_at.nil?
estimation = (last_incremental_run_at - earliest_last_run_at) + average_aggregation_duration
estimation < 1 ? nil : estimation.from_now
end
def self.safe_create_for_group(group) def self.safe_create_for_group(group)
top_level_group = group.root_ancestor top_level_group = group.root_ancestor
return if Analytics::CycleAnalytics::Aggregation.exists?(group_id: top_level_group.id) aggregation = find_by(group_id: top_level_group.id)
return aggregation if aggregation.present?
insert({ group_id: top_level_group.id }, unique_by: :group_id) insert({ group_id: top_level_group.id }, unique_by: :group_id)
find_by(group_id: top_level_group.id)
end
private
def average_aggregation_duration
return 0.seconds if incremental_runtimes_in_seconds.empty?
average = incremental_runtimes_in_seconds.sum.fdiv(incremental_runtimes_in_seconds.size)
average.seconds
end
def earliest_last_run_at
max = self.class.select(:last_incremental_run_at)
.where(enabled: true)
.where.not(last_incremental_run_at: nil)
.priority_order
.limit(1)
.to_sql
connection.select_value("(#{max})")
end end
def self.load_batch(last_run_at, batch_size = 100) def self.load_batch(last_run_at, batch_size = 100)

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module RunnersTokenPrefixable
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
end

View File

@ -22,11 +22,6 @@ class Group < Namespace
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
def self.sti_name def self.sti_name
'Group' 'Group'
end end
@ -124,7 +119,7 @@ class Group < Namespace
add_authentication_token_field :runners_token, add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
prefix: ->(instance) { instance.runners_token_prefix } prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
after_create :post_create_hook after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
@ -678,13 +673,9 @@ class Group < Namespace
ensure_runners_token! ensure_runners_token!
end end
def runners_token_prefix
Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
end
override :format_runners_token override :format_runners_token
def format_runners_token(token) def format_runners_token(token)
"#{runners_token_prefix}#{token}" "#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
end end
def project_creation_level def project_creation_level

View File

@ -90,11 +90,6 @@ class Project < ApplicationRecord
DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}' DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}'
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
default_value_for :packages_enabled, true default_value_for :packages_enabled, true
@ -117,7 +112,7 @@ class Project < ApplicationRecord
add_authentication_token_field :runners_token, add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
prefix: ->(instance) { instance.runners_token_prefix } prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@ -1887,13 +1882,9 @@ class Project < ApplicationRecord
ensure_runners_token! ensure_runners_token!
end end
def runners_token_prefix
Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
end
override :format_runners_token override :format_runners_token
def format_runners_token(token) def format_runners_token(token)
"#{runners_token_prefix}#{token}" "#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
end end
def pages_deployed? def pages_deployed?

View File

@ -27,8 +27,6 @@
= render_if_exists 'admin/users/gma_user_badge' = render_if_exists 'admin/users/gma_user_badge'
.gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2 .gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
- if @user != current_user - if @user != current_user
.gl-p-2 .gl-p-2
- if impersonation_enabled? && @user.can?(:log_in) - if impersonation_enabled? && @user.can?(:log_in)
@ -36,6 +34,8 @@
- if can_force_email_confirmation?(@user) - if can_force_email_confirmation?(@user)
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) } %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user') = _('Confirm user')
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
= gl_tabs_nav do = gl_tabs_nav do
= gl_tab_link_to _("Account"), admin_user_path(@user) = gl_tab_link_to _("Account"), admin_user_path(@user)
= gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user) = gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user)

View File

@ -7,4 +7,4 @@
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2 %span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate") = s_("ClusterIntegration|Connect cluster with certificate")
#js-clusters-list-app{ data: js_clusters_list_data(clusterable) } .js-clusters-main-view{ data: js_clusters_list_data(clusterable) }

View File

@ -0,0 +1,24 @@
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
'mr-path' => merge_requests_dashboard_path,
'autocomplete-path' => search_autocomplete_path } }
= form_tag search_path, method: :get do |_f|
.gl-search-box-by-type
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', autocomplete: 'off' }
= hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group]
= hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project]
- if header_search_context[:group] || header_search_context[:project]
= hidden_field_tag :scope, header_search_context[:scope]
= hidden_field_tag :search_code, header_search_context[:code_search]
= hidden_field_tag :snippets, header_search_context[:for_snippets]
= hidden_field_tag :repository_ref, header_search_context[:ref]
= hidden_field_tag :nav_source, 'navbar'
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search'

View File

@ -41,14 +41,7 @@
%li.nav-item.header-search-new.d-none.d-lg-block.m-auto %li.nav-item.header-search-new.d-none.d-lg-block.m-auto
- unless current_controller?(:search) - unless current_controller?(:search)
- if Feature.enabled?(:new_header_search) - if Feature.enabled?(:new_header_search)
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json, = render 'layouts/header_search'
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
'mr-path' => merge_requests_dashboard_path,
'autocomplete-path' => search_autocomplete_path } }
.gl-search-box-by-type
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%input{ type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', id: 'search', autocomplete: 'off' }
- else - else
= render 'layouts/search' = render 'layouts/search'
%li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' } %li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' }

View File

@ -1,8 +0,0 @@
---
name: groups_runners_token_prefix
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
milestone: '14.9'
type: development
group: group::database
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: projects_runners_token_prefix
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
milestone: '14.9'
type: development
group: group::database
default_enabled: true

View File

@ -377,7 +377,7 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked. The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked.
```shell ```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -repository <repository> -replicate-immediately sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -authoritative-storage <storage-name> -repository <repository> -replicate-immediately
``` ```
- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following: - `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following:

View File

@ -479,3 +479,19 @@ ssh: Could not resolve hostname gitlab.example.com: nodename nor servname provid
``` ```
If you receive this error, restart your terminal and try the command again. If you receive this error, restart your terminal and try the command again.
### `Key enrollment failed: invalid format` error
You may receive the following error when [generating an SSH key pair for a FIDO/U2F hardware security key](#generate-an-ssh-key-pair-for-a-fidou2f-hardware-security-key):
```shell
Key enrollment failed: invalid format
```
You can troubleshoot this by trying the following:
- Run the `ssh-keygen` command using `sudo`.
- Verify your IDO/U2F hardware security key supports
the key type provided.
- Verify the version of OpenSSH is 8.2 or greater by
running `ssh -v`.

View File

@ -181,7 +181,7 @@ table.supported-languages ul {
</tr> </tr>
<tr> <tr>
<td rowspan="2">Java</td> <td rowspan="2">Java</td>
<td rowspan="2">8, 11, 13, 14, 15, or 16</td> <td rowspan="2">8, 11, 13, 14, 15, 16, or 17</td>
<td><a href="https://gradle.org/">Gradle</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-1">1</a></b></sup></td> <td><a href="https://gradle.org/">Gradle</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-1">1</a></b></sup></td>
<td> <td>
<ul> <ul>
@ -335,26 +335,60 @@ To support the following package managers, the GitLab analyzers proceed in two s
1. Execute the package manager or a specific task, to export the dependency information. 1. Execute the package manager or a specific task, to export the dependency information.
1. Parse the exported dependency information. 1. Parse the exported dependency information.
| Package Manager | Preinstalled Versions | Tested Versions | | Package Manager | Pre-installed Versions | Tested Versions |
| ------ | ------ | ------ | | ------ | ------ | ------ |
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) | | Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) | | sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) | | Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5) | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3) | | Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup> | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L289-297)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup>, [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3), [7.3](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-7-3/gradle/wrapper/gradle-wrapper.properties#L3), [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L299-317)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup> |
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) | | setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) | | pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) | | Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-4">4</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
<!-- markdownlint-disable MD044 --> <!-- markdownlint-disable MD044 -->
<ol> <ol>
<li> <li>
<a id="exported-dependency-information-notes-1"></a> <a id="exported-dependency-information-notes-1"></a>
<p> <p>
The installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a> The pre-installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>.
</p> </p>
</li> </li>
<li> <li>
<a id="exported-dependency-information-notes-2"></a> <a id="exported-dependency-information-notes-2"></a>
<p>
Different versions of Java require different versions of Gradle. The versions of Gradle listed in the above table are pre-installed
in the analyzer image. The version of Gradle used by the analyzer depends on whether your project uses a <code>gradlew</code>
(Gradle wrapper) file or not:
</p>
<ul>
<li>
<p>
If your project <i>does not use</i> a <code>gradlew</code> file, then the analyzer automatically switches to one of the
pre-installed Gradle versions, based on the version of Java specified by the
<a href="#configuring-specific-analyzers-used-by-dependency-scanning"><code>DS_JAVA_VERSION</code></a> variable.
</p>
<p>You can view the
<a href="https://docs.gradle.org/current/userguide/compatibility.html#java">Gradle Java compatibility matrix</a> to see which version
of Gradle is selected for each Java version. Note that we only support switching to one of these pre-installed Gradle versions
for Java versions 13 to 17.
</p>
</li>
<li>
<p>
If your project <i>does use</i> a <code>gradlew</code> file, then the version of Gradle pre-installed in the analyzer image is
ignored, and the version specified in your <code>gradlew</code> file is used instead.
</p>
</li>
</ul>
</li>
<li>
<a id="exported-dependency-information-notes-3"></a>
<p>
These tests confirms that if a <code>gradlew</code> file does not exist, the version of <code>Gradle</code> pre-installed in the analyzer image is used.
</p>
</li>
<li>
<a id="exported-dependency-information-notes-4"></a>
<p> <p>
This test confirms that if a <code>Pipfile.lock</code> file is found, it will be used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file. This test confirms that if a <code>Pipfile.lock</code> file is found, it will be used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file.
</p> </p>
@ -563,7 +597,7 @@ The following variables are used for configuring specific analyzers (used for a
| `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. | | `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. |
| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. | | `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
| `GEMNASIUM_LIBRARY_SCAN_ENABLED` | `gemnasium` | `"true"` | Enable detecting vulnerabilities in vendored JavaScript libraries. For now, `gemnasium` leverages [`Retire.js`](https://github.com/RetireJS/retire.js) to do this job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350512) in GitLab 14.8. | | `GEMNASIUM_LIBRARY_SCAN_ENABLED` | `gemnasium` | `"true"` | Enable detecting vulnerabilities in vendored JavaScript libraries. For now, `gemnasium` leverages [`Retire.js`](https://github.com/RetireJS/retire.js) to do this job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350512) in GitLab 14.8. |
| `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`. | | `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`, `17`. |
| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that are passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repositories). | | `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that are passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repositories). |
| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that are passed to `gradle` by the analyzer. | | `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that are passed to `gradle` by the analyzer. |
| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer passes to `sbt`. | | `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer passes to `sbt`. |

View File

@ -217,7 +217,11 @@ cannot change them:
This ensures that your job uses the settings you intend and that they are not overridden by This ensures that your job uses the settings you intend and that they are not overridden by
project-level pipelines. project-level pipelines.
##### Avoid parent and child pipelines ##### Avoid parent and child pipelines in GitLab 14.7 and earlier
NOTE:
This advice does not apply to GitLab 14.8 and later because [a fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78878) added
compatibility for combining compliance pipelines, and parent and child pipelines.
Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline. triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.

View File

@ -86,6 +86,7 @@ module Gitlab
def to_data_attributes def to_data_attributes
{}.tap do |attrs| {}.tap do |attrs|
attrs[:aggregation] = aggregation_attributes if group
attrs[:group] = group_data_attributes if group attrs[:group] = group_data_attributes if group
attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream
attrs[:created_after] = created_after.to_date.iso8601 attrs[:created_after] = created_after.to_date.iso8601
@ -103,6 +104,15 @@ module Gitlab
private private
def aggregation_attributes
aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
{
enabled: aggregation.enabled.to_s,
last_run_at: aggregation.last_incremental_run_at,
next_run_at: aggregation.estimated_next_run_at
}
end
def group_data_attributes def group_data_attributes
{ {
id: group.id, id: group.id,

View File

@ -3,6 +3,7 @@
module Gitlab module Gitlab
class OmniauthInitializer class OmniauthInitializer
OAUTH2_TIMEOUT_SECONDS = 10 OAUTH2_TIMEOUT_SECONDS = 10
ConfigurationError = Class.new(StandardError)
def initialize(devise_config) def initialize(devise_config)
@devise_config = devise_config @devise_config = devise_config
@ -75,16 +76,29 @@ module Gitlab
provider_arguments << provider[argument] if provider[argument] provider_arguments << provider[argument] if provider[argument]
end end
case provider['args'] arguments = provider.fetch('args', {})
when Array
# An Array from the configuration will be expanded.
provider_arguments.concat provider['args']
when Hash
defaults = provider_defaults(provider) defaults = provider_defaults(provider)
hash_arguments = provider['args'].deep_symbolize_keys.deep_merge(defaults)
case arguments
when Array
# An Array from the configuration will be expanded
provider_arguments.concat arguments
provider_arguments << defaults unless defaults.empty?
when Hash
hash_arguments = arguments.deep_symbolize_keys.deep_merge(defaults)
normalized = normalize_hash_arguments(hash_arguments)
# A Hash from the configuration will be passed as is. # A Hash from the configuration will be passed as is.
provider_arguments << normalize_hash_arguments(hash_arguments) provider_arguments << normalized unless normalized.empty?
else
# this will prevent the application from starting in development mode.
# we still set defaults, and let the application start in prod.
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
ConfigurationError.new("Arguments were provided for #{provider['name']}, but not as an array or a hash"),
provider_name: provider['name'],
arguments_type: arguments.class.name
)
provider_arguments << defaults unless defaults.empty?
end end
provider_arguments provider_arguments

View File

@ -23,6 +23,12 @@ module QA
end end
end end
def filter_by_name(name)
within_element(:project_filter_form) do
fill_in :name, with: name
end
end
def go_to_project(name) def go_to_project(name)
filter_by_name(name) filter_by_name(name)
@ -40,14 +46,6 @@ module QA
def clear_project_filter def clear_project_filter
fill_element(:project_filter_form, "") fill_element(:project_filter_form, "")
end end
private
def filter_by_name(name)
within_element(:project_filter_form) do
fill_in :name, with: name
end
end
end end
end end
end end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module QA
module Page
module Modal
class DeleteIssue < Base
view 'app/assets/javascripts/issues/show/components/delete_issue_modal.vue' do
element :confirm_delete_issue_button, required: true
end
def confirm_delete_issue
click_element :confirm_delete_issue_button
end
end
end
end
end

View File

@ -18,6 +18,8 @@ module QA
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
element :close_issue_button element :close_issue_button
element :reopen_issue_button element :reopen_issue_button
element :issue_actions_ellipsis_dropdown
element :delete_issue_button
end end
view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
@ -69,6 +71,20 @@ module QA
def has_reopen_issue_button? def has_reopen_issue_button?
has_element?(:reopen_issue_button) has_element?(:reopen_issue_button)
end end
def has_delete_issue_button?
click_element(:issue_actions_ellipsis_dropdown)
has_element?(:delete_issue_button)
end
def delete_issue
click_element(:issue_actions_ellipsis_dropdown)
click_element(:delete_issue_button, Page::Modal::DeleteIssue)
Page::Modal::DeleteIssue.perform(&:confirm_delete_issue)
wait_for_requests
end
end end
end end
end end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage' do
describe 'Personal project permissions' do
let!(:owner) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) }
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
project.api_client = owner_api_client
project.name = 'qa-owner-personal-project'
project.personal_namespace = owner.username
end
end
after do
project&.remove_via_api!
end
context 'when user is added as Owner' do
let(:issue) do
Resource::Issue.fabricate_via_api! do |issue|
issue.api_client = owner_api_client
issue.project = project
issue.title = 'Test Owner deletes issue'
end
end
before do
Flow::Login.sign_in(as: owner)
end
it "has Owner role with Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Owner')
end
expect_owner_permissions_allow_delete_issue
end
end
context 'when user is added as Maintainer' do
let(:maintainer) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
let(:issue) do
Resource::Issue.fabricate_via_api! do |issue|
issue.api_client = owner_api_client
issue.project = project
issue.title = 'Test Maintainer deletes issue'
end
end
before do
project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in(as: maintainer)
end
it "has Maintainer role without Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Maintainer')
end
expect_maintainer_permissions_do_not_allow_delete_issue
end
end
private
def expect_owner_permissions_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end.not_to raise_error
end
def expect_maintainer_permissions_do_not_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform do |issue|
expect(issue).not_to have_delete_issue_button
end
end.not_to raise_error
end
end
end
end

View File

@ -18,6 +18,9 @@ require_relative '../config/settings'
require_relative 'support/rspec' require_relative 'support/rspec'
require 'active_support/all' require 'active_support/all'
require_relative 'simplecov_env'
SimpleCovEnv.start!
unless ActiveSupport::Dependencies.autoload_paths.frozen? unless ActiveSupport::Dependencies.autoload_paths.frozen?
ActiveSupport::Dependencies.autoload_paths << 'lib' ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib' ActiveSupport::Dependencies.autoload_paths << 'ee/lib'

View File

@ -72,6 +72,10 @@ RSpec.describe 'Global search' do
# TODO: Remove this along with feature flag #339348 # TODO: Remove this along with feature flag #339348
stub_feature_flags(new_header_search: true) stub_feature_flags(new_header_search: true)
visit dashboard_projects_path visit dashboard_projects_path
# intialize javascript loaded input search input field
find('#search').click
find('body').click
end end
it 'renders updated search bar' do it 'renders updated search bar' do

View File

@ -77,6 +77,12 @@ describe('AdminUserActions component', () => {
expect(findActionsDropdown().exists()).toBe(true); expect(findActionsDropdown().exists()).toBe(true);
}); });
it('renders the tooltip', () => {
const tooltip = getBinding(findActionsDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(I18N_USER_ACTIONS.userAdministration);
});
describe('when there are actions that require confirmation', () => { describe('when there are actions that require confirmation', () => {
beforeEach(() => { beforeEach(() => {
initComponent({ actions: CONFIRMATION_ACTIONS }); initComponent({ actions: CONFIRMATION_ACTIONS });
@ -152,7 +158,7 @@ describe('AdminUserActions component', () => {
describe('when `showButtonLabels` prop is `false`', () => { describe('when `showButtonLabels` prop is `false`', () => {
beforeEach(() => { beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] }); initComponent({ actions: [EDIT] });
}); });
it('does not render "Edit" button label', () => { it('does not render "Edit" button label', () => {
@ -163,16 +169,11 @@ describe('AdminUserActions component', () => {
expect(tooltip).toBeDefined(); expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit); expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit);
}); });
it('does not render "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(true);
});
}); });
describe('when `showButtonLabels` prop is `true`', () => { describe('when `showButtonLabels` prop is `true`', () => {
beforeEach(() => { beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true }); initComponent({ actions: [EDIT], showButtonLabels: true });
}); });
it('renders "Edit" button label', () => { it('renders "Edit" button label', () => {
@ -181,10 +182,5 @@ describe('AdminUserActions component', () => {
expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit); expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit);
expect(tooltip).not.toBeDefined(); expect(tooltip).not.toBeDefined();
}); });
it('renders "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(false);
});
}); });
}); });

View File

@ -14,10 +14,13 @@ describe('ClustersActionsComponent', () => {
newClusterPath, newClusterPath,
addClusterPath, addClusterPath,
canAddCluster: true, canAddCluster: true,
displayClusterAgents: true,
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link'); const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link'); const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link'); const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
@ -47,26 +50,11 @@ describe('ClustersActionsComponent', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton); expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
}); });
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders correct href attributes for the links', () => { it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath); expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath); expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
}); });
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
describe('when user cannot add clusters', () => { describe('when user cannot add clusters', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ canAddCluster: false }); createWrapper({ canAddCluster: false });
@ -80,5 +68,67 @@ describe('ClustersActionsComponent', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip'); const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint); expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
}); });
it('does not bind split dropdown button', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
});
describe('when on project level', () => {
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItemIds()).toEqual([
'connect-new-agent-link',
'new-cluster-link',
'connect-cluster-link',
]);
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders a dropdown with 2 actions items', () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
});
it('does not show split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(false);
});
it('does not bind dropdown button to modal', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
}); });
}); });

View File

@ -7,6 +7,7 @@ import {
AGENT, AGENT,
CERTIFICATE_BASED, CERTIFICATE_BASED,
CLUSTERS_TABS, CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST, MAX_CLUSTERS_LIST,
MAX_LIST_COUNT, MAX_LIST_COUNT,
EVENT_LABEL_TABS, EVENT_LABEL_TABS,
@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName, defaultBranchName,
}; };
beforeEach(() => { const createWrapper = ({ displayClusterAgents }) => {
wrapper = shallowMountExtended(ClustersMainView, { wrapper = shallowMountExtended(ClustersMainView, {
propsData, propsData,
provide: { displayClusterAgents },
}); });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); };
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
@ -40,6 +41,12 @@ describe('ClustersMainViewComponent', () => {
const findComponent = () => wrapper.findByTestId('clusters-tab-component'); const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal); const findModal = () => wrapper.findComponent(InstallAgentModal);
describe('when on project level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => { it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true); expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true); expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
@ -68,7 +75,9 @@ describe('ClustersMainViewComponent', () => {
tab | tabName tab | tabName
${'1'} | ${AGENT} ${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED} ${'2'} | ${CERTIFICATE_BASED}
`('when the child component emits the tab change event for $tabName tab', ({ tab, tabName }) => { `(
'when the child component emits the tab change event for $tabName tab',
({ tab, tabName }) => {
beforeEach(() => { beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName); findComponent().vm.$emit('changeTab', tabName);
}); });
@ -76,7 +85,8 @@ describe('ClustersMainViewComponent', () => {
it(`changes the tab value to ${tab}`, () => { it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab); expect(findTabs().attributes('value')).toBe(tab);
}); });
}); },
);
describe.each` describe.each`
tab | tabName | maxAgents tab | tabName | maxAgents
@ -102,4 +112,19 @@ describe('ClustersMainViewComponent', () => {
}); });
}); });
}); });
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
it('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
});
});
}); });

View File

@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do
expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create") expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
end end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
context 'user has no permissions to create a cluster' do context 'user has no permissions to create a cluster' do
it 'displays that user can\'t add cluster' do it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false") expect(subject[:can_add_cluster]).to eq("false")
@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do
it 'doesn\'t display empty state help text' do it 'doesn\'t display empty state help text' do
expect(subject[:empty_state_help_text]).to be_nil expect(subject[:empty_state_help_text]).to be_nil
end end
it 'displays display_cluster_agents as true' do
expect(subject[:display_cluster_agents]).to eq("true")
end
end end
context 'group cluster' do context 'group cluster' do
@ -123,6 +131,10 @@ RSpec.describe ClustersHelper do
it 'displays empty state help text' do it 'displays empty state help text' do
expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')) expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.'))
end end
it 'displays display_cluster_agents as false' do
expect(subject[:display_cluster_agents]).to eq("false")
end
end end
end end
@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do
expect(subject[:project_path]).to eq(project.full_path) expect(subject[:project_path]).to eq(project.full_path)
end end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
it 'displays kas address' do it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url) expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end end

View File

@ -5,7 +5,161 @@ require 'spec_helper'
RSpec.describe Gitlab::OmniauthInitializer do RSpec.describe Gitlab::OmniauthInitializer do
let(:devise_config) { class_double(Devise) } let(:devise_config) { class_double(Devise) }
subject { described_class.new(devise_config) } subject(:initializer) { described_class.new(devise_config) }
describe '.arguments_for' do
let(:devise_config) { nil }
let(:arguments) { initializer.send(:arguments_for, provider) }
context 'when there are no args at all' do
let(:provider) { { 'name' => 'unknown' } }
it 'returns an empty array' do
expect(arguments).to eq []
end
end
context 'when there is an app_id and an app_secret' do
let(:provider) { { 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2 } }
it 'includes both of them, in positional order' do
expect(arguments).to eq [1, 2]
end
end
context 'when there is an app_id and an app_secret, and an array of args' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
}
end
it 'concatenates the args on the end' do
expect(arguments).to eq [1, 2, 'one', 'two', 'three']
end
end
context 'when there is an app_id and an app_secret, and an array of args, and default values' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, 'one', 'two', 'three', { default_arg: :some_value }]
end
end
context 'when there is an app_id and an app_secret, and a hash of args' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
}
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { foo: 100, bar: 200, nested: { value: 300 } }]
end
end
context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { default_arg: :some_value, foo: 100, bar: 200, nested: { value: 300 } }]
end
end
context 'when there is an app_id and an app_secret, no args, and default values' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { default_arg: :some_value }]
end
end
context 'when there are args, of an unsupported type' do
let(:provider) do
{
'name' => 'unknown',
'args' => 1
}
end
context 'when there are default arguments' do
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'tracks a configuration error' do
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
expect(arguments)
.to eq [{ default_arg: :some_value }]
end
end
context 'when there are no default arguments' do
it 'tracks a configuration error' do
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
expect(arguments).to be_empty
end
end
end
end
describe '#execute' do describe '#execute' do
it 'configures providers from array' do it 'configures providers from array' do
@ -105,9 +259,48 @@ RSpec.describe Gitlab::OmniauthInitializer do
it 'configures defaults for gitlab' do it 'configures defaults for gitlab' do
conf = { conf = {
'name' => 'gitlab', 'name' => 'gitlab',
"args" => {} "args" => { 'client_options' => { 'site' => generate(:url) } }
} }
expect(devise_config).to receive(:omniauth).with(
:gitlab,
client_options: { site: conf.dig('args', 'client_options', 'site') },
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'configures defaults for gitlab, when arguments are not provided' do
conf = { 'name' => 'gitlab' }
expect(devise_config).to receive(:omniauth).with(
:gitlab,
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'configures defaults for gitlab, when array arguments are provided' do
conf = { 'name' => 'gitlab', 'args' => ['a'] }
expect(devise_config).to receive(:omniauth).with(
:gitlab,
'a',
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'tracks a configuration error if the arguments are neither a hash nor an array' do
conf = { 'name' => 'gitlab', 'args' => 17 }
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'gitlab', arguments_type: 'Integer')
expect(devise_config).to receive(:omniauth).with( expect(devise_config).to receive(:omniauth).with(
:gitlab, :gitlab,
authorize_params: { gl_auth_type: 'login' } authorize_params: { gl_auth_type: 'login' }

View File

@ -25,18 +25,16 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:subgroup) { create(:group, parent: group) }
it 'creates the aggregation record' do it 'creates the aggregation record' do
described_class.safe_create_for_group(group) record = described_class.safe_create_for_group(group)
record = described_class.find_by(group_id: group) expect(record).to be_persisted
expect(record).to be_present
end end
context 'when non top-level group is given' do context 'when non top-level group is given' do
it 'creates the aggregation record for the top-level group' do it 'creates the aggregation record for the top-level group' do
described_class.safe_create_for_group(subgroup) record = described_class.safe_create_for_group(subgroup)
record = described_class.find_by(group_id: group) expect(record).to be_persisted
expect(record).to be_present
end end
end end
@ -75,4 +73,56 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
expect(last_two).to eq([aggregation5, aggregation2]) expect(last_two).to eq([aggregation5, aggregation2])
end end
end end
describe '#estimated_next_run_at' do
around do |example|
freeze_time { example.run }
end
context 'when aggregation was not yet executed for the given group' do
let(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: nil) }
it { expect(aggregation.estimated_next_run_at).to eq(nil) }
end
context 'when aggregation was already run' do
let!(:other_aggregation1) { create(:cycle_analytics_aggregation, last_incremental_run_at: 10.minutes.ago) }
let!(:other_aggregation2) { create(:cycle_analytics_aggregation, last_incremental_run_at: 15.minutes.ago) }
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
it 'returns the duration between the previous run timestamp and the earliest last_incremental_run_at' do
expect(aggregation.estimated_next_run_at).to eq(10.minutes.from_now)
end
context 'when the aggregation has persisted previous runtimes' do
before do
aggregation.update!(incremental_runtimes_in_seconds: [30, 60, 90])
end
it 'adds the runtime to the estimation' do
expect(aggregation.estimated_next_run_at).to eq((10.minutes.seconds + 60.seconds).from_now)
end
end
end
context 'when no records are present in the DB' do
it 'returns nil' do
expect(described_class.new.estimated_next_run_at).to eq(nil)
end
end
context 'when only one aggregation record present' do
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
it 'returns nil' do
expect(aggregation.estimated_next_run_at).to eq(nil)
end
end
context 'when the aggregation is disabled' do
it 'returns nil' do
expect(described_class.new(enabled: false).estimated_next_run_at).to eq(nil)
end
end
end
end end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RunnersTokenPrefixable do
describe 'runners token prefix' do
subject { described_class::RUNNERS_TOKEN_PREFIX }
it 'has the correct value' do
expect(subject).to eq('GR1348941')
end
end
end

View File

@ -441,7 +441,7 @@ RSpec.shared_examples 'prefixed token rotation' do
context 'token is not set' do context 'token is not set' do
it 'generates a new token' do it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/) expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).not_to be_persisted expect(instance).not_to be_persisted
end end
end end
@ -452,26 +452,14 @@ RSpec.shared_examples 'prefixed token rotation' do
end end
it 'generates a new token' do it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/) expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).not_to be_persisted expect(instance).not_to be_persisted
end end
context 'feature flag is disabled' do
before do
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
stub_feature_flags(flag => false)
end
it 'leaves the token unchanged' do
expect { subject }.not_to change(instance, :runners_token)
expect(instance).not_to be_persisted
end
end
end end
context 'token is set and matches prefix' do context 'token is set and matches prefix' do
before do before do
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef') instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
end end
it 'leaves the token unchanged' do it 'leaves the token unchanged' do
@ -486,7 +474,7 @@ RSpec.shared_examples 'prefixed token rotation' do
context 'token is not set' do context 'token is not set' do
it 'generates a new token' do it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/) expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).to be_persisted expect(instance).to be_persisted
end end
end end
@ -497,25 +485,14 @@ RSpec.shared_examples 'prefixed token rotation' do
end end
it 'generates a new token' do it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/) expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).to be_persisted expect(instance).to be_persisted
end end
context 'feature flag is disabled' do
before do
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
stub_feature_flags(flag => false)
end
it 'leaves the token unchanged' do
expect { subject }.not_to change(instance, :runners_token)
end
end
end end
context 'token is set and matches prefix' do context 'token is set and matches prefix' do
before do before do
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef') instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
instance.save! instance.save!
end end

View File

@ -3239,12 +3239,4 @@ RSpec.describe Group do
it_behaves_like 'no effective expiration interval' it_behaves_like 'no effective expiration interval'
end end
end end
describe '#runners_token' do
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'it has a prefixable runners_token', :groups_runners_token_prefix
end
end end

View File

@ -813,8 +813,8 @@ RSpec.describe Project, factory_default: :keep do
end end
it 'does not set an random token if one provided' do it 'does not set an random token if one provided' do
project = FactoryBot.create(:project, runners_token: "#{Project::RUNNERS_TOKEN_PREFIX}my-token") project = FactoryBot.create(:project, runners_token: "#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
expect(project.runners_token).to eq("#{Project::RUNNERS_TOKEN_PREFIX}my-token") expect(project.runners_token).to eq("#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
end end
end end
@ -8077,14 +8077,6 @@ RSpec.describe Project, factory_default: :keep do
end end
end end
describe '#runners_token' do
let_it_be(:project) { create(:project) }
subject { project }
it_behaves_like 'it has a prefixable runners_token', :projects_runners_token_prefix
end
private private
def finish_job(export_job) def finish_job(export_job)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/database/establish_connection' require_relative '../../../../rubocop/cop/database/establish_connection'
RSpec.describe RuboCop::Cop::Database::EstablishConnection do RSpec.describe RuboCop::Cop::Database::EstablishConnection do

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'it has a prefixable runners_token' do |feature_flag|
context 'feature flag enabled' do
before do
stub_feature_flags(feature_flag => [subject])
end
describe '#runners_token' do
it 'has a runners_token_prefix' do
expect(subject.runners_token_prefix).not_to be_empty
end
it 'starts with the runners_token_prefix' do
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
end
end
end
context 'feature flag disabled' do
before do
stub_feature_flags(feature_flag => false)
end
describe '#runners_token' do
it 'does not have a runners_token_prefix' do
expect(subject.runners_token_prefix).to be_empty
end
it 'starts with the runners_token_prefix' do
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
end
end
end
end

View File

@ -0,0 +1,113 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'layouts/_header_search' do
let(:project) { nil }
let(:group) { nil }
let(:scope) { nil }
let(:ref) { nil }
let(:code_search) { false }
let(:for_snippets) { false}
let(:header_search_context) do
{
project: project,
group: group,
scope: scope,
ref: ref,
code_search: code_search,
for_snippets: for_snippets
}
end
before do
allow(view).to receive(:header_search_context).and_return(header_search_context)
end
shared_examples 'hidden fields are properly set' do
context 'when search_context has a scope value' do
let(:scope) { 'issues' }
it 'sets scope input to issues' do
render
expect(rendered).to have_css("input[name='scope'][value='#{scope}']", count: 1, visible: false)
end
end
context 'when search_context has a code_search value' do
let(:code_search) { true }
it 'sets search_code input to true' do
render
expect(rendered).to have_css("input[name='search_code'][value='#{code_search}']", count: 1, visible: false)
end
end
context 'when search_context has a ref value' do
let(:ref) { 'test-branch' }
it 'sets repository_ref input to test-branch' do
render
expect(rendered).to have_css("input[name='repository_ref'][value='#{ref}']", count: 1, visible: false)
end
end
context 'when search_context has a for_snippets value' do
let(:for_snippets) { true }
it 'sets for_snippets input to true' do
render
expect(rendered).to have_css("input[name='snippets'][value='#{for_snippets}']", count: 1, visible: false)
end
end
context 'nav_source' do
it 'always set to navbar' do
render
expect(rendered).to have_css("input[name='nav_source'][value='navbar']", count: 1, visible: false)
end
end
context 'submit button' do
it 'always renders for specs' do
render
expect(rendered).to have_css('noscript button', text: 'Search')
end
end
end
context 'when doing a project level search' do
let(:project) do
{ id: 123, name: 'foo' }
end
it 'sets project_id field' do
render
expect(rendered).to have_css("input[name='project_id'][value='#{project[:id]}']", count: 1, visible: false)
end
it_behaves_like 'hidden fields are properly set'
end
context 'when doing a group level search' do
let(:group) do
{ id: 123, name: 'bar' }
end
it 'sets group_id field' do
render
expect(rendered).to have_css("input[name='group_id'][value='#{group[:id]}']", count: 1, visible: false)
end
it_behaves_like 'hidden fields are properly set'
end
end