Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-19 12:12:41 +00:00
parent 94ecc00f47
commit 0512d12bf1
64 changed files with 1016 additions and 595 deletions

View File

@ -539,4 +539,4 @@ gem 'ipaddress', '~> 0.8.3'
gem 'parslet', '~> 1.8'
gem 'ipynbdiff', '0.3.7'
gem 'ipynbdiff', '0.3.8'

View File

@ -641,7 +641,7 @@ GEM
invisible_captcha (1.1.0)
rails (>= 4.2)
ipaddress (0.8.3)
ipynbdiff (0.3.7)
ipynbdiff (0.3.8)
diffy (= 3.3.0)
json (= 2.5.1)
jaeger-client (1.1.0)
@ -1510,7 +1510,7 @@ DEPENDENCIES
icalendar
invisible_captcha (~> 1.1.0)
ipaddress (~> 0.8.3)
ipynbdiff (= 0.3.7)
ipynbdiff (= 0.3.8)
jira-ruby (~> 2.1.4)
js_regex (~> 3.7)
json (~> 2.5.1)

View File

@ -1 +0,0 @@
<svg id="Logos" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80" height="80" viewBox="0 0 80 80"><defs><style>.cls-1{fill:#7a869a;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:url(#linear-gradient-2);}</style><linearGradient id="linear-gradient" x1="38.11" y1="18.54" x2="23.17" y2="33.48" gradientUnits="userSpaceOnUse"><stop offset="0.18" stop-color="#344563"/><stop offset="1" stop-color="#7a869a"/></linearGradient><linearGradient id="linear-gradient-2" x1="42.07" y1="61.47" x2="56.98" y2="46.55" xlink:href="#linear-gradient"/></defs><title>jira software-icon-gradient-neutral</title><path class="cls-1" d="M74.18,38,43,6.9l-3-3h0L16.58,27.32h0L5.86,38a2.86,2.86,0,0,0,0,4.05L27.28,63.51,40,76.25,63.47,52.81l.36-.36L74.18,42.09A2.86,2.86,0,0,0,74.18,38ZM40,50.77l-10.7-10.7L40,29.37l10.7,10.7Z"/><path class="cls-2" d="M40,29.37A18,18,0,0,1,40,4L16.54,27.37,29.28,40.11,40,29.37Z"/><path class="cls-3" d="M50.75,40,40,50.77a18,18,0,0,1,0,25.48h0L63.5,52.78Z"/></svg>

Before

Width:  |  Height:  |  Size: 1016 B

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
@ -8,46 +8,33 @@ export default {
modalId: INSTALL_AGENT_MODAL_ID,
multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
installDocsUrl: helpPagePath('administration/clusters/kas'),
getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
}),
components: {
GlButton,
GlEmptyState,
GlLink,
GlSprintf,
GlAlert,
},
directives: {
GlModalDirective,
},
inject: ['emptyStateImage', 'projectPath'],
inject: ['emptyStateImage'],
props: {
hasConfigurations: {
type: Boolean,
required: true,
},
isChildComponent: {
default: false,
required: false,
type: Boolean,
},
},
computed: {
repositoryPath() {
return `/${this.projectPath}`;
},
},
};
</script>
<template>
<gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
<p class="mw-460 gl-mx-auto gl-text-left">
<p class="gl-text-left">
{{ $options.i18n.introText }}
</p>
<p class="mw-460 gl-mx-auto gl-text-left">
<p class="gl-text-left">
<gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
<gl-link
@ -61,42 +48,17 @@ export default {
</gl-sprintf>
</p>
<p class="mw-460 gl-mx-auto">
<p>
<gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
{{ $options.i18n.learnMoreText }}
</gl-link>
</p>
<gl-alert
v-if="!hasConfigurations"
variant="warning"
class="gl-mb-5 text-left"
:dismissible="false"
>
{{ $options.i18n.warningText }}
<template #actions>
<gl-button
category="primary"
variant="info"
:href="$options.getStartedDocsUrl"
target="_blank"
class="gl-ml-0!"
>
{{ $options.i18n.readMoreText }}
</gl-button>
<gl-button category="secondary" variant="info" :href="repositoryPath">
{{ $options.i18n.repositoryButtonText }}
</gl-button>
</template>
</gl-alert>
</template>
<template #actions>
<gl-button
v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button"
category="primary"
variant="confirm"

View File

@ -86,9 +86,6 @@ export default {
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
},
hasConfigurations() {
return Boolean(this.agents?.project?.repository?.tree?.trees?.nodes?.length);
},
},
methods: {
reloadAgents() {
@ -161,11 +158,7 @@ export default {
</div>
</div>
<agent-empty-state
v-else
:has-configurations="hasConfigurations"
:is-child-component="isChildComponent"
/>
<agent-empty-state v-else :is-child-component="isChildComponent" />
</section>
<gl-alert v-else variant="danger" :dismissible="false">

View File

@ -1,7 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
export default {
name: 'AvailableAgentsDropdown',
@ -10,36 +9,22 @@ export default {
GlDropdown,
GlDropdownItem,
},
inject: ['projectPath'],
props: {
isRegistering: {
required: true,
type: Boolean,
},
},
apollo: {
agents: {
query: agentConfigurations,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
this.populateAvailableAgents(data);
},
availableAgents: {
required: true,
type: Array,
},
},
data() {
return {
availableAgents: [],
selectedAgent: null,
};
},
computed: {
isLoading() {
return this.$apollo.queries.agents.loading;
},
dropdownText() {
if (this.isRegistering) {
return this.$options.i18n.registeringAgent;
@ -58,18 +43,11 @@ export default {
isSelected(agent) {
return this.selectedAgent === agent;
},
populateAvailableAgents(data) {
const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
const configuredAgents =
data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
},
},
};
</script>
<template>
<gl-dropdown :text="dropdownText" :loading="isLoading || isRegistering">
<gl-dropdown :text="dropdownText" :loading="isRegistering">
<gl-dropdown-item
v-for="agent in availableAgents"
:key="agent"

View File

@ -12,16 +12,16 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants';
import { addAgentToStore } from '../graphql/cache_update';
import { INSTALL_AGENT_MODAL_ID, I18N_AGENT_MODAL, KAS_DISABLED_ERROR } from '../constants';
import { addAgentToStore, addAgentConfigToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default {
modalId: INSTALL_AGENT_MODAL_ID,
i18n: I18N_INSTALL_AGENT_MODAL,
components: {
AvailableAgentsDropdown,
ClipboardButton,
@ -34,7 +34,7 @@ export default {
GlModal,
GlSprintf,
},
inject: ['projectPath', 'kasAddress'],
inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
props: {
defaultBranchName: {
default: '.noBranch',
@ -46,6 +46,22 @@ export default {
type: Number,
},
},
apollo: {
agents: {
query: agentConfigurations,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
this.populateAvailableAgents(data);
},
error(error) {
this.kasDisabled = error?.message?.indexOf(KAS_DISABLED_ERROR) >= 0;
},
},
},
data() {
return {
registering: false,
@ -53,6 +69,8 @@ export default {
agentToken: null,
error: null,
clusterAgent: null,
availableAgents: [],
kasDisabled: false,
};
},
computed: {
@ -63,7 +81,7 @@ export default {
return !this.registering && this.agentName !== null;
},
canCancel() {
return !this.registered && !this.registering;
return !this.registered && !this.registering && this.isRegisterModal;
},
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
@ -76,6 +94,9 @@ export default {
advancedInstallPath() {
return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
},
enableKasPath() {
return helpPagePath('administration/clusters/kas');
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
@ -84,6 +105,29 @@ export default {
projectPath: this.projectPath,
};
},
installAgentPath() {
return helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
});
},
i18n() {
return I18N_AGENT_MODAL[this.modalType];
},
repositoryPath() {
return `/${this.projectPath}`;
},
modalType() {
return !this.availableAgents?.length && !this.registered ? 'install' : 'register';
},
modalSize() {
return this.isInstallModal ? 'sm' : 'md';
},
isInstallModal() {
return this.modalType === 'install';
},
isRegisterModal() {
return this.modalType === 'register';
},
},
methods: {
setAgentName(name) {
@ -96,8 +140,16 @@ export default {
this.registering = false;
this.agentName = null;
this.agentToken = null;
this.clusterAgent = null;
this.error = null;
},
populateAvailableAgents(data) {
const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
const configuredAgents =
data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
},
createAgentMutation() {
return this.$apollo
.mutate({
@ -117,7 +169,9 @@ export default {
);
},
})
.then(({ data: { createClusterAgent } }) => createClusterAgent);
.then(({ data: { createClusterAgent } }) => {
return createClusterAgent;
});
},
createAgentTokenMutation(agendId) {
return this.$apollo
@ -129,6 +183,17 @@ export default {
name: this.agentName,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentConfigToStore(
store,
clusterAgentTokenCreate,
this.clusterAgent,
agentConfigurations,
{
projectPath: this.projectPath,
},
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
@ -158,7 +223,7 @@ export default {
if (error) {
this.error = error.message;
} else {
this.error = this.$options.i18n.unknownError;
this.error = this.i18n.unknownError;
}
} finally {
this.registering = false;
@ -172,115 +237,142 @@ export default {
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
:title="i18n.modalTitle"
:size="modalSize"
static
lazy
@hidden="resetModal"
>
<template v-if="!registered">
<p>
<strong>{{ $options.i18n.selectAgentTitle }}</strong>
</p>
<template v-if="isRegisterModal">
<template v-if="!registered">
<p>
<strong>{{ i18n.selectAgentTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.selectAgentBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<p>
<gl-link :href="basicInstallPath" target="_blank"> {{ i18n.learnMoreLink }}</gl-link>
</p>
<form>
<gl-form-group label-for="agent-name">
<available-agents-dropdown
class="gl-w-70p"
:is-registering="registering"
@agentSelected="setAgentName"
/>
</gl-form-group>
</form>
<form>
<gl-form-group label-for="agent-name">
<available-agents-dropdown
class="gl-w-70p"
:is-registering="registering"
:available-agents="availableAgents"
@agentSelected="setAgentName"
/>
</gl-form-group>
</form>
<p v-if="error">
<gl-alert
:title="$options.i18n.registrationErrorTitle"
variant="danger"
:dismissible="false"
>
{{ error }}
</gl-alert>
</p>
<p v-if="error">
<gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
</p>
</template>
<template v-else>
<p>
<strong>{{ i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false">
{{ i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<clipboard-button :text="agentToken" :title="i18n.copyToken" />
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ i18n.basicInstallBody }}
</p>
<p>
<code-block :code="agentRegistrationCommand" />
</p>
<p>
<strong>{{ i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</template>
<template v-else>
<p>
<strong>{{ $options.i18n.tokenTitle }}</strong>
</p>
<div class="gl-text-center gl-mb-5">
<img :alt="i18n.altText" :src="emptyStateImage" height="100" />
</div>
<p>{{ i18n.modalBody }}</p>
<p>
<gl-sprintf :message="$options.i18n.tokenBody">
<p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
<gl-link :href="enableKasPath"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-alert
:title="$options.i18n.tokenSingleUseWarningTitle"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<clipboard-button :text="agentToken" :title="$options.i18n.copyToken" />
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ $options.i18n.basicInstallBody }}
</p>
<p>
<code-block :code="agentRegistrationCommand" />
</p>
<p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
<p class="gl-mb-0">
<gl-link :href="installAgentPath">
{{ i18n.docsLinkText }}
</gl-link>
</p>
</template>
<template #modal-footer>
<gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
<gl-button v-if="canCancel" @click="closeModal">{{ i18n.cancel }} </gl-button>
<gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
>{{ $options.i18n.close }}
>{{ i18n.close }}
</gl-button>
<gl-button
v-else
v-else-if="isRegisterModal"
:disabled="!nextButtonDisabled"
variant="confirm"
category="primary"
@click="registerAgent"
>{{ $options.i18n.registerAgentButton }}
>{{ i18n.registerAgentButton }}
</gl-button>
<gl-button
v-if="isInstallModal"
:href="repositoryPath"
variant="confirm"
category="secondary"
data-testid="agent-secondary-button"
>{{ i18n.secondaryButton }}
</gl-button>
<gl-button v-if="isInstallModal" variant="confirm" category="primary" @click="closeModal"
>{{ i18n.done }}
</gl-button>
</template>
</gl-modal>

View File

@ -64,45 +64,63 @@ export const STATUSES = {
creating: { title: __('Creating') },
};
export const I18N_INSTALL_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register Agent'),
close: __('Close'),
cancel: __('Cancel'),
export const I18N_AGENT_MODAL = {
register: {
registerAgentButton: s__('ClusterAgents|Register Agent'),
close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'),
modalTitle: s__('ClusterAgents|Connect with Agent'),
selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
selectAgentBody: s__(
`ClusterAgents|Select the Agent you want to register with GitLab and install on your cluster. To learn more about the Kubernetes Agent registration process %{linkStart}go to the documentation%{linkEnd}.`,
),
selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
selectAgentBody: s__(
'ClusterAgents|Select an Agent to register with GitLab and install on your cluster.',
),
learnMoreLink: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent registration.'),
copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'),
tokenBody: s__(
`ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`,
),
copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'),
tokenBody: s__(
`ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.',
),
tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.',
),
tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`,
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
},
install: {
modalTitle: s__('ClusterAgents|Install new Agent'),
modalBody: s__(
'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
),
docsLinkText: s__('ClusterAgents|Learn more about installing a GitLab Kubernetes Agent'),
enableKasText: s__(
'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}',
),
altText: s__('ClusterAgents|GitLab Kubernetes Agent'),
secondaryButton: s__('ClusterAgents|Go to the repository'),
done: __('Done'),
},
};
export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError';
export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
selectAgent: s__('ClusterAgents|Select an Agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'),
@ -149,11 +167,6 @@ export const I18N_AGENTS_EMPTY_STATE = {
'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
),
learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
warningText: s__(
'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
),
readMoreText: s__('ClusterAgents|Read more about getting started'),
repositoryButtonText: s__('ClusterAgents|Go to the repository'),
primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
};

View File

@ -1,29 +1,64 @@
import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
export const hasErrors = ({ errors = [] }) => errors?.length;
export function addAgentToStore(store, createClusterAgent, query, variables) {
const { clusterAgent } = createClusterAgent;
const sourceData = store.readQuery({
query,
variables,
});
if (!hasErrors(createClusterAgent)) {
const { clusterAgent } = createClusterAgent;
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const configuration = {
name: clusterAgent.name,
path: getAgentConfigPath(clusterAgent.name),
webPath: clusterAgent.webPath,
__typename: 'TreeEntry',
};
const data = produce(sourceData, (draftData) => {
const configuration = {
name: clusterAgent.name,
path: getAgentConfigPath(clusterAgent.name),
webPath: clusterAgent.webPath,
__typename: 'TreeEntry',
};
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.clusterAgents.count += 1;
draftData.project.repository.tree.trees.nodes.push(configuration);
});
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.clusterAgents.count += 1;
draftData.project.repository.tree.trees.nodes.push(configuration);
});
store.writeQuery({
query,
variables,
data,
});
store.writeQuery({
query,
variables,
data,
});
}
}
export function addAgentConfigToStore(
store,
clusterAgentTokenCreate,
clusterAgent,
query,
variables,
) {
if (!hasErrors(clusterAgentTokenCreate)) {
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const configuration = {
agentName: clusterAgent.name,
__typename: 'AgentConfiguration',
};
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.agentConfigurations.nodes.push(configuration);
});
store.writeQuery({
query,
variables,
data,
});
}
}

View File

@ -1,5 +1,8 @@
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import {
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
@ -17,6 +20,9 @@ export default {
DiffViewer,
ImageDiffOverlay,
},
directives: {
SafeHtml,
},
props: {
discussion: {
type: Object,
@ -92,11 +98,7 @@ export default {
>
<td :class="line.type" class="diff-line-num old_line">{{ line.old_line }}</td>
<td :class="line.type" class="diff-line-num new_line">{{ line.new_line }}</td>
<td
:class="line.type"
class="line_content"
v-html="trimChar(line.rich_text) /* eslint-disable-line vue/no-v-html */"
></td>
<td v-safe-html="trimChar(line.rich_text)" :class="line.type" class="line_content"></td>
</tr>
</template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">

View File

@ -7,20 +7,6 @@
}
}
.agents-empty-state {
.text-content {
@include gl-max-w-full;
@include media-breakpoint-up(lg) {
max-width: 70%;
}
}
.gl-alert-actions {
@include gl-mt-0;
@include gl-flex-wrap;
}
}
.gl-card-body {
@include media-breakpoint-up(sm) {
@include gl-pt-2;

View File

@ -14,7 +14,7 @@ module Resolvers
return [] unless can_read_agent_configuration?
kas_client.list_agent_config_files(project: project)
rescue GRPC::BadStatus => e
rescue GRPC::BadStatus, Gitlab::Kas::Client::ConfigurationError => e
raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Types
class BaseEdge < GraphQL::Types::Relay::BaseEdge
field_class Types::BaseField
end
end

View File

@ -78,6 +78,8 @@ module Types
attr_reader :feature_flag
def field_authorized?(object, ctx)
object = object.node if object.is_a?(GraphQL::Pagination::Connection::Edge)
authorization.ok?(object, ctx[:current_user])
end

View File

@ -7,6 +7,7 @@ module Types
prepend Gitlab::Graphql::MarkdownField
field_class Types::BaseField
edge_type_class Types::BaseEdge
def self.accepts(*types)
@accepts ||= []

View File

@ -3,7 +3,7 @@
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerWebUrlEdge < GraphQL::Types::Relay::BaseEdge
class RunnerWebUrlEdge < ::Types::BaseEdge
include FindClosest
field :web_url, GraphQL::Types::String, null: true,

View File

@ -6,6 +6,7 @@ module MergeRequests
return unless merge_request.discussions_resolved?
SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
execute_hooks(merge_request, 'update')
notification_service.async.resolve_all_discussions(merge_request, current_user)
end
end

View File

@ -2717,15 +2717,6 @@
:weight: 1
:idempotent: true
:tags: []
- :name: propagate_service_template
:worker_name: PropagateServiceTemplateWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: reactive_caching
:worker_name: ReactiveCachingWorker
:feature_category: :not_owned

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
# No longer in use https://gitlab.com/groups/gitlab-org/-/epics/5672
# To be removed https://gitlab.com/gitlab-org/gitlab/-/issues/335178
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
data_consistency :always
sidekiq_options retry: 3
feature_category :integrations
LEASE_TIMEOUT = 4.hours.to_i
def perform(template_id)
return unless try_obtain_lease_for(template_id)
::Integrations::PropagateTemplateService.propagate(Integration.find_by_id(template_id))
end
private
def try_obtain_lease_for(template_id)
Gitlab::ExclusiveLease
.new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT)
.try_obtain
end
end

View File

@ -1,8 +0,0 @@
---
name: between_commits_via_list_commits
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74273
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345458
milestone: '14.5'
type: development
group: group::gitaly
default_enabled: false

View File

@ -351,8 +351,6 @@
- 1
- - propagate_integration_project
- 1
- - propagate_service_template
- 1
- - reactive_caching
- 1
- - rebase

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ScheduleRecalculateVulnerabilityFindingSignaturesForFindings < Gitlab::Database::Migration[1.0]
MIGRATION = 'RecalculateVulnerabilityFindingSignaturesForFindings'
BATCH_SIZE = 1_000
DELAY_INTERVAL = 2.minutes
disable_ddl_transaction!
def up
return unless Gitlab.ee?
queue_background_migration_jobs_by_range_at_intervals(
define_batchable_model('vulnerability_finding_signatures'),
MIGRATION,
DELAY_INTERVAL,
batch_size: BATCH_SIZE,
track_jobs: true
)
end
def down
# no-op
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RemovePropagateServiceTemplateWorker < Gitlab::Database::Migration[1.0]
def up
Sidekiq::Queue.new('propagate_service_template').clear
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
b372da05f40fa67680b6a28ddf9bed3dc4b95795c144bf4367e4826b5cd64d6b

View File

@ -0,0 +1 @@
d16d62b2984586540a99aa5fc67de6459a4cd473089ddbae8d45e8783863d78d

View File

@ -400,7 +400,7 @@ limit is checked every time a new trigger is created.
If a new trigger would cause the total number of pipeline triggers to exceed the
limit, the trigger is considered invalid.
Set the limit to `0` to disable it. Defaults to `0` on self-managed instances.
Set the limit to `0` to disable it. Defaults to `150` on self-managed instances.
To set this limit to `100` on a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):

View File

@ -354,7 +354,7 @@ gitlab_rails['object_store']['connection'] = {
'provider' => 'AzureRM',
'azure_storage_account_name' => '<AZURE STORAGE ACCOUNT NAME>',
'azure_storage_access_key' => '<AZURE STORAGE ACCESS KEY>',
'azure_storage_domain' => '<AZURE STORAGE DOMAIN>',
'azure_storage_domain' => '<AZURE STORAGE DOMAIN>'
}
```

View File

@ -173,9 +173,11 @@ The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv
- The variable key can contain only letters, digits, and underscores (`_`).
- The maximum size of the `.env` file is 5 KB.
- In GitLab 13.5 and older, the maximum number of inherited variables is 10.
- In [GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/247913),
the maximum number of inherited variables is 20.
This limit [can be changed on self-managed instances](../../administration/instance_limits.md#limit-dotenv-file-size).
- On GitLab.com, [the maximum number of inherited variables](../../user/gitlab_com/index.md#gitlab-cicd)
is 50 for Free, 100 for Premium and 150 for Ultimate. The default for
self-managed instances is 150, and can be changed by changing the
`dotenv_variables` [application limit](../../administration/instance_limits.md#limit-dotenv-variables).
- Variable substitution in the `.env` file is not supported.
- The `.env` file can't have empty lines or comments (starting with `#`).
- Key values in the `env` file cannot have spaces or newline characters (`\n`), including when using single or double quotes.

View File

@ -249,6 +249,9 @@ In line with our `CodeReuse/ActiveRecord` cop, you should only use forms like
use the `ApplicationRecord`-provided `.pluck_primary_key` helper method instead.
In the latter, you should add a small helper method to the relevant model.
If you have strong reasons to use `pluck`, it could make sense to limit the number
of records plucked. `MAX_PLUCK` defaults to `1_000` in `ApplicationRecord`.
## Inherit from ApplicationRecord
Most models in the GitLab codebase should inherit from `ApplicationRecord`,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -101,10 +101,10 @@ Consider this example:
You can browse, search, and view issues from a selected Jira project directly in GitLab,
if your GitLab administrator [has configured it](configure.md).
To do this, in GitLab, go to your project and select **Jira > Issues list**. The issue list
To do this, in GitLab, go to your project and select **Issues > Jira issues**. The issue list
sorts by **Created date** by default, with the newest issues listed at the top:
![Jira issues integration enabled](img/open_jira_issues_list_v13.2.png)
![Jira issues integration enabled](img/open_jira_issues_list_v14_6.png)
- To display the most recently updated issues first, select **Last updated**.
- You can [search and filter](#search-and-filter-the-issues-list) the issues list.

View File

@ -141,7 +141,7 @@ the related documentation.
| [Scheduled Job Archival](../../user/admin_area/settings/continuous_integration.md#archive-jobs) | 3 months | Never |
| Max test cases per [unit test report](../../ci/unit_test_reports.md) | `500_000` | Unlimited |
| [Max registered runners](../../administration/instance_limits.md#number-of-registered-runners-per-scope) | Free tier: `50` per-group / `50` per-project <br/> All paid tiers: `1_000` per-group / `1_000` per-project | `1_000` per-group / `1_000` per-project |
| [Limit dotenv variables](../../administration/instance_limits.md#limit-dotenv-variables) | Free tier: `50` / Premium tier: `100` / Ultimate tier: `150` | Unlimited |
| [Limit dotenv variables](../../administration/instance_limits.md#limit-dotenv-variables) | Free tier: `50` / Premium tier: `100` / Ultimate tier: `150` | 150 |
## Account and limit settings

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module BulkImports
module Projects
module Pipelines
class CiPipelinesPipeline
include NdjsonPipeline
relation_name 'ci_pipelines'
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
end
end
end
end

View File

@ -43,6 +43,10 @@ module BulkImports
pipeline: BulkImports::Projects::Pipelines::ProtectedBranchesPipeline,
stage: 4
},
ci_pipelines: {
pipeline: BulkImports::Projects::Pipelines::CiPipelinesPipeline,
stage: 4
},
wiki: {
pipeline: BulkImports::Common::Pipelines::WikiPipeline,
stage: 5

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop: disable Style/Documentation
class RecalculateVulnerabilityFindingSignaturesForFindings
def perform(start_id, stop_id)
end
end
end
end
Gitlab::BackgroundMigration::RecalculateVulnerabilityFindingSignaturesForFindings.prepend_mod

View File

@ -204,19 +204,6 @@ module Gitlab
Gitlab::Git::Commit.new(@repository, gitaly_commit)
end
def between(from, to)
return list_commits(["^" + from, to], reverse: true) if Feature.enabled?(:between_commits_via_list_commits)
request = Gitaly::CommitsBetweenRequest.new(
repository: @gitaly_repo,
from: from,
to: to
)
response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
def diff_stats(left_commit_sha, right_commit_sha)
request = Gitaly::DiffStatsRequest.new(
repository: @gitaly_repo,

View File

@ -7,6 +7,7 @@ module Gitlab
%i[
assignee_id
author_id
blocking_discussions_resolved
created_at
description
head_pipeline_id
@ -57,7 +58,8 @@ module Gitlab
human_time_estimate: merge_request.human_time_estimate,
assignee_ids: merge_request.assignee_ids,
assignee_id: merge_request.assignee_ids.first, # This key is deprecated
state: merge_request.state # This key is deprecated
state: merge_request.state, # This key is deprecated
blocking_discussions_resolved: merge_request.mergeable_discussions_state?
}
merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)

View File

@ -44,6 +44,14 @@ module Sidebars
list[index] = new_element
end
def remove_element(list, element_to_remove)
index = index_of(list, element_to_remove)
return unless index
list.slice!(index)
end
private
# Classes including this method will have to define

View File

@ -37,6 +37,10 @@ module Sidebars
replace_element(@menus, menu_to_replace, new_menu)
end
def remove_menu(menu_to_remove)
remove_element(@menus, menu_to_remove)
end
def set_scope_menu(scope_menu)
@scope_menu = scope_menu
end

View File

@ -7423,6 +7423,9 @@ msgstr ""
msgid "ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}"
msgstr ""
msgid "ClusterAgents|GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Go to the repository"
msgstr ""
@ -7444,6 +7447,12 @@ msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot"
msgstr ""
msgid "ClusterAgents|Learn more about installing a GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent registration."
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent."
msgstr ""
@ -7468,9 +7477,6 @@ msgstr ""
msgid "ClusterAgents|Not connected"
msgstr ""
msgid "ClusterAgents|Read more about getting started"
msgstr ""
msgid "ClusterAgents|Recommended"
msgstr ""
@ -7492,7 +7498,7 @@ msgstr ""
msgid "ClusterAgents|Select an Agent"
msgstr ""
msgid "ClusterAgents|Select the Agent you want to register with GitLab and install on your cluster. To learn more about the Kubernetes Agent registration process %{linkStart}go to the documentation%{linkEnd}."
msgid "ClusterAgents|Select an Agent to register with GitLab and install on your cluster."
msgstr ""
msgid "ClusterAgents|Select which Agent you want to install"
@ -7501,6 +7507,9 @@ msgstr ""
msgid "ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgstr ""
msgid "ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}"
msgstr ""
msgid "ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window."
msgstr ""
@ -19648,9 +19657,6 @@ msgstr ""
msgid "Japanese language support using"
msgstr ""
msgid "Jira Issues"
msgstr ""
msgid "Jira display name"
msgstr ""
@ -19780,18 +19786,12 @@ msgstr ""
msgid "JiraService|If different from Web URL."
msgstr ""
msgid "JiraService|Issue List"
msgstr ""
msgid "JiraService|Issues created from vulnerabilities in this project will be Jira issues, even if GitLab issues are enabled."
msgstr ""
msgid "JiraService|Jira API URL"
msgstr ""
msgid "JiraService|Jira Issues"
msgstr ""
msgid "JiraService|Jira comments are created when an issue is referenced in a commit."
msgstr ""
@ -19801,6 +19801,9 @@ msgstr ""
msgid "JiraService|Jira issue type"
msgstr ""
msgid "JiraService|Jira issues"
msgstr ""
msgid "JiraService|Jira project key"
msgstr ""

View File

@ -8,7 +8,7 @@ module RuboCop
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
# We want to exclude our own basetypes and scalars
ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType SubscriptionType
ALLOWED_TYPES = %w[BaseEnum BaseEdge BaseScalar BasePermissionType MutationType SubscriptionType
QueryType GraphQL::Schema BaseUnion BaseInputObject].freeze
def_node_search :authorize?, <<~PATTERN

View File

@ -26,8 +26,6 @@ function retrieve_tests_metadata() {
fi
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
# Fixed ID to get the report back to a good state after https://gitlab.com/gitlab-org/gitlab/-/issues/345798 / https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74617
test_metadata_job_id=1766932099
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
fi

View File

@ -25,7 +25,7 @@ RSpec.describe 'Cluster agent registration', :js do
it 'allows the user to select an agent to install, and displays the resulting agent token' do
click_button('Actions')
expect(page).to have_content('Install new Agent')
expect(page).to have_content('Register Agent')
click_button('Select an Agent')
click_button('example-agent-2')

View File

@ -26,8 +26,7 @@ RSpec.describe 'User activates Jira', :js do
unless Gitlab.ee?
it 'adds Jira link to sidebar menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link('Jira Issues')
expect(page).not_to have_link('Issue List', visible: false)
expect(page).not_to have_link('Jira issues', visible: false)
expect(page).not_to have_link('Open Jira', href: url, visible: false)
expect(page).to have_link('Jira', href: url)
end

View File

@ -1,25 +1,20 @@
import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui';
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import { INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project';
const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
const installDocsUrl = helpPagePath('administration/clusters/kas');
describe('AgentEmptyStateComponent', () => {
let wrapper;
const propsData = {
hasConfigurations: false,
};
const provideData = {
emptyStateImage,
projectPath,
};
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
@ -27,8 +22,10 @@ describe('AgentEmptyStateComponent', () => {
beforeEach(() => {
wrapper = shallowMountExtended(AgentEmptyState, {
propsData,
provide: provideData,
directives: {
GlModalDirective: createMockDirective(),
},
stubs: { GlEmptyState, GlSprintf },
});
});
@ -39,33 +36,22 @@ describe('AgentEmptyStateComponent', () => {
}
});
it('renders the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('renders button for the agent registration', () => {
expect(findIntegrationButton().exists()).toBe(true);
});
it('renders correct href attributes for the links', () => {
expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});
describe('when there are no agent configurations in repository', () => {
it('should render notification message box', () => {
expect(findConfigurationsAlert().exists()).toBe(true);
});
it('renders correct modal id for the agent registration modal', () => {
const binding = getBinding(findIntegrationButton().element, 'gl-modal-directive');
it('should disable integration button', () => {
expect(findIntegrationButton().attributes('disabled')).toBe('true');
});
});
describe('when there is a list of agent configurations', () => {
beforeEach(() => {
propsData.hasConfigurations = true;
wrapper = shallowMountExtended(AgentEmptyState, {
propsData,
provide: provideData,
});
});
it('should render content without notification message box', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findConfigurationsAlert().exists()).toBe(false);
expect(findIntegrationButton().attributes('disabled')).toBeUndefined();
});
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});

View File

@ -19,7 +19,6 @@ describe('Agents', () => {
};
const provideData = {
projectPath: 'path/to/project',
kasAddress: 'kas.example.com',
};
const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
@ -216,24 +215,6 @@ describe('Agents', () => {
});
});
describe('when the agent configurations are present', () => {
const trees = [
{
name: 'agent-1',
path: '.gitlab/agents/agent-1',
webPath: '/project/path/.gitlab/agents/agent-1',
},
];
beforeEach(() => {
return createWrapper({ agents: [], trees });
});
it('should pass the correct hasConfigurations boolean value to empty state component', () => {
expect(findEmptyState().props('hasConfigurations')).toEqual(true);
});
});
describe('when agents query has errored', () => {
beforeEach(() => {
return createWrapper({ agents: null });

View File

@ -1,14 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
import agentConfigurationsQuery from '~/clusters_list/graphql/queries/agent_configurations.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { agentConfigurationsResponse } from './mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('AvailableAgentsDropdown', () => {
let wrapper;
@ -18,46 +11,19 @@ describe('AvailableAgentsDropdown', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findConfiguredAgentItem = () => findDropdownItems().at(0);
const createWrapper = ({ propsData = {}, isLoading = false }) => {
const provide = {
projectPath: 'path/to/project',
};
wrapper = (() => {
if (isLoading) {
const mocks = {
$apollo: {
queries: {
agents: {
loading: true,
},
},
},
};
return mount(AvailableAgentsDropdown, { mocks, provide, propsData });
}
const apolloProvider = createMockApollo([
[agentConfigurationsQuery, jest.fn().mockResolvedValue(agentConfigurationsResponse)],
]);
return mount(AvailableAgentsDropdown, {
localVue,
apolloProvider,
provide,
propsData,
});
})();
const createWrapper = ({ propsData }) => {
wrapper = shallowMount(AvailableAgentsDropdown, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('there are agents available', () => {
const propsData = {
availableAgents: ['configured-agent'],
isRegistering: false,
};
@ -69,12 +35,6 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('text')).toBe(i18n.selectAgent);
});
it('shows only agents that are not yet installed', () => {
expect(findDropdownItems()).toHaveLength(1);
expect(findConfiguredAgentItem().text()).toBe('configured-agent');
expect(findConfiguredAgentItem().props('isChecked')).toBe(false);
});
describe('click events', () => {
beforeEach(() => {
findConfiguredAgentItem().vm.$emit('click');
@ -93,6 +53,7 @@ describe('AvailableAgentsDropdown', () => {
describe('registration in progress', () => {
const propsData = {
availableAgents: ['configured-agent'],
isRegistering: true,
};
@ -108,22 +69,4 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('loading')).toBe(true);
});
});
describe('agents query is loading', () => {
const propsData = {
isRegistering: false,
};
beforeEach(() => {
createWrapper({ propsData, isLoading: true });
});
it('updates the text in the dropdown', () => {
expect(findDropdown().text()).toBe(i18n.selectAgent);
});
it('displays a loading icon', () => {
expect(findDropdown().props('loading')).toBe(true);
});
});
});

View File

@ -1,10 +1,12 @@
import { GlAlert, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
import { I18N_INSTALL_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
import { I18N_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import getAgentConfigurations from '~/clusters_list/graphql/queries/agent_configurations.query.graphql';
import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.mutation.graphql';
import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -23,6 +25,9 @@ const localVue = createLocalVue();
localVue.use(VueApollo);
const projectPath = 'path/to/project';
const kasAddress = 'kas.example.com';
const kasEnabled = true;
const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT;
@ -30,7 +35,16 @@ describe('InstallAgentModal', () => {
let wrapper;
let apolloProvider;
const i18n = I18N_INSTALL_AGENT_MODAL;
const configurations = [{ agentName: 'agent-name' }];
const apolloQueryResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: configurations },
},
},
};
const findModal = () => wrapper.findComponent(ModalStub);
const findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown);
const findAlert = () => findModal().findComponent(GlAlert);
@ -40,6 +54,8 @@ describe('InstallAgentModal', () => {
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default');
const findSecondaryButton = () => wrapper.findByTestId('agent-secondary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.install.altText });
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
@ -52,7 +68,9 @@ describe('InstallAgentModal', () => {
const createWrapper = () => {
const provide = {
projectPath,
kasAddress: 'kas.example.com',
kasAddress,
kasEnabled,
emptyStateImage,
};
const propsData = {
@ -60,7 +78,7 @@ describe('InstallAgentModal', () => {
maxAgents,
};
wrapper = shallowMount(InstallAgentModal, {
wrapper = shallowMountExtended(InstallAgentModal, {
attachTo: document.body,
stubs: {
GlModal: ModalStub,
@ -85,10 +103,12 @@ describe('InstallAgentModal', () => {
});
};
const mockSelectedAgentResponse = () => {
const mockSelectedAgentResponse = async () => {
createWrapper();
writeQuery();
await wrapper.vm.$nextTick();
wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click');
@ -96,121 +116,160 @@ describe('InstallAgentModal', () => {
};
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
]);
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
apolloProvider = null;
});
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
expect(findAgentDropdown().isVisible()).toBe(true);
expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
describe('when agent configurations are present', () => {
const i18n = I18N_AGENT_MODAL.register;
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
expect(findAgentDropdown().isVisible()).toBe(true);
expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
describe('an agent is selected', () => {
beforeEach(() => {
findAgentDropdown().vm.$emit('agentSelected');
});
it('enables the next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expectDisabledAttribute(findActionButton(), false);
});
});
it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
describe('registering an agent', () => {
const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
]);
return mockSelectedAgentResponse();
});
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath },
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
input: { clusterAgentId: 'agent-id', name: 'agent-name' },
});
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
it('shows agent instructions', () => {
const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
const token = findModal().findComponent(GlFormInputGroup);
expect(token.props('value')).toBe('mock-agent-token');
const alert = findModal().findComponent(GlAlert);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
const code = findModal().findComponent(CodeBlock).props('code');
expect(code).toContain('--agent-token=mock-agent-token');
expect(code).toContain('--kas-address=kas.example.com');
});
describe('error creating agent', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentErrorResponse.data.createClusterAgent.errors[0],
);
});
});
describe('error creating token', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)],
[createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
});
});
describe('an agent is selected', () => {
beforeEach(() => {
findAgentDropdown().vm.$emit('agentSelected');
});
it('enables the next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expectDisabledAttribute(findActionButton(), false);
});
});
describe('registering an agent', () => {
const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
describe('when there are no agent configurations present', () => {
const i18n = I18N_AGENT_MODAL.install;
const apolloQueryEmptyResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: [] },
},
},
};
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryEmptyResponse)],
]);
return mockSelectedAgentResponse(apolloProvider);
createWrapper();
});
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath },
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
input: { clusterAgentId: 'agent-id', name: 'agent-name' },
});
it('renders empty state image', () => {
expect(findImage().attributes('src')).toBe(emptyStateImage);
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
it('shows agent instructions', () => {
const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
const token = findModal().findComponent(GlFormInputGroup);
expect(token.props('value')).toBe('mock-agent-token');
const alert = findModal().findComponent(GlAlert);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
const code = findModal().findComponent(CodeBlock).props('code');
expect(code).toContain('--agent-token=mock-agent-token');
expect(code).toContain('--kas-address=kas.example.com');
});
describe('error creating agent', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(createAgentErrorResponse.data.createClusterAgent.errors[0]);
});
});
describe('error creating token', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)],
[createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
it('renders a secondary button', () => {
expect(findSecondaryButton().isVisible()).toBe(true);
expect(findSecondaryButton().text()).toBe(i18n.secondaryButton);
});
});
});

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::BaseEdge do
include GraphqlHelpers
let_it_be(:test_schema) do
project_edge_type = Class.new(described_class) do
field :proof_of_admin_rights, String,
null: true, authorize: :admin_project
def proof_of_admin_rights
'ok'
end
end
project_type = Class.new(::Types::BaseObject) do
graphql_name 'Project'
authorize :read_project
edge_type_class project_edge_type
field :name, String, null: false
end
Class.new(GraphQL::Schema) do
lazy_resolve ::Gitlab::Graphql::Lazy, :force
use ::GraphQL::Pagination::Connections
use ::Gitlab::Graphql::Pagination::Connections
query(Class.new(::Types::BaseObject) do
graphql_name 'Query'
field :projects, project_type.connection_type, null: false
def projects
context[:projects]
end
end)
end
end
def document
GraphQL.parse(<<~GQL)
query {
projects {
edges {
proofOfAdminRights
node { name }
}
}
}
GQL
end
it 'supports field authorization on edge fields' do
user = create(:user)
private_project = create(:project, :private)
member_project = create(:project, :private)
maintainer_project = create(:project, :private)
public_project = create(:project, :public)
member_project.add_developer(user)
maintainer_project.add_maintainer(user)
projects = [private_project, member_project, maintainer_project, public_project]
data = { current_user: user, projects: projects }
query = GraphQL::Query.new(test_schema, document: document, context: data)
result = query.result.to_h
expect(graphql_dig_at(result, 'data', 'projects', 'edges', 'node', 'name'))
.to contain_exactly(member_project.name, maintainer_project.name, public_project.name)
expect(graphql_dig_at(result, 'data', 'projects', 'edges', 'proofOfAdminRights'))
.to contain_exactly(nil, 'ok', nil)
end
end

View File

@ -0,0 +1,176 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
let_it_be(:entity) do
create(
:bulk_import_entity,
:project_entity,
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Project',
destination_namespace: group.full_path
)
end
let(:ci_pipeline_attributes) { {} }
let(:ci_pipeline) do
{
sha: "fakesha",
ref: "fakeref",
project: project,
source: "web"
}.merge(ci_pipeline_attributes)
end
let(:ci_pipeline2) do
{
sha: "fakesha2",
ref: "fakeref2",
project: project,
source: "web"
}.merge(ci_pipeline_attributes)
end
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
subject(:pipeline) { described_class.new(context) }
describe '#run' do
before do
group.add_owner(user)
allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
allow(extractor).to receive(:extract).and_return(
BulkImports::Pipeline::ExtractedData.new(data: [ci_pipeline, ci_pipeline2])
)
end
allow_next_instance_of(Repository) do |repository|
allow(repository).to receive(:fetch_source_branch!)
end
pipeline.run
end
it 'imports Ci::Pipeline into destination project' do
expect(project.all_pipelines.count).to eq(2)
expect(project.ci_pipelines.first.sha).to eq('fakesha')
expect(project.ci_pipelines.second.sha).to eq('fakesha2')
end
context 'notes' do
let(:ci_pipeline_attributes) do
{
'notes' => [
{
'note' => 'test note',
'author_id' => 22,
'noteable_type' => 'Commit',
'sha' => '',
'author' => {
'name' => 'User 22'
},
'commit_id' => 'fakesha',
'updated_at' => '2016-06-14T15:02:47.770Z',
'events' => [
{
'action' => 'created',
'author_id' => 22
}
]
}
]
}
end
it 'imports pipeline with notes' do
note = project.all_pipelines.first.notes.first
expect(note.note).to include('test note')
expect(note.events.first.action).to eq('created')
end
end
context 'stages' do
let(:ci_pipeline_attributes) do
{
'stages' => [
{
'name' => 'test stage',
'statuses' => [
{
'name' => 'first status',
'status' => 'created'
}
]
}
]
}
end
it 'imports pipeline with notes' do
stage = project.all_pipelines.first.stages.first
expect(stage.name).to eq('test stage')
expect(stage.statuses.first.name).to eq('first status')
end
end
context 'external pull request' do
let(:ci_pipeline_attributes) do
{
'source' => 'external_pull_request_event',
'external_pull_request' => {
'source_branch' => 'test source branch',
'target_branch' => 'master',
'source_sha' => 'testsha',
'target_sha' => 'targetsha',
'source_repository' => 'test repository',
'target_repository' => 'test repository',
'status' => 1,
'pull_request_iid' => 1
}
}
end
it 'imports pipeline with external pull request' do
pull_request = project.all_pipelines.first.external_pull_request
expect(pull_request.source_branch).to eq('test source branch')
expect(pull_request.status).to eq('open')
end
end
context 'merge request' do
let(:ci_pipeline_attributes) do
{
'source' => 'merge_request_event',
'merge_request' => {
'description' => 'test merge request',
'title' => 'test MR',
'source_branch' => 'test source branch',
'target_branch' => 'master',
'source_sha' => 'testsha',
'target_sha' => 'targetsha',
'source_repository' => 'test repository',
'target_repository' => 'test repository',
'target_project_id' => project.id,
'source_project_id' => project.id,
'author_id' => user.id
}
}
end
it 'imports pipeline with external pull request' do
merge_request = project.all_pipelines.first.merge_request
expect(merge_request.source_branch).to eq('test source branch')
expect(merge_request.description).to eq('test merge request')
end
end
end
end

View File

@ -14,6 +14,7 @@ RSpec.describe BulkImports::Projects::Stage do
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
[4, BulkImports::Projects::Pipelines::ProtectedBranchesPipeline],
[4, BulkImports::Projects::Pipelines::CiPipelinesPipeline],
[5, BulkImports::Common::Pipelines::WikiPipeline],
[5, BulkImports::Common::Pipelines::UploadsPipeline],
[6, BulkImports::Common::Pipelines::EntityFinisher]

View File

@ -53,7 +53,7 @@ RSpec.describe Gitlab::Diff::File do
describe 'initialize' do
context 'when file is ipynb with a change after transformation' do
let(:commit) { project.commit("f6b7a707") }
let(:commit) { project.commit("532c837") }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
@ -63,7 +63,7 @@ RSpec.describe Gitlab::Diff::File do
end
it 'recreates the diff by transforming the files' do
expect(diff_file.diff.diff).not_to include('"| Fake')
expect(diff_file.diff.diff).not_to include('cell_type')
end
end
@ -73,7 +73,7 @@ RSpec.describe Gitlab::Diff::File do
end
it 'does not recreate the diff' do
expect(diff_file.diff.diff).to include('"| Fake')
expect(diff_file.diff.diff).to include('cell_type')
end
end
end

View File

@ -108,45 +108,6 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
end
describe '#between' do
let(:from) { 'master' }
let(:to) { Gitlab::Git::EMPTY_TREE_ID }
context 'with between_commits_via_list_commits enabled' do
before do
stub_feature_flags(between_commits_via_list_commits: true)
end
it 'sends an RPC request' do
request = Gitaly::ListCommitsRequest.new(
repository: repository_message, revisions: ["^" + from, to], reverse: true
)
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:list_commits)
.with(request, kind_of(Hash)).and_return([])
described_class.new(repository).between(from, to)
end
end
context 'with between_commits_via_list_commits disabled' do
before do
stub_feature_flags(between_commits_via_list_commits: false)
end
it 'sends an RPC request' do
request = Gitaly::CommitsBetweenRequest.new(
repository: repository_message, from: from, to: to
)
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commits_between)
.with(request, kind_of(Hash)).and_return([])
described_class.new(repository).between(from, to)
end
end
end
describe '#diff_stats' do
let(:left_commit_id) { 'master' }
let(:right_commit_id) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }

View File

@ -15,6 +15,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
assignee_id
assignee_ids
author_id
blocking_discussions_resolved
created_at
description
head_pipeline_id

View File

@ -153,6 +153,25 @@ RSpec.describe Sidebars::Menu do
end
end
describe '#remove_element' do
let(:item1) { Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}, item_id: :foo1) }
let(:item2) { Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: {}, item_id: :foo2) }
let(:item3) { Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: {}, item_id: :foo3) }
let(:list) { [item1, item2, item3] }
it 'removes specific element' do
menu.remove_element(list, :foo2)
expect(list).to eq [item1, item3]
end
it 'does not remove nil elements' do
menu.remove_element(list, nil)
expect(list).to eq [item1, item2, item3]
end
end
describe '#container_html_options' do
before do
allow(menu).to receive(:title).and_return('Foo Menu')

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :migration do
before do
allow(Gitlab).to receive(:ee?).and_return(ee?)
stub_const("#{described_class.name}::BATCH_SIZE", 2)
end
context 'when the Gitlab instance is FOSS' do
let(:ee?) { false }
it 'does not run the migration' do
expect { migrate! }.not_to change { BackgroundMigrationWorker.jobs.size }
end
end
context 'when the Gitlab instance is EE' do
let(:ee?) { true }
let_it_be(:namespaces) { table(:namespaces) }
let_it_be(:projects) { table(:projects) }
let_it_be(:findings) { table(:vulnerability_occurrences) }
let_it_be(:scanners) { table(:vulnerability_scanners) }
let_it_be(:identifiers) { table(:vulnerability_identifiers) }
let_it_be(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
let_it_be(:namespace) { namespaces.create!(name: 'test', path: 'test') }
let_it_be(:project) { projects.create!(namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') }
let_it_be(:scanner) do
scanners.create!(project_id: project.id, external_id: 'trivy', name: 'Security Scanner')
end
let_it_be(:identifier) do
identifiers.create!(project_id: project.id,
fingerprint: 'd432c2ad2953e8bd587a3a43b3ce309b5b0154c123',
external_type: 'SECURITY_ID',
external_id: 'SECURITY_0',
name: 'SECURITY_IDENTIFIER 0')
end
let_it_be(:finding1) { findings.create!(finding_params) }
let_it_be(:signature1) { vulnerability_finding_signatures.create!(finding_id: finding1.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
let_it_be(:finding2) { findings.create!(finding_params) }
let_it_be(:signature2) { vulnerability_finding_signatures.create!(finding_id: finding2.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
let_it_be(:finding3) { findings.create!(finding_params) }
let_it_be(:signature3) { vulnerability_finding_signatures.create!(finding_id: finding3.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
it 'schedules the background jobs', :aggregate_failure do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION)
.to be_scheduled_migration_with_multiple_args(signature1.id, signature2.id)
expect(described_class::MIGRATION)
.to be_scheduled_migration_with_multiple_args(signature3.id, signature3.id)
end
end
end
def finding_params
uuid = SecureRandom.uuid
{
severity: 0,
confidence: 5,
report_type: 2,
project_id: project.id,
scanner_id: scanner.id,
primary_identifier_id: identifier.id,
location: nil,
project_fingerprint: SecureRandom.hex(20),
location_fingerprint: Digest::SHA1.hexdigest(SecureRandom.hex(10)),
uuid: uuid,
name: "Vulnerability Finding #{uuid}",
metadata_version: '1.3',
raw_metadata: '{}'
}
end
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe 'package details' do
subject
expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to be_empty
expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil]
end
end
end

View File

@ -26,6 +26,12 @@ RSpec.describe MergeRequests::ResolvedDiscussionNotificationService do
subject.execute(merge_request)
end
it "doesn't send a webhook" do
expect_any_instance_of(MergeRequests::BaseService).not_to receive(:execute_hooks)
subject.execute(merge_request)
end
end
context "when all discussions are resolved" do
@ -44,6 +50,12 @@ RSpec.describe MergeRequests::ResolvedDiscussionNotificationService do
subject.execute(merge_request)
end
it "sends a webhook" do
expect_any_instance_of(MergeRequests::BaseService).to receive(:execute_hooks).with(merge_request, 'update')
subject.execute(merge_request)
end
end
end
end

View File

@ -514,8 +514,13 @@ module GraphqlHelpers
# Allows for array indexing, like this
# ['project', 'boards', 'edges', 0, 'node', 'lists']
keys.reduce(data) do |memo, key|
if memo.is_a?(Array)
key.is_a?(Integer) ? memo[key] : memo.flat_map { |e| Array.wrap(e[key]) }
if memo.is_a?(Array) && key.is_a?(Integer)
memo[key]
elsif memo.is_a?(Array)
memo.compact.flat_map do |e|
x = e[key]
x.nil? ? [x] : Array.wrap(x)
end
else
memo&.dig(key)
end

View File

@ -53,7 +53,7 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
'add-ipython-files' => '2b5ef814',
'add-ipython-files' => '532c837',
'add-pdf-file' => 'e774ebd',
'squash-large-files' => '54cec52',
'add-pdf-text-binary' => '79faa7b',

View File

@ -55,7 +55,7 @@ RSpec.shared_examples 'group and project packages query' do
end
it 'deals with metadata' do
expect(target_shas).to contain_exactly(composer_metadatum.target_sha)
expect(target_shas.compact).to contain_exactly(composer_metadatum.target_sha)
end
it 'returns the count of the packages' do

View File

@ -43,6 +43,21 @@ RSpec.describe GraphqlHelpers do
expect(graphql_dig_at(data, :foo, :nodes, :bar, :nodes, :id)).to eq([1, 2, 3, 4])
end
it 'does not omit nils at the leaves' do
data = {
'foo' => {
'nodes' => [
{ 'bar' => { 'nodes' => [{ 'id' => nil }, { 'id' => 2 }] } },
{ 'bar' => { 'nodes' => [{ 'id' => 3 }, { 'id' => nil }] } },
{ 'bar' => nil }
]
},
'irrelevant' => 'the field is a red-herring'
}
expect(graphql_dig_at(data, :foo, :nodes, :bar, :nodes, :id)).to eq([nil, 2, 3, nil])
end
end
describe 'var' do

View File

@ -398,7 +398,6 @@ RSpec.describe 'Every Sidekiq worker' do
'PropagateIntegrationInheritWorker' => 3,
'PropagateIntegrationProjectWorker' => 3,
'PropagateIntegrationWorker' => 3,
'PropagateServiceTemplateWorker' => 3,
'PurgeDependencyProxyCacheWorker' => 3,
'ReactiveCachingWorker' => 3,
'RebaseWorker' => 3,