Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1c27dcaf69
commit
492f99eac8
|
@ -1 +1 @@
|
|||
4e18794f846ad0d27bea3443caa2b51cd9afd722
|
||||
1dcc934bcbe0f712306913a5a3d7963d77ae2e70
|
||||
|
|
|
@ -100,13 +100,13 @@ export default {
|
|||
<div :class="`issuable-status-box status-box ${statusBoxClass}`">
|
||||
{{ stateHumanName }}
|
||||
</div>
|
||||
<span class="text-secondary">Opened <time v-text="formattedTime"></time></span>
|
||||
<span class="gl-text-secondary">Opened <time v-text="formattedTime"></time></span>
|
||||
</div>
|
||||
<ci-icon v-if="detailedStatus" :status="detailedStatus" />
|
||||
</div>
|
||||
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
|
||||
<div class="text-secondary">
|
||||
<div class="gl-text-secondary">
|
||||
{{ `${projectPath}!${mergeRequestIID}` }}
|
||||
</div>
|
||||
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlEmptyState,
|
||||
GlFormGroup,
|
||||
GlFormInputGroup,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
|
@ -15,7 +16,10 @@ import {
|
|||
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
|
||||
DEPENDENCY_PROXY_DOCS_PATH,
|
||||
} from '~/packages_and_registries/settings/group/constants';
|
||||
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
|
||||
import {
|
||||
GRAPHQL_PAGE_SIZE,
|
||||
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
|
||||
} from '~/packages_and_registries/dependency_proxy/constants';
|
||||
|
||||
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
|
||||
|
||||
|
@ -25,6 +29,7 @@ export default {
|
|||
GlEmptyState,
|
||||
GlFormGroup,
|
||||
GlFormInputGroup,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
ClipboardButton,
|
||||
|
@ -37,7 +42,7 @@ export default {
|
|||
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
|
||||
),
|
||||
proxyDisabledText: s__(
|
||||
'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.',
|
||||
'DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}.',
|
||||
),
|
||||
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
|
||||
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
|
||||
|
@ -45,6 +50,10 @@ export default {
|
|||
pageTitle: s__('DependencyProxy|Dependency Proxy'),
|
||||
noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
|
||||
},
|
||||
links: {
|
||||
DEPENDENCY_PROXY_DOCS_PATH,
|
||||
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
group: {},
|
||||
|
@ -162,7 +171,11 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
<gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
|
||||
{{ $options.i18n.proxyDisabledText }}
|
||||
<gl-sprintf :message="$options.i18n.proxyDisabledText">
|
||||
<template #docLink="{ content }">
|
||||
<gl-link :href="$options.links.ENABLE_DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export const GRAPHQL_PAGE_SIZE = 20;
|
||||
export const ENABLE_DEPENDENCY_PROXY_DOCS_PATH = helpPagePath(
|
||||
'user/packages/dependency_proxy/index',
|
||||
{ anchor: 'enable-or-disable-the-dependency-proxy-for-a-group' },
|
||||
);
|
||||
|
|
|
@ -18,9 +18,10 @@ export default () => {
|
|||
el,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
groupPath: el.dataset.groupPath,
|
||||
groupDependencyProxyPath: el.dataset.groupDependencyProxyPath,
|
||||
defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
|
||||
dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable),
|
||||
groupPath: el.dataset.groupPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(SettingsApp);
|
||||
|
|
|
@ -23,12 +23,15 @@ export default {
|
|||
i18n: {
|
||||
DEPENDENCY_PROXY_HEADER,
|
||||
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
|
||||
label: s__('DependencyProxy|Enable Proxy'),
|
||||
label: s__('DependencyProxy|Enable Dependency Proxy'),
|
||||
enabledProxyHelpText: s__(
|
||||
'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}',
|
||||
),
|
||||
},
|
||||
links: {
|
||||
DEPENDENCY_PROXY_DOCS_PATH,
|
||||
},
|
||||
inject: ['defaultExpanded', 'groupPath'],
|
||||
inject: ['defaultExpanded', 'groupPath', 'groupDependencyProxyPath'],
|
||||
props: {
|
||||
dependencyProxySettings: {
|
||||
type: Object,
|
||||
|
@ -49,6 +52,9 @@ export default {
|
|||
this.updateSettings({ enabled });
|
||||
},
|
||||
},
|
||||
helpText() {
|
||||
return this.enabled ? this.$options.i18n.enabledProxyHelpText : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateSettings(payload) {
|
||||
|
@ -91,7 +97,11 @@ export default {
|
|||
<span data-testid="description">
|
||||
<gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION">
|
||||
<template #docLink="{ content }">
|
||||
<gl-link :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
|
||||
<gl-link
|
||||
data-testid="description-link"
|
||||
:href="$options.links.DEPENDENCY_PROXY_DOCS_PATH"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
|
@ -102,9 +112,22 @@ export default {
|
|||
v-model="enabled"
|
||||
:disabled="isLoading"
|
||||
:label="$options.i18n.label"
|
||||
:help="helpText"
|
||||
data-qa-selector="dependency_proxy_setting_toggle"
|
||||
data-testid="dependency-proxy-setting-toggle"
|
||||
/>
|
||||
>
|
||||
<template #help>
|
||||
<span class="gl-overflow-break-word gl-max-w-100vw gl-display-inline-block">
|
||||
<gl-sprintf :message="$options.i18n.enabledProxyHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link data-testid="toggle-help-link" :href="groupDependencyProxyPath">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
</gl-toggle>
|
||||
</div>
|
||||
</template>
|
||||
</settings-block>
|
||||
|
|
|
@ -73,7 +73,7 @@ export default {
|
|||
});
|
||||
},
|
||||
onReset() {
|
||||
this.$emit('cancel');
|
||||
this.$emit('resetContent');
|
||||
},
|
||||
scrollIntoView() {
|
||||
this.$el.scrollIntoView({ behavior: 'smooth' });
|
||||
|
@ -86,7 +86,7 @@ export default {
|
|||
startMergeRequest: __('Start a %{new_merge_request} with these changes'),
|
||||
newMergeRequest: __('new merge request'),
|
||||
commitChanges: __('Commit changes'),
|
||||
cancel: __('Cancel'),
|
||||
resetContent: __('Reset'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -148,7 +148,7 @@ export default {
|
|||
{{ $options.i18n.commitChanges }}
|
||||
</gl-button>
|
||||
<gl-button type="reset" category="secondary" class="gl-mr-3">
|
||||
{{ $options.i18n.cancel }}
|
||||
{{ $options.i18n.resetContent }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-form>
|
||||
|
|
|
@ -127,9 +127,6 @@ export default {
|
|||
this.isSaving = false;
|
||||
}
|
||||
},
|
||||
onCommitCancel() {
|
||||
this.$emit('resetContent');
|
||||
},
|
||||
updateCurrentBranch(currentBranch) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCurrentBranchMutation,
|
||||
|
@ -153,7 +150,6 @@ export default {
|
|||
:is-saving="isSaving"
|
||||
:scroll-to-commit-form="scrollToCommitForm"
|
||||
v-on="$listeners"
|
||||
@cancel="onCommitCancel"
|
||||
@submit="onCommitSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
|
||||
|
||||
|
@ -30,6 +30,7 @@ export default {
|
|||
components: {
|
||||
ConfirmUnsavedChangesDialog,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
PipelineEditorEmptyState,
|
||||
PipelineEditorHome,
|
||||
PipelineEditorMessages,
|
||||
|
@ -54,6 +55,7 @@ export default {
|
|||
lastCommittedContent: '',
|
||||
shouldSkipStartScreen: false,
|
||||
showFailure: false,
|
||||
showResetComfirmationModal: false,
|
||||
showStartScreen: false,
|
||||
showSuccess: false,
|
||||
starterTemplate: '',
|
||||
|
@ -224,6 +226,18 @@ export default {
|
|||
tabGraph: s__('Pipelines|Visualize'),
|
||||
tabLint: s__('Pipelines|Lint'),
|
||||
},
|
||||
resetModal: {
|
||||
actionPrimary: {
|
||||
text: __('Reset file'),
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
},
|
||||
body: s__(
|
||||
'Pipeline Editor|Are you sure you want to reset the file to its last committed version?',
|
||||
),
|
||||
title: __('Discard changes'),
|
||||
},
|
||||
watch: {
|
||||
isEmpty(flag) {
|
||||
if (flag) {
|
||||
|
@ -242,6 +256,11 @@ export default {
|
|||
hideSuccess() {
|
||||
this.showSuccess = false;
|
||||
},
|
||||
confirmReset() {
|
||||
if (this.hasUnsavedChanges) {
|
||||
this.showResetComfirmationModal = true;
|
||||
}
|
||||
},
|
||||
async refetchContent() {
|
||||
this.$apollo.queries.initialCiFileContent.skip = false;
|
||||
await this.$apollo.queries.initialCiFileContent.refetch();
|
||||
|
@ -262,6 +281,7 @@ export default {
|
|||
this.successType = type;
|
||||
},
|
||||
resetContent() {
|
||||
this.showResetComfirmationModal = false;
|
||||
this.currentCiFileContent = this.lastCommittedContent;
|
||||
},
|
||||
setAppStatus(appStatus) {
|
||||
|
@ -335,12 +355,22 @@ export default {
|
|||
:has-unsaved-changes="hasUnsavedChanges"
|
||||
:is-new-ci-config-file="isNewCiConfigFile"
|
||||
@commit="updateOnCommit"
|
||||
@resetContent="resetContent"
|
||||
@resetContent="confirmReset"
|
||||
@showError="showErrorAlert"
|
||||
@refetchContent="refetchContent"
|
||||
@updateCiConfig="updateCiConfig"
|
||||
@updateCommitSha="updateCommitSha"
|
||||
/>
|
||||
<gl-modal
|
||||
v-model="showResetComfirmationModal"
|
||||
modal-id="reset-content"
|
||||
:title="$options.resetModal.title"
|
||||
:action-cancel="$options.resetModal.actionCancel"
|
||||
:action-primary="$options.resetModal.actionPrimary"
|
||||
@primary="resetContent"
|
||||
>
|
||||
{{ $options.resetModal.body }}
|
||||
</gl-modal>
|
||||
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
|
||||
%section#js-packages-and-registries-settings{ data: { default_expanded: expanded_by_default?.to_s,
|
||||
group_path: @group.full_path,
|
||||
dependency_proxy_available: dependency_proxy_available.to_s } }
|
||||
dependency_proxy_available: dependency_proxy_available.to_s,
|
||||
group_dependency_proxy_path: group_dependency_proxy_path(@group) } }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings < Gitlab::Database::Migration[1.0]
|
||||
MIGRATION = 'RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings'
|
||||
DELAY_INTERVAL = 2.minutes.to_i
|
||||
BATCH_SIZE = 10_000
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
queue_background_migration_jobs_by_range_at_intervals(
|
||||
define_batchable_model('vulnerability_occurrences'),
|
||||
MIGRATION,
|
||||
DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
track_jobs: true
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
664c7fa75d3283b6e984fcca4ffcefab6dba24a78e4cc24ac86f791ab4495def
|
|
@ -4,7 +4,7 @@ group: Import
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Group Import/Export API **(FREE)**
|
||||
# Group import/export API **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8.
|
||||
|
||||
|
|
|
@ -9,49 +9,49 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6861) in GitLab 11.6.
|
||||
|
||||
[Group owners](../permissions.md#group-members-permissions) can set a subgroup to
|
||||
be the source of project templates that are selectable when a new project is created
|
||||
in the group. These templates can be selected when you go to **New project > Create from template**
|
||||
in the group and select the **Group** tab.
|
||||
When you create a project, you can [choose from a list of templates](../project/working_with_projects.md#project-templates).
|
||||
These templates, for things like GitLab Pages or Ruby, populate the new project with a copy of the files contained in the
|
||||
template. This information is identical to the information used by [GitLab project import/export](../project/settings/import_export.md)
|
||||
and can help you start a new project more quickly.
|
||||
|
||||
Every project in the subgroup, but not nested subgroups, can be selected by members
|
||||
of the group when a new project is created.
|
||||
You can [customize the list](../project/working_with_projects.md#custom-project-templates) of available templates, so
|
||||
that all projects in your group have the same list. To do this, you populate a subgroup with the projects you want to
|
||||
use as templates.
|
||||
|
||||
- Public projects can be selected by any signed-in user as a template for a new project,
|
||||
if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
|
||||
except for **GitLab Pages** and **Security & Compliance** are set to **Everyone With Access**.
|
||||
The same applies to internal projects.
|
||||
- Private projects can be selected only by users who are members of the projects.
|
||||
|
||||
Repository and database information that is copied over to each new project is identical to the
|
||||
data exported with the [GitLab Project Import/Export](../project/settings/import_export.md).
|
||||
|
||||
To set custom project templates at the instance level, see [Custom instance-level project templates](../admin_area/custom_project_templates.md).
|
||||
You can also configure [custom templates for the instance](../admin_area/custom_project_templates.md).
|
||||
|
||||
## Set up group-level project templates
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must have the [Owner role for the group](../permissions.md#group-members-permissions).
|
||||
|
||||
To set up custom project templates in a group, add the subgroup that contains the
|
||||
project templates to the group settings:
|
||||
|
||||
1. In the group, create a [subgroup](subgroups/index.md).
|
||||
1. [Add projects to the new subgroup](index.md#add-projects-to-a-group) as your templates.
|
||||
1. In the left menu for the group, go to **Settings > General**.
|
||||
1. In the left menu for the group, select **Settings > General**.
|
||||
1. Expand **Custom project templates** and select the subgroup.
|
||||
|
||||
If all enabled [project features](../project/settings/index.md#sharing-and-permissions)
|
||||
(except for GitLab Pages) are set to **Everyone With Access**, then every project
|
||||
template in the subgroup is available to every member of the group.
|
||||
The next time a group member creates a project, they can select any of the projects in the subgroup.
|
||||
|
||||
Any projects added to the subgroup later can be selected the next time a group member
|
||||
creates a new project.
|
||||
Projects in nested subgroups are not included in the template list.
|
||||
|
||||
### Example structure
|
||||
## Which projects are available as templates
|
||||
|
||||
Here's a sample group/project structure for project templates, for a hypothetical _Acme Co_:
|
||||
- Public and internal projects can be selected by any signed-in user as a template for a new project,
|
||||
if all [project features](../project/settings/index.md#sharing-and-permissions)
|
||||
except for **GitLab Pages** and **Security & Compliance** are set to **Everyone With Access**.
|
||||
- Private projects can be selected only by users who are members of the projects.
|
||||
|
||||
## Example structure
|
||||
|
||||
Here's a sample group and project structure for project templates, for `myorganization`:
|
||||
|
||||
```plaintext
|
||||
# GitLab instance and group
|
||||
gitlab.com/acmeco/
|
||||
gitlab.com/myorganization/
|
||||
# Subgroups
|
||||
internal
|
||||
tools
|
||||
|
@ -61,7 +61,7 @@ gitlab.com/acmeco/
|
|||
# Project templates
|
||||
client-site-django
|
||||
client-site-gatsby
|
||||
client-site-hTML
|
||||
client-site-html
|
||||
|
||||
# Other projects
|
||||
client-site-a
|
||||
|
|
|
@ -9,19 +9,16 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.
|
||||
|
||||
Existing groups running on any GitLab instance or GitLab.com can be exported with all their related data and moved to a
|
||||
new GitLab instance.
|
||||
You can export groups, with all their related data, from one GitLab instance to another.
|
||||
You can also [export projects](../../project/settings/import_export.md).
|
||||
|
||||
The **GitLab import/export** button is displayed if the group import option in enabled.
|
||||
## Enable export for a group
|
||||
|
||||
See also:
|
||||
Prerequisite:
|
||||
|
||||
- [Group Import/Export API](../../../api/group_import_export.md)
|
||||
- [Project Import/Export](../../project/settings/import_export.md)
|
||||
- [Project Import/Export API](../../../api/project_import_export.md)
|
||||
- You must have the [Owner role](../../permissions.md) for the group.
|
||||
|
||||
Users with the [Owner role](../../permissions.md) for a group can enable
|
||||
import and export for that group:
|
||||
To enable import and export for a group:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
|
@ -70,8 +67,11 @@ WARNING:
|
|||
This feature will be [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619)
|
||||
in GitLab 14.6 and replaced by [GitLab Migration](../import/).
|
||||
|
||||
Users with the [Owner role](../../permissions.md) for a group can export the
|
||||
contents of that group:
|
||||
Prerequisites:
|
||||
|
||||
- You must have the [Owner role](../../permissions.md) for the group.
|
||||
|
||||
To export the contents of a group:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
|
@ -88,6 +88,8 @@ NOTE:
|
|||
The maximum import file size can be set by the Administrator, default is `0` (unlimited).
|
||||
As an administrator, you can modify the maximum import file size. To do so, use the `max_import_size` option in the [Application settings API](../../../api/settings.md#change-application-settings) or the [Admin UI](../../admin_area/settings/account_and_limit_settings.md). Default [modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8.
|
||||
|
||||
You can also use the [group import/export API](../../../api/group_import_export.md).
|
||||
|
||||
### Between CE and EE
|
||||
|
||||
You can export groups from the [Community Edition to the Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/) and vice versa.
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration will look for Vulnerabilities::Finding objects that would have a duplicate UUIDv5 if the UUID was
|
||||
# recalculated. Then it removes Vulnerabilities::FindingPipeline objects associated with those Findings.
|
||||
# We can't just drop those Findings directly since the cascade drop will timeout if any given Finding has too many
|
||||
# associated FindingPipelines
|
||||
class Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings
|
||||
# rubocop:disable Gitlab/NamespacedClass, Style/Documentation
|
||||
class VulnerabilitiesFinding < ActiveRecord::Base
|
||||
self.table_name = "vulnerability_occurrences"
|
||||
end
|
||||
|
||||
class VulnerabilitiesFindingPipeline < ActiveRecord::Base
|
||||
include EachBatch
|
||||
self.table_name = "vulnerability_occurrence_pipelines"
|
||||
end
|
||||
# rubocop:enable Gitlab/NamespacedClass, Style/Documentation
|
||||
|
||||
def perform(start_id, end_id)
|
||||
ids_to_look_for = findings_that_would_produce_duplicate_uuids(start_id, end_id)
|
||||
|
||||
ids_to_look_for.each do |finding_id|
|
||||
VulnerabilitiesFindingPipeline.where(occurrence_id: finding_id).each_batch(of: 1000) do |pipelines|
|
||||
pipelines.delete_all
|
||||
end
|
||||
end
|
||||
|
||||
VulnerabilitiesFinding.where(id: ids_to_look_for).delete_all
|
||||
|
||||
mark_job_as_succeeded(start_id, end_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def findings_that_would_produce_duplicate_uuids(start_id, end_id)
|
||||
VulnerabilitiesFinding
|
||||
.from("vulnerability_occurrences to_delete")
|
||||
.where("to_delete.id BETWEEN ? AND ?", start_id, end_id)
|
||||
.where(
|
||||
"EXISTS (
|
||||
SELECT 1
|
||||
FROM vulnerability_occurrences duplicates
|
||||
WHERE duplicates.report_type = to_delete.report_type
|
||||
AND duplicates.location_fingerprint = to_delete.location_fingerprint
|
||||
AND duplicates.primary_identifier_id = to_delete.primary_identifier_id
|
||||
AND duplicates.project_id = to_delete.project_id
|
||||
AND duplicates.id > to_delete.id
|
||||
)"
|
||||
)
|
||||
.pluck("to_delete.id")
|
||||
end
|
||||
|
||||
def mark_job_as_succeeded(*arguments)
|
||||
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
|
||||
self.class.name.demodulize,
|
||||
arguments
|
||||
)
|
||||
end
|
||||
end
|
|
@ -42,7 +42,7 @@ module Gitlab
|
|||
def renamed_tables_cache
|
||||
@renamed_tables ||= begin
|
||||
Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, new_name|
|
||||
ActiveRecord::Base.connection.view_exists?(old_name)
|
||||
connection.view_exists?(old_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11426,24 +11426,27 @@ msgstr ""
|
|||
msgid "DependencyProxy|Dependency Proxy"
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner."
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|Dependency Proxy feature is limited to public groups for now."
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|Dependency Proxy image prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|Enable Proxy"
|
||||
msgid "DependencyProxy|Enable Dependency Proxy"
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|Image list"
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|There are no images in the cache"
|
||||
msgstr ""
|
||||
|
||||
msgid "DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Depends on %d merge request being merged"
|
||||
msgid_plural "Depends on %d merge requests being merged"
|
||||
msgstr[0] ""
|
||||
|
@ -25301,6 +25304,9 @@ msgstr ""
|
|||
msgid "Pipeline Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline Editor|Are you sure you want to reset the file to its last committed version?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline ID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29535,6 +29541,9 @@ msgstr ""
|
|||
msgid "Reset authorization key?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reset file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reset filters"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -52,4 +52,53 @@ RSpec.describe 'Pipeline Editor', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Editor content' do
|
||||
it 'user can reset their CI configuration' do
|
||||
click_button 'Collapse'
|
||||
|
||||
page.within('#source-editor-') do
|
||||
find('textarea').send_keys '123'
|
||||
end
|
||||
|
||||
# It takes some time after sending keys for the reset
|
||||
# btn to register the changes inside the editor
|
||||
sleep 1
|
||||
click_button 'Reset'
|
||||
|
||||
expect(page).to have_css('#reset-content')
|
||||
|
||||
page.within('#reset-content') do
|
||||
click_button 'Reset file'
|
||||
end
|
||||
|
||||
page.within('#source-editor-') do
|
||||
expect(page).to have_content('Default Content')
|
||||
expect(page).not_to have_content('Default Content123')
|
||||
end
|
||||
end
|
||||
|
||||
it 'user can cancel reseting their CI configuration' do
|
||||
click_button 'Collapse'
|
||||
|
||||
page.within('#source-editor-') do
|
||||
find('textarea').send_keys '123'
|
||||
end
|
||||
|
||||
# It takes some time after sending keys for the reset
|
||||
# btn to register the changes inside the editor
|
||||
sleep 1
|
||||
click_button 'Reset'
|
||||
|
||||
expect(page).to have_css('#reset-content')
|
||||
|
||||
page.within('#reset-content') do
|
||||
click_button 'Cancel'
|
||||
end
|
||||
|
||||
page.within('#source-editor-') do
|
||||
expect(page).to have_content('Default Content123')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,7 +26,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
|
|||
</div>
|
||||
|
||||
<span
|
||||
class="text-secondary"
|
||||
class="gl-text-secondary"
|
||||
>
|
||||
Opened
|
||||
<time>
|
||||
|
@ -49,7 +49,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
|
|||
</h5>
|
||||
|
||||
<div
|
||||
class="text-secondary"
|
||||
class="gl-text-secondary"
|
||||
>
|
||||
|
||||
foo/bar!1
|
||||
|
@ -80,7 +80,7 @@ exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
|
|||
<!---->
|
||||
|
||||
<div
|
||||
class="text-secondary"
|
||||
class="gl-text-secondary"
|
||||
>
|
||||
|
||||
foo/bar!1
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
GlFormGroup,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
|
@ -11,7 +12,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stripTypenames } from 'helpers/graphql_helpers';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
|
||||
import {
|
||||
GRAPHQL_PAGE_SIZE,
|
||||
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
|
||||
} from '~/packages_and_registries/dependency_proxy/constants';
|
||||
|
||||
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
|
@ -55,6 +59,7 @@ describe('DependencyProxyApp', () => {
|
|||
|
||||
const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available');
|
||||
const findProxyDisabledAlert = () => wrapper.findByTestId('proxy-disabled');
|
||||
const findDisabledAlertLink = () => findProxyDisabledAlert().findComponent(GlLink);
|
||||
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
|
||||
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
|
||||
|
@ -238,7 +243,15 @@ describe('DependencyProxyApp', () => {
|
|||
});
|
||||
|
||||
it('shows a proxy disabled alert', () => {
|
||||
expect(findProxyDisabledAlert().text()).toBe(DependencyProxyApp.i18n.proxyDisabledText);
|
||||
expect(findProxyDisabledAlert().text()).toMatchInterpolatedText(
|
||||
DependencyProxyApp.i18n.proxyDisabledText,
|
||||
);
|
||||
});
|
||||
|
||||
it('disabled alert has a link to the docs', () => {
|
||||
expect(findDisabledAlertLink().attributes()).toMatchObject({
|
||||
href: ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GlSprintf, GlLink, GlToggle } from '@gitlab/ui';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlSprintf, GlToggle } from '@gitlab/ui';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
|
@ -16,7 +17,7 @@ import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/gr
|
|||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
|
||||
import {
|
||||
dependencyProxySettings,
|
||||
dependencyProxySettings as dependencyProxySettingsMock,
|
||||
dependencyProxySettingMutationMock,
|
||||
groupPackageSettingsMock,
|
||||
dependencyProxySettingMutationErrorMock,
|
||||
|
@ -34,6 +35,7 @@ describe('DependencyProxySettings', () => {
|
|||
const defaultProvide = {
|
||||
defaultExpanded: false,
|
||||
groupPath: 'foo_group_path',
|
||||
groupDependencyProxyPath: 'group_dependency_proxy_path',
|
||||
};
|
||||
|
||||
localVue.use(VueApollo);
|
||||
|
@ -42,21 +44,23 @@ describe('DependencyProxySettings', () => {
|
|||
provide = defaultProvide,
|
||||
mutationResolver = jest.fn().mockResolvedValue(dependencyProxySettingMutationMock()),
|
||||
isLoading = false,
|
||||
dependencyProxySettings = dependencyProxySettingsMock(),
|
||||
} = {}) => {
|
||||
const requestHandlers = [[updateDependencyProxySettings, mutationResolver]];
|
||||
|
||||
apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = shallowMount(component, {
|
||||
wrapper = shallowMountExtended(component, {
|
||||
localVue,
|
||||
apolloProvider,
|
||||
provide,
|
||||
propsData: {
|
||||
dependencyProxySettings: dependencyProxySettings(),
|
||||
dependencyProxySettings,
|
||||
isLoading,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlToggle,
|
||||
SettingsBlock,
|
||||
},
|
||||
});
|
||||
|
@ -67,9 +71,10 @@ describe('DependencyProxySettings', () => {
|
|||
});
|
||||
|
||||
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
|
||||
const findDescription = () => wrapper.find('[data-testid="description"');
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
const findDescription = () => wrapper.findByTestId('description');
|
||||
const findDescriptionLink = () => wrapper.findByTestId('description-link');
|
||||
const findToggle = () => wrapper.findComponent(GlToggle);
|
||||
const findToggleHelpLink = () => wrapper.findByTestId('toggle-help-link');
|
||||
|
||||
const fillApolloCache = () => {
|
||||
apolloProvider.defaultClient.cache.writeQuery({
|
||||
|
@ -112,10 +117,59 @@ describe('DependencyProxySettings', () => {
|
|||
it('has the correct link', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findLink().attributes()).toMatchObject({
|
||||
expect(findDescriptionLink().attributes()).toMatchObject({
|
||||
href: DEPENDENCY_PROXY_DOCS_PATH,
|
||||
});
|
||||
expect(findLink().text()).toBe('Learn more');
|
||||
expect(findDescriptionLink().text()).toBe('Learn more');
|
||||
});
|
||||
|
||||
describe('enable toggle', () => {
|
||||
it('exists', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findToggle().props()).toMatchObject({
|
||||
label: component.i18n.label,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when enabled', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('has the help prop correctly set', () => {
|
||||
expect(findToggle().props()).toMatchObject({
|
||||
help: component.i18n.enabledProxyHelpText,
|
||||
});
|
||||
});
|
||||
|
||||
it('has help text with a link', () => {
|
||||
expect(findToggle().text()).toContain(
|
||||
'To see the image prefix and what is in the cache, visit the Dependency Proxy',
|
||||
);
|
||||
expect(findToggleHelpLink().attributes()).toMatchObject({
|
||||
href: defaultProvide.groupDependencyProxyPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when disabled', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
dependencyProxySettings: dependencyProxySettingsMock({ enabled: false }),
|
||||
});
|
||||
});
|
||||
|
||||
it('has the help prop set to empty', () => {
|
||||
expect(findToggle().props()).toMatchObject({
|
||||
help: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('the help text is not visible', () => {
|
||||
expect(findToggleHelpLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings update', () => {
|
||||
|
|
|
@ -5,8 +5,9 @@ export const packageSettings = () => ({
|
|||
genericDuplicateExceptionRegex: '',
|
||||
});
|
||||
|
||||
export const dependencyProxySettings = () => ({
|
||||
export const dependencyProxySettings = (extend) => ({
|
||||
enabled: true,
|
||||
...extend,
|
||||
});
|
||||
|
||||
export const groupPackageSettingsMock = {
|
||||
|
|
|
@ -78,7 +78,7 @@ describe('Pipeline Editor | Commit Form', () => {
|
|||
it('emits an event when the form resets', () => {
|
||||
findCancelBtn().trigger('click');
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1);
|
||||
expect(wrapper.emitted('resetContent')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -103,11 +103,6 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
await waitForPromises();
|
||||
};
|
||||
|
||||
const cancelCommitForm = async () => {
|
||||
const findCancelBtn = () => wrapper.find('[type="reset"]');
|
||||
await findCancelBtn().trigger('click');
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mockMutate.mockReset();
|
||||
wrapper.destroy();
|
||||
|
@ -266,18 +261,6 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the commit form is cancelled', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('emits an event so that it cab be reseted', async () => {
|
||||
await cancelCommitForm();
|
||||
|
||||
expect(wrapper.emitted('resetContent')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets listeners on commit form', () => {
|
||||
const handler = jest.fn();
|
||||
createComponent({ options: { listeners: { event: handler } } });
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings do
|
||||
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
|
||||
let(:users) { table(:users) }
|
||||
let(:user) { create_user! }
|
||||
let(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
|
||||
let(:scanners) { table(:vulnerability_scanners) }
|
||||
let!(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
|
||||
let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
|
||||
let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
|
||||
let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
|
||||
let(:vulnerabilities) { table(:vulnerabilities) }
|
||||
let(:vulnerability_findings) { table(:vulnerability_occurrences) }
|
||||
let(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
|
||||
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
|
||||
let(:vulnerability_identifier) do
|
||||
vulnerability_identifiers.create!(
|
||||
id: 1244459,
|
||||
project_id: project.id,
|
||||
external_type: 'vulnerability-identifier',
|
||||
external_id: 'vulnerability-identifier',
|
||||
fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
|
||||
name: 'vulnerability identifier')
|
||||
end
|
||||
|
||||
let!(:vulnerability_for_first_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:first_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 5606961,
|
||||
uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
|
||||
vulnerability_id: vulnerability_for_first_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner1.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:vulnerability_for_second_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:second_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 8765432,
|
||||
uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
|
||||
vulnerability_id: vulnerability_for_second_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner2.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:vulnerability_for_third_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:third_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 8832995,
|
||||
uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
|
||||
vulnerability_id: vulnerability_for_third_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner3.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:unrelated_finding) do
|
||||
create_finding!(
|
||||
id: 9999999,
|
||||
uuid: "unreleated_finding",
|
||||
vulnerability_id: nil,
|
||||
report_type: 1,
|
||||
location_fingerprint: 'random_location_fingerprint',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: unrelated_scanner.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
subject { described_class.new.perform(first_finding_duplicate.id, unrelated_finding.id) }
|
||||
|
||||
before do
|
||||
4.times do
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: first_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: second_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: third_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: unrelated_finding.id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes Vulnerabilities::OccurrencePipelines for matching Vulnerabilities::Finding' do
|
||||
expect(vulnerability_findings.count).to eq(4)
|
||||
expect(vulnerability_finding_pipelines.count).to eq(16)
|
||||
|
||||
expect { subject }.to change(vulnerability_finding_pipelines, :count).from(16).to(8)
|
||||
.and change(vulnerability_findings, :count).from(4).to(2)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
|
||||
vulnerabilities.create!(
|
||||
project_id: project_id,
|
||||
author_id: author_id,
|
||||
title: title,
|
||||
severity: severity,
|
||||
confidence: confidence,
|
||||
report_type: report_type
|
||||
)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def create_finding!(
|
||||
id: nil,
|
||||
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
|
||||
name: "test", severity: 7, confidence: 7, report_type: 0,
|
||||
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
|
||||
metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
|
||||
params = {
|
||||
vulnerability_id: vulnerability_id,
|
||||
project_id: project_id,
|
||||
name: name,
|
||||
severity: severity,
|
||||
confidence: confidence,
|
||||
report_type: report_type,
|
||||
project_fingerprint: project_fingerprint,
|
||||
scanner_id: scanner_id,
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
location_fingerprint: location_fingerprint,
|
||||
metadata_version: metadata_version,
|
||||
raw_metadata: raw_metadata,
|
||||
uuid: uuid
|
||||
}
|
||||
params[:id] = id unless id.nil?
|
||||
vulnerability_findings.create!(params)
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
|
||||
table(:users).create!(
|
||||
name: name,
|
||||
email: email,
|
||||
username: name,
|
||||
projects_limit: 0,
|
||||
user_type: user_type,
|
||||
confirmed_at: confirmed_at
|
||||
)
|
||||
end
|
||||
|
||||
def create_finding_pipeline!(project_id:, finding_id:)
|
||||
pipeline = table(:ci_pipelines).create!(project_id: project_id)
|
||||
vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,191 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings, :migration do
|
||||
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
|
||||
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
|
||||
let_it_be(:users) { table(:users) }
|
||||
let_it_be(:user) { create_user! }
|
||||
let_it_be(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
|
||||
let_it_be(:pipelines) { table(:ci_pipelines) }
|
||||
let_it_be(:scanners) { table(:vulnerability_scanners) }
|
||||
let_it_be(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
|
||||
let_it_be(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
|
||||
let_it_be(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
|
||||
let_it_be(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
|
||||
let_it_be(:vulnerabilities) { table(:vulnerabilities) }
|
||||
let_it_be(:vulnerability_findings) { table(:vulnerability_occurrences) }
|
||||
let_it_be(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
|
||||
let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
|
||||
let_it_be(:vulnerability_identifier) do
|
||||
vulnerability_identifiers.create!(
|
||||
id: 1244459,
|
||||
project_id: project.id,
|
||||
external_type: 'vulnerability-identifier',
|
||||
external_id: 'vulnerability-identifier',
|
||||
fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
|
||||
name: 'vulnerability identifier')
|
||||
end
|
||||
|
||||
let_it_be(:vulnerability_for_first_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:first_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 5606961,
|
||||
uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
|
||||
vulnerability_id: vulnerability_for_first_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner1.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:vulnerability_for_second_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:second_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 8765432,
|
||||
uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
|
||||
vulnerability_id: vulnerability_for_second_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner2.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:vulnerability_for_third_duplicate) do
|
||||
create_vulnerability!(
|
||||
project_id: project.id,
|
||||
author_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:third_finding_duplicate) do
|
||||
create_finding!(
|
||||
id: 8832995,
|
||||
uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
|
||||
vulnerability_id: vulnerability_for_third_duplicate.id,
|
||||
report_type: 0,
|
||||
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: scanner3.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:unrelated_finding) do
|
||||
create_finding!(
|
||||
id: 9999999,
|
||||
uuid: "unreleated_finding",
|
||||
vulnerability_id: nil,
|
||||
report_type: 1,
|
||||
location_fingerprint: 'random_location_fingerprint',
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
scanner_id: unrelated_scanner.id,
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 1)
|
||||
|
||||
4.times do
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: first_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: second_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: third_finding_duplicate.id)
|
||||
create_finding_pipeline!(project_id: project.id, finding_id: unrelated_finding.id)
|
||||
end
|
||||
end
|
||||
|
||||
around do |example|
|
||||
freeze_time { Sidekiq::Testing.fake! { example.run } }
|
||||
end
|
||||
|
||||
it 'schedules background migrations' do
|
||||
migrate!
|
||||
|
||||
expect(background_migration_jobs.count).to eq(4)
|
||||
expect(background_migration_jobs.first.arguments).to match_array([first_finding_duplicate.id, first_finding_duplicate.id])
|
||||
expect(background_migration_jobs.second.arguments).to match_array([second_finding_duplicate.id, second_finding_duplicate.id])
|
||||
expect(background_migration_jobs.third.arguments).to match_array([third_finding_duplicate.id, third_finding_duplicate.id])
|
||||
expect(background_migration_jobs.fourth.arguments).to match_array([unrelated_finding.id, unrelated_finding.id])
|
||||
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(4)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, first_finding_duplicate.id, first_finding_duplicate.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, second_finding_duplicate.id, second_finding_duplicate.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, third_finding_duplicate.id, third_finding_duplicate.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, unrelated_finding.id, unrelated_finding.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
|
||||
vulnerabilities.create!(
|
||||
project_id: project_id,
|
||||
author_id: author_id,
|
||||
title: title,
|
||||
severity: severity,
|
||||
confidence: confidence,
|
||||
report_type: report_type
|
||||
)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def create_finding!(
|
||||
id: nil,
|
||||
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
|
||||
name: "test", severity: 7, confidence: 7, report_type: 0,
|
||||
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
|
||||
metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
|
||||
params = {
|
||||
vulnerability_id: vulnerability_id,
|
||||
project_id: project_id,
|
||||
name: name,
|
||||
severity: severity,
|
||||
confidence: confidence,
|
||||
report_type: report_type,
|
||||
project_fingerprint: project_fingerprint,
|
||||
scanner_id: scanner_id,
|
||||
primary_identifier_id: vulnerability_identifier.id,
|
||||
location_fingerprint: location_fingerprint,
|
||||
metadata_version: metadata_version,
|
||||
raw_metadata: raw_metadata,
|
||||
uuid: uuid
|
||||
}
|
||||
params[:id] = id unless id.nil?
|
||||
vulnerability_findings.create!(params)
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
|
||||
users.create!(
|
||||
name: name,
|
||||
email: email,
|
||||
username: name,
|
||||
projects_limit: 0,
|
||||
user_type: user_type,
|
||||
confirmed_at: confirmed_at
|
||||
)
|
||||
end
|
||||
|
||||
def create_finding_pipeline!(project_id:, finding_id:)
|
||||
pipeline = pipelines.create!(project_id: project_id)
|
||||
vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue