Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
65de487500
commit
fd5a9d4a57
30 changed files with 472 additions and 98 deletions
|
@ -21,6 +21,8 @@ import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_ite
|
|||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_CREATING,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
TASK_TYPE_NAME,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
|
@ -466,7 +468,7 @@ export default {
|
|||
this.openWorkItemDetailModal(el);
|
||||
} catch (error) {
|
||||
createFlash({
|
||||
message: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
|
||||
message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
|
||||
error,
|
||||
captureError: true,
|
||||
});
|
||||
|
|
|
@ -90,9 +90,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span ref="userAvatar">
|
||||
<gl-avatar
|
||||
ref="userAvatar"
|
||||
:class="{
|
||||
lazy: lazy,
|
||||
[cssClasses]: true,
|
||||
|
@ -108,7 +107,7 @@ export default {
|
|||
tooltipText ||
|
||||
$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
|
||||
"
|
||||
:target="() => $refs.userAvatar.$el"
|
||||
:target="() => $refs.userAvatar"
|
||||
:placement="tooltipPlacement"
|
||||
boundary="window"
|
||||
>
|
||||
|
|
|
@ -39,14 +39,14 @@ export default {
|
|||
:class="{ 'gl-cursor-text': disabled }"
|
||||
aria-labelledby="item-title"
|
||||
>
|
||||
<div
|
||||
<span
|
||||
id="item-title"
|
||||
ref="titleEl"
|
||||
role="textbox"
|
||||
:aria-label="__('Title')"
|
||||
:data-placeholder="placeholder"
|
||||
:contenteditable="!disabled"
|
||||
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base"
|
||||
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base gl-display-block"
|
||||
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
|
||||
@blur="handleBlur"
|
||||
@keyup="handleInput"
|
||||
|
@ -55,8 +55,7 @@ export default {
|
|||
@keydown.meta.u.prevent
|
||||
@keydown.ctrl.b.prevent
|
||||
@keydown.meta.b.prevent
|
||||
>{{ title }}</span
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</h2>
|
||||
</template>
|
||||
|
|
|
@ -8,10 +8,14 @@ import {
|
|||
} from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_DELETE,
|
||||
I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
deleteTask: s__('WorkItem|Delete task'),
|
||||
enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
|
||||
disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
|
||||
},
|
||||
|
@ -31,6 +35,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -53,6 +62,14 @@ export default {
|
|||
},
|
||||
},
|
||||
emits: ['deleteWorkItem', 'toggleWorkItemConfidentiality'],
|
||||
computed: {
|
||||
i18n() {
|
||||
return {
|
||||
deleteWorkItem: sprintfWorkItem(I18N_WORK_ITEM_DELETE, this.workItemType),
|
||||
areYouSureDelete: sprintfWorkItem(I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, this.workItemType),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleToggleWorkItemConfidentiality() {
|
||||
this.track('click_toggle_work_item_confidentiality');
|
||||
|
@ -75,6 +92,7 @@ export default {
|
|||
<div>
|
||||
<gl-dropdown
|
||||
icon="ellipsis_v"
|
||||
data-testid="work-item-actions-dropdown"
|
||||
text-sr-only
|
||||
:text="__('More actions')"
|
||||
category="tertiary"
|
||||
|
@ -97,20 +115,18 @@ export default {
|
|||
v-if="canDelete"
|
||||
v-gl-modal="'work-item-confirm-delete'"
|
||||
data-testid="delete-action"
|
||||
>{{ $options.i18n.deleteTask }}</gl-dropdown-item
|
||||
>{{ i18n.deleteWorkItem }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
<gl-modal
|
||||
modal-id="work-item-confirm-delete"
|
||||
:title="$options.i18n.deleteWorkItem"
|
||||
:ok-title="$options.i18n.deleteWorkItem"
|
||||
:title="i18n.deleteWorkItem"
|
||||
:ok-title="i18n.deleteWorkItem"
|
||||
ok-variant="danger"
|
||||
@ok="handleDeleteWorkItem"
|
||||
@hide="handleCancelDeleteWorkItem"
|
||||
>
|
||||
{{
|
||||
s__('WorkItem|Are you sure you want to delete the task? This action cannot be reversed.')
|
||||
}}
|
||||
{{ i18n.areYouSureDelete }}
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -279,11 +279,12 @@ export default {
|
|||
<work-item-actions
|
||||
v-if="canUpdate || canDelete"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-type="workItemType"
|
||||
:can-delete="canDelete"
|
||||
:can-update="canUpdate"
|
||||
:is-confidential="workItem.confidential"
|
||||
:is-parent-confidential="parentWorkItemConfidentiality"
|
||||
@deleteWorkItem="$emit('deleteWorkItem')"
|
||||
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
|
||||
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
||||
@error="error = $event"
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
i18n,
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
STATE_EVENT_CLOSE,
|
||||
|
@ -93,7 +94,9 @@ export default {
|
|||
throw new Error(errors[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', i18n.updateError);
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<script>
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Tracking from '~/tracking';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '../constants';
|
||||
import { getUpdateWorkItemMutation } from './update_work_item';
|
||||
import ItemTitle from './item_title.vue';
|
||||
|
||||
|
@ -78,7 +82,8 @@ export default {
|
|||
throw new Error(errors[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', i18n.updateError);
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,11 @@ import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import { __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '../constants';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
|
@ -125,7 +129,8 @@ export default {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit('error', i18n.updateError);
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
|
||||
export const STATE_OPEN = 'OPEN';
|
||||
export const STATE_CLOSED = 'CLOSED';
|
||||
|
@ -31,6 +32,30 @@ export const i18n = {
|
|||
),
|
||||
};
|
||||
|
||||
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
|
||||
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_ERROR_UPDATING = s__(
|
||||
'WorkItem|Something went wrong while updating the %{workItemType}. Please try again.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_ERROR_DELETING = s__(
|
||||
'WorkItem|Something went wrong when deleting the %{workItemType}. Please try again.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_DELETE = s__('WorkItem|Delete %{workItemType}');
|
||||
export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
|
||||
'WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
|
||||
|
||||
export const sprintfWorkItem = (msg, workItemTypeArg) => {
|
||||
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
|
||||
return capitalizeFirstCharacter(
|
||||
sprintf(msg, {
|
||||
workItemType: workItemType.toLocaleLowerCase(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const WIDGET_ICONS = {
|
||||
TASK: 'issue-type-task',
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { getPreferredLocales, s__ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
||||
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
|
||||
|
@ -10,7 +12,6 @@ import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.
|
|||
import ItemTitle from '../components/item_title.vue';
|
||||
|
||||
export default {
|
||||
createErrorText: s__('WorkItem|Something went wrong when creating a task. Please try again'),
|
||||
fetchTypesErrorText: s__(
|
||||
'WorkItem|Something went wrong when fetching work item types. Please try again',
|
||||
),
|
||||
|
@ -69,7 +70,7 @@ export default {
|
|||
update(data) {
|
||||
return data.workspace?.workItemTypes?.nodes.map((node) => ({
|
||||
value: node.id,
|
||||
text: node.name,
|
||||
text: capitalizeFirstCharacter(node.name.toLocaleLowerCase(getPreferredLocales()[0])),
|
||||
}));
|
||||
},
|
||||
error() {
|
||||
|
@ -78,15 +79,19 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownButtonText() {
|
||||
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
|
||||
},
|
||||
formOptions() {
|
||||
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
|
||||
},
|
||||
isButtonDisabled() {
|
||||
return this.title.trim().length === 0 || !this.selectedWorkItemType;
|
||||
},
|
||||
createErrorText() {
|
||||
const workItemType = this.workItemTypes.find(
|
||||
(item) => item.value === this.selectedWorkItemType,
|
||||
)?.text;
|
||||
|
||||
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async createWorkItem() {
|
||||
|
@ -128,7 +133,7 @@ export default {
|
|||
} = response;
|
||||
this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
|
||||
} catch {
|
||||
this.error = this.$options.createErrorText;
|
||||
this.error = this.createErrorText;
|
||||
}
|
||||
},
|
||||
async createWorkItemFromTask() {
|
||||
|
@ -150,7 +155,7 @@ export default {
|
|||
});
|
||||
this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
|
||||
} catch {
|
||||
this.error = this.$options.createErrorText;
|
||||
this.error = this.createErrorText;
|
||||
}
|
||||
},
|
||||
handleTitleInput(title) {
|
||||
|
|
|
@ -3,9 +3,13 @@ import { GlAlert } from '@gitlab/ui';
|
|||
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import WorkItemDetail from '../components/work_item_detail.vue';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_DELETING,
|
||||
I18N_WORK_ITEM_DELETED,
|
||||
} from '../constants';
|
||||
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
@ -34,7 +38,7 @@ export default {
|
|||
this.ZenMode = new ZenMode();
|
||||
},
|
||||
methods: {
|
||||
deleteWorkItem() {
|
||||
deleteWorkItem(workItemType) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteWorkItemMutation,
|
||||
|
@ -53,13 +57,12 @@ export default {
|
|||
throw new Error(workItemDelete.errors[0]);
|
||||
}
|
||||
|
||||
this.$toast.show(s__('WorkItem|Work item deleted'));
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_DELETED, workItemType);
|
||||
this.$toast.show(msg);
|
||||
visitUrl(this.issuesListPath);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.error =
|
||||
e.message ||
|
||||
s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
|
||||
this.error = e.message || sprintfWorkItem(I18N_WORK_ITEM_ERROR_DELETING, workItemType);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -69,6 +72,6 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
|
||||
<work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" />
|
||||
<work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem($event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
= s_("Profiles|This email will be displayed on your public profile.")
|
||||
|
||||
.form-group.gl-form-group
|
||||
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank')
|
||||
- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
|
||||
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits')
|
||||
- commit_email_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: commit_email_link_url }
|
||||
- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
|
||||
= form.label :commit_email, s_('Profiles|Commit email')
|
||||
.gl-md-form-input-lg
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleBackfillClusterAgentsHasVulnerabilities < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
MIGRATION = 'BackfillClusterAgentsHasVulnerabilities'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillVulnerabilityReadsClusterAgent',
|
||||
table_name: :vulnerability_reads,
|
||||
column_name: :id,
|
||||
job_arguments: []
|
||||
)
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:cluster_agents,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :cluster_agents, :id, [])
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220615154500
Normal file
1
db/schema_migrations/20220615154500
Normal file
|
@ -0,0 +1 @@
|
|||
fd138239f6970b892fdb8190fb65b3364bb9ba5396100ba3d5d695eef6436dcf
|
|
@ -46,7 +46,8 @@ Read more about [author responsibilities](#the-responsibility-of-the-merge-reque
|
|||
|
||||
Domain experts are team members who have substantial experience with a specific technology,
|
||||
product feature, or area of the codebase. Team members are encouraged to self-identify as
|
||||
domain experts and add it to their [team profiles](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team_members/person/README.md).
|
||||
domain experts and add it to their
|
||||
[team profiles](https://about.gitlab.com/handbook/engineering/workflow/code-review/#how-to-self-identify-as-a-domain-expert).
|
||||
|
||||
When self-identifying as a domain expert, it is recommended to assign the MR changing the `.yml` file to be merged by an already established Domain Expert or a corresponding Engineering Manager.
|
||||
|
||||
|
|
|
@ -124,17 +124,16 @@ This table shows available scopes per token. Scopes can be limited further on to
|
|||
|
||||
## Security considerations
|
||||
|
||||
Access tokens should be treated like passwords and kept secure.
|
||||
|
||||
Adding them to URLs is a security risk. This is especially true when cloning or adding a remote, as Git then writes the URL to its `.git/config` file in plain text. URLs are also generally logged by proxies and application servers, which makes those credentials visible to system administrators.
|
||||
|
||||
Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
|
||||
|
||||
Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
|
||||
|
||||
Tokens should not be committed to your source code. Instead, consider an approach such as [using external secrets in CI](../ci/secrets/index.md).
|
||||
|
||||
When creating a scoped token, consider using the most limited scope possible to reduce the impact of accidentally leaking the token.
|
||||
|
||||
When creating a token, consider setting a token that expires when your task is complete. For example, if performing a one-off import, set the
|
||||
token to expire after a few hours or a day. This reduces the impact of a token that is accidentally leaked because it is useless when it expires.
|
||||
- Access tokens should be treated like passwords and kept secure.
|
||||
- Adding access tokens to URLs is a security risk, especially when cloning or adding a remote because Git then writes the URL to its `.git/config` file in plain text. URLs are
|
||||
also generally logged by proxies and application servers, which makes those credentials visible to system administrators. Instead, pass API calls an access token using
|
||||
headers like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
|
||||
- Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
|
||||
- Tokens should not be committed to your source code. Instead, consider an approach such as [using external secrets in CI](../ci/secrets/index.md).
|
||||
- When creating a scoped token, consider using the most limited scope possible to reduce the impact of accidentally leaking the token.
|
||||
- When creating a token, consider setting a token that expires when your task is complete. For example, if performing a one-off import, set the
|
||||
token to expire after a few hours or a day. This reduces the impact of a token that is accidentally leaked because it is useless when it expires.
|
||||
- Be careful not to include tokens when pasting code, console commands, or log outputs into an issue or MR description or comment.
|
||||
- Don’t log credentials in the console logs. Consider [protecting](../ci/variables/index.md#protected-cicd-variables) and
|
||||
[masking](../ci/variables/index.md#mask-a-cicd-variable) your credentials.
|
||||
- Review all currently active access tokens of all types on a regular basis and revoke any that are no longer needed.
|
||||
|
|
|
@ -29,14 +29,40 @@ For more details about the agent's purpose and architecture, see the [architectu
|
|||
|
||||
## Workflows
|
||||
|
||||
You can choose from two primary workflows.
|
||||
You can choose from two primary workflows. The GitOps workflow is recommended.
|
||||
|
||||
In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and
|
||||
any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based,
|
||||
### GitOps workflow
|
||||
|
||||
In a [**GitOps** workflow](gitops.md):
|
||||
|
||||
- You keep your Kubernetes manifests in GitLab.
|
||||
- You install a GitLab agent in your cluster.
|
||||
- Any time you update your manifests, the agent updates the cluster.
|
||||
- The cluster automatically cleans up unexpected changes. It uses
|
||||
[server-side applies](https://kubernetes.io/docs/reference/using-api/server-side-apply/)
|
||||
to fix any configuration inconsistencies that third parties introduce.
|
||||
|
||||
This workflow is fully driven with Git and is considered **pull-based**,
|
||||
because the cluster is pulling updates from your GitLab repository.
|
||||
|
||||
In a [**CI/CD** workflow](ci_cd_workflow.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API.
|
||||
This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster.
|
||||
GitLab recommends this workflow. We are actively investing in this workflow
|
||||
so we can provide a first-class experience.
|
||||
|
||||
### GitLab CI/CD workflow
|
||||
|
||||
In a [**CI/CD** workflow](ci_cd_workflow.md):
|
||||
|
||||
- You configure GitLab CI/CD to use the Kubernetes API to query and update your cluster.
|
||||
|
||||
This workflow is considered **push-based**, because GitLab is pushing requests
|
||||
from GitLab CI/CD to your cluster.
|
||||
|
||||
Use this workflow:
|
||||
|
||||
- When you have a heavily pipeline-oriented processes.
|
||||
- When you need to migrate to the agent but the GitOps workflow cannot support the use case you need.
|
||||
|
||||
This workflow has a weaker security model and is not recommended for production deployments.
|
||||
|
||||
## Supported cluster versions
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Backfills the `vulnerability_reads.casted_cluster_agent_id` column
|
||||
class BackfillClusterAgentsHasVulnerabilities < Gitlab::BackgroundMigration::BatchedMigrationJob
|
||||
VULNERABILITY_READS_JOIN = <<~SQL
|
||||
INNER JOIN vulnerability_reads
|
||||
ON vulnerability_reads.casted_cluster_agent_id = cluster_agents.id AND
|
||||
vulnerability_reads.project_id = cluster_agents.project_id AND
|
||||
vulnerability_reads.report_type = 7
|
||||
SQL
|
||||
|
||||
RELATION = ->(relation) do
|
||||
relation
|
||||
.where(has_vulnerabilities: false)
|
||||
end
|
||||
|
||||
def perform
|
||||
each_sub_batch(
|
||||
operation_name: :update_all,
|
||||
batching_scope: RELATION
|
||||
) do |sub_batch|
|
||||
sub_batch
|
||||
.joins(VULNERABILITY_READS_JOIN)
|
||||
.update_all(has_vulnerabilities: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,7 @@ module Gitlab
|
|||
ProjectFeature.connection.execute(
|
||||
<<~SQL
|
||||
UPDATE project_features pf
|
||||
SET package_registry_access_level = (CASE p.packages_enabled
|
||||
SET package_registry_access_level = (CASE p.packages_enabled
|
||||
WHEN true THEN (CASE p.visibility_level
|
||||
WHEN #{PROJECT_PUBLIC} THEN #{FEATURE_PUBLIC}
|
||||
WHEN #{PROJECT_INTERNAL} THEN #{FEATURE_ENABLED}
|
||||
|
|
|
@ -44420,6 +44420,9 @@ msgstr ""
|
|||
msgid "Work in progress Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|%{workItemType} deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44438,7 +44441,7 @@ msgstr ""
|
|||
msgid "WorkItem|Are you sure you want to cancel editing?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to delete the task? This action cannot be reversed."
|
||||
msgid "WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Assignee"
|
||||
|
@ -44464,7 +44467,7 @@ msgstr ""
|
|||
msgid "WorkItem|Create work item"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Delete task"
|
||||
msgid "WorkItem|Delete %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Expand tasks"
|
||||
|
@ -44503,18 +44506,15 @@ msgstr ""
|
|||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when creating a task. Please try again"
|
||||
msgid "WorkItem|Something went wrong when creating %{workItemType}. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
|
||||
msgid "WorkItem|Something went wrong when deleting the %{workItemType}. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when deleting the task. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when deleting the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching tasks. Please refresh this page."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44530,6 +44530,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while updating the %{workItemType}. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44551,9 +44554,6 @@ msgstr ""
|
|||
msgid "WorkItem|Turn on confidentiality"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Undo"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44563,7 +44563,7 @@ msgstr ""
|
|||
msgid "WorkItem|Work Items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Work item deleted"
|
||||
msgid "WorkItem|Work item"
|
||||
msgstr ""
|
||||
|
||||
msgid "Would you like to create a new branch?"
|
||||
|
|
|
@ -12,6 +12,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import createFlash from '~/flash';
|
||||
import Description from '~/issues/show/components/description.vue';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
|
@ -71,7 +72,11 @@ describe('Description component', () => {
|
|||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
|
||||
|
||||
function createComponent({ props = {}, provide } = {}) {
|
||||
function createComponent({
|
||||
props = {},
|
||||
provide,
|
||||
createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
|
||||
} = {}) {
|
||||
wrapper = shallowMountExtended(Description, {
|
||||
propsData: {
|
||||
issueId: 1,
|
||||
|
@ -85,7 +90,7 @@ describe('Description component', () => {
|
|||
apolloProvider: createMockApollo([
|
||||
[workItemQuery, queryHandler],
|
||||
[workItemTypesQuery, workItemTypesQueryHandler],
|
||||
[createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler],
|
||||
[createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
|
||||
]),
|
||||
mocks: {
|
||||
$toast,
|
||||
|
@ -317,7 +322,28 @@ describe('Description component', () => {
|
|||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows toast after delete success', async () => {
|
||||
const newDesc = 'description';
|
||||
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
|
||||
|
||||
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
|
||||
expect($toast.show).toHaveBeenCalledWith('Task deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating work item from checklist item', () => {
|
||||
it('emits `updateDescription` after creating new work item', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
descriptionHtml: descriptionHtmlWithCheckboxes,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
workItemsCreateFromMarkdown: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newDescription = `<p>New description</p>`;
|
||||
|
||||
await findConvertToTaskButton().trigger('click');
|
||||
|
@ -327,12 +353,28 @@ describe('Description component', () => {
|
|||
expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
|
||||
});
|
||||
|
||||
it('shows toast after delete success', async () => {
|
||||
const newDesc = 'description';
|
||||
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
|
||||
it('shows flash message when creating task fails', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
descriptionHtml: descriptionHtmlWithCheckboxes,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
workItemsCreateFromMarkdown: true,
|
||||
},
|
||||
},
|
||||
createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
|
||||
});
|
||||
|
||||
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
|
||||
expect($toast.show).toHaveBeenCalledWith('Task deleted');
|
||||
await findConvertToTaskButton().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Something went wrong when creating task. Please try again.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { GlDropdownDivider, GlModal } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
|
||||
|
||||
const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
|
||||
const TEST_ID_DELETE_ACTION = 'delete-action';
|
||||
|
||||
describe('WorkItemActions component', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findConfidentialityToggleButton = () =>
|
||||
wrapper.findByTestId('confidentiality-toggle-action');
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete-action');
|
||||
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
|
||||
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
|
||||
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
|
||||
const findDropdownItemsActual = () =>
|
||||
findDropdownItems().wrappers.map((x) => {
|
||||
if (x.is(GlDropdownDivider)) {
|
||||
return { divider: true };
|
||||
}
|
||||
|
||||
return {
|
||||
testId: x.attributes('data-testid'),
|
||||
text: x.text(),
|
||||
};
|
||||
});
|
||||
|
||||
const createComponent = ({
|
||||
canUpdate = true,
|
||||
|
@ -19,7 +34,14 @@ describe('WorkItemActions component', () => {
|
|||
} = {}) => {
|
||||
glModalDirective = jest.fn();
|
||||
wrapper = shallowMountExtended(WorkItemActions, {
|
||||
propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
|
||||
propsData: {
|
||||
workItemId: '123',
|
||||
canUpdate,
|
||||
canDelete,
|
||||
isConfidential,
|
||||
isParentConfidential,
|
||||
workItemType: 'Task',
|
||||
},
|
||||
directives: {
|
||||
glModal: {
|
||||
bind(_, { value }) {
|
||||
|
@ -44,8 +66,19 @@ describe('WorkItemActions component', () => {
|
|||
it('renders dropdown actions', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findConfidentialityToggleButton().exists()).toBe(true);
|
||||
expect(findDeleteButton().exists()).toBe(true);
|
||||
expect(findDropdownItemsActual()).toEqual([
|
||||
{
|
||||
testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
|
||||
text: 'Turn on confidentiality',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_DELETE_ACTION,
|
||||
text: 'Delete task',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('toggle confidentiality action', () => {
|
||||
|
@ -103,7 +136,8 @@ describe('WorkItemActions component', () => {
|
|||
canDelete: false,
|
||||
});
|
||||
|
||||
expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
|
||||
expect(findDeleteButton().exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import ItemState from '~/work_items/components/item_state.vue';
|
||||
import WorkItemState from '~/work_items/components/work_item_state.vue';
|
||||
import {
|
||||
i18n,
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
STATE_EVENT_CLOSE,
|
||||
|
@ -104,7 +103,9 @@ describe('WorkItemState component', () => {
|
|||
findItemState().vm.$emit('changed', STATE_CLOSED);
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('tracks editing the state', async () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ItemTitle from '~/work_items/components/item_title.vue';
|
||||
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
|
||||
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
|
||||
|
@ -116,7 +116,9 @@ describe('WorkItemTitle component', () => {
|
|||
findItemTitle().vm.$emit('title-changed', 'new title');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('tracks editing the title', async () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { __ } from '~/locale';
|
||||
import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
|
||||
|
||||
|
@ -181,7 +181,9 @@ describe('WorkItemWeight component', () => {
|
|||
findInput().trigger('blur');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits an error when there is a network error', async () => {
|
||||
|
@ -194,7 +196,9 @@ describe('WorkItemWeight component', () => {
|
|||
findInput().trigger('blur');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('tracks updating the weight', () => {
|
||||
|
|
|
@ -193,6 +193,8 @@ describe('Create work item component', () => {
|
|||
wrapper.find('form').trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().text()).toBe(CreateWorkItem.createErrorText);
|
||||
expect(findAlert().text()).toBe(
|
||||
'Something went wrong when creating work item. Please try again.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -379,11 +379,12 @@ describe('WorkItemDetail component', () => {
|
|||
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
const updateError = 'Failed to update';
|
||||
|
||||
findWorkItemTitle().vm.$emit('error', i18n.updateError);
|
||||
findWorkItemTitle().vm.$emit('error', updateError);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().text()).toBe(i18n.updateError);
|
||||
expect(findAlert().text()).toBe(updateError);
|
||||
});
|
||||
|
||||
it('calls the subscription', () => {
|
||||
|
@ -456,11 +457,12 @@ describe('WorkItemDetail component', () => {
|
|||
it('shows an error message when it emits an `error` event', async () => {
|
||||
createComponent({ workItemsMvc2Enabled: true });
|
||||
await waitForPromises();
|
||||
const updateError = 'Failed to update';
|
||||
|
||||
findWorkItemWeight().vm.$emit('error', i18n.updateError);
|
||||
findWorkItemWeight().vm.$emit('error', updateError);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().text()).toBe(i18n.updateError);
|
||||
expect(findAlert().text()).toBe(updateError);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration do # rubocop:disable Layout/LineLength
|
||||
let(:migration) do
|
||||
described_class.new(start_id: 1, end_id: 10,
|
||||
batch_table: table_name, batch_column: batch_column,
|
||||
sub_batch_size: sub_batch_size, pause_ms: pause_ms,
|
||||
connection: ApplicationRecord.connection)
|
||||
end
|
||||
|
||||
let(:users_table) { table(:users) }
|
||||
let(:vulnerability_reads_table) { table(:vulnerability_reads) }
|
||||
let(:vulnerability_scanners_table) { table(:vulnerability_scanners) }
|
||||
let(:vulnerabilities_table) { table(:vulnerabilities) }
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:cluster_agents_table) { table(:cluster_agents) }
|
||||
|
||||
let(:table_name) { 'cluster_agents' }
|
||||
let(:batch_column) { :id }
|
||||
let(:sub_batch_size) { 100 }
|
||||
let(:pause_ms) { 0 }
|
||||
|
||||
subject(:perform_migration) { migration.perform }
|
||||
|
||||
before do
|
||||
users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5)
|
||||
|
||||
namespaces_table.create!(id: 1, name: 'Namespace 1', path: 'namespace-1')
|
||||
namespaces_table.create!(id: 2, name: 'Namespace 2', path: 'namespace-2')
|
||||
namespaces_table.create!(id: 3, name: 'Namespace 3', path: 'namespace-3')
|
||||
|
||||
projects_table.create!(id: 1, namespace_id: 1, name: 'Project 1', path: 'project-1', project_namespace_id: 1)
|
||||
projects_table.create!(id: 2, namespace_id: 2, name: 'Project 2', path: 'project-2', project_namespace_id: 2)
|
||||
projects_table.create!(id: 3, namespace_id: 2, name: 'Project 3', path: 'project-3', project_namespace_id: 3)
|
||||
|
||||
cluster_agents_table.create!(id: 1, name: 'Agent 1', project_id: 1)
|
||||
cluster_agents_table.create!(id: 2, name: 'Agent 2', project_id: 2)
|
||||
cluster_agents_table.create!(id: 3, name: 'Agent 3', project_id: 1)
|
||||
cluster_agents_table.create!(id: 4, name: 'Agent 4', project_id: 1)
|
||||
cluster_agents_table.create!(id: 5, name: 'Agent 5', project_id: 1)
|
||||
cluster_agents_table.create!(id: 6, name: 'Agent 6', project_id: 1)
|
||||
cluster_agents_table.create!(id: 7, name: 'Agent 7', project_id: 3)
|
||||
cluster_agents_table.create!(id: 8, name: 'Agent 8', project_id: 1)
|
||||
cluster_agents_table.create!(id: 9, name: 'Agent 9', project_id: 1)
|
||||
cluster_agents_table.create!(id: 10, name: 'Agent 10', project_id: 3)
|
||||
cluster_agents_table.create!(id: 11, name: 'Agent 11', project_id: 1)
|
||||
|
||||
vulnerability_scanners_table.create!(id: 1, project_id: 1, external_id: 'starboard', name: 'Starboard')
|
||||
vulnerability_scanners_table.create!(id: 2, project_id: 2, external_id: 'starboard', name: 'Starboard')
|
||||
vulnerability_scanners_table.create!(id: 3, project_id: 3, external_id: 'starboard', name: 'Starboard')
|
||||
|
||||
add_vulnerability_read!(1, project_id: 1, cluster_agent_id: 1, report_type: 7)
|
||||
add_vulnerability_read!(2, project_id: 1, cluster_agent_id: nil, report_type: 7)
|
||||
add_vulnerability_read!(3, project_id: 1, cluster_agent_id: 3, report_type: 7)
|
||||
add_vulnerability_read!(4, project_id: 1, cluster_agent_id: nil, report_type: 7)
|
||||
add_vulnerability_read!(5, project_id: 2, cluster_agent_id: 5, report_type: 5)
|
||||
add_vulnerability_read!(7, project_id: 2, cluster_agent_id: 7, report_type: 7)
|
||||
add_vulnerability_read!(9, project_id: 3, cluster_agent_id: 9, report_type: 7)
|
||||
add_vulnerability_read!(10, project_id: 1, cluster_agent_id: 10, report_type: 7)
|
||||
add_vulnerability_read!(11, project_id: 2, cluster_agent_id: 11, report_type: 7)
|
||||
end
|
||||
|
||||
it 'backfills `has_vulnerabilities` for the selected records', :aggregate_failures do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
perform_migration
|
||||
end
|
||||
|
||||
expect(queries.count).to eq(3)
|
||||
expect(cluster_agents_table.where(has_vulnerabilities: true).count).to eq 2
|
||||
expect(cluster_agents_table.where(has_vulnerabilities: true).pluck(:id)).to match_array([1, 3])
|
||||
end
|
||||
|
||||
it 'tracks timings of queries' do
|
||||
expect(migration.batch_metrics.timings).to be_empty
|
||||
|
||||
expect { perform_migration }.to change { migration.batch_metrics.timings }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:)
|
||||
vulnerabilities_table.create!(
|
||||
id: id,
|
||||
project_id: project_id,
|
||||
author_id: 1,
|
||||
title: "Vulnerability #{id}",
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: report_type
|
||||
)
|
||||
|
||||
vulnerability_reads_table.create!(
|
||||
id: id,
|
||||
uuid: SecureRandom.uuid,
|
||||
severity: 5,
|
||||
state: 1,
|
||||
vulnerability_id: id,
|
||||
scanner_id: project_id,
|
||||
casted_cluster_agent_id: cluster_agent_id,
|
||||
project_id: project_id,
|
||||
report_type: report_type
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleBackfillClusterAgentsHasVulnerabilities do
|
||||
let_it_be(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules background jobs for each batch of cluster agents' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :cluster_agents,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,11 @@ RSpec.describe 'profiles/show' do
|
|||
|
||||
expect(rendered).to have_field('user_name', with: user.name)
|
||||
expect(rendered).to have_field('user_id', with: user.id)
|
||||
|
||||
expectd_link = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits')
|
||||
expected_link_html = "<a href=\"#{expectd_link}\" target=\"_blank\" " \
|
||||
"rel=\"noopener noreferrer\">#{_('Learn more.')}</a>"
|
||||
expect(rendered.include?(expected_link_html)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue