Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-02-26 15:12:07 +00:00
parent 4e98c75b8c
commit 8b2a413032
49 changed files with 950 additions and 247 deletions

View File

@ -1,12 +0,0 @@
---
# Cop supports --autocorrect.
Lint/NonAtomicFileOperation:
Exclude:
- 'lib/gitlab/ci/trace.rb'
- 'lib/gitlab/database/migrations/test_batched_background_runner.rb'
- 'lib/gitlab/gpg.rb'
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
- 'lib/gitlab/import_export/recursive_merge_folders.rb'
- 'lib/gitlab/memory/upload_and_cleanup_reports.rb'
- 'lib/tasks/gitlab/update_templates.rake'
- 'lib/tasks/tanuki_emoji.rake'

View File

@ -87,7 +87,7 @@ export default {
},
dropzoneAllowList: ['.csv'],
docsLink: helpPagePath('user/project/import/_index', {
anchor: 'reassign-contributions-and-memberships',
anchor: 'request-reassignment-by-using-a-csv-file',
}),
i18n: {
description: s__(

View File

@ -1,5 +1,6 @@
<script>
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__, sprintf } from '~/locale';
import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql';
@ -116,6 +117,9 @@ export default {
this.$emit('hide');
},
},
compactionAlgorithmHelpPage: helpPagePath('ci/jobs/ci_job_token', {
anchor: 'auto-populate-a-projects-allowlist',
}),
};
</script>
@ -129,6 +133,7 @@ export default {
@primary.prevent="autopopulateAllowlist"
@secondary="hideModal"
@canceled="hideModal"
@hidden="hideModal"
>
<gl-alert v-if="errorMessage" variant="danger" class="gl-mb-3" :dismissible="false">
{{ errorMessage }}
@ -138,8 +143,6 @@ export default {
{{ authLogExceedsLimitMessage }}
</gl-alert>
<p data-testid="modal-description">
<!-- TODO: Update documentation link -->
<!-- See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181294 -->
<gl-sprintf
:message="
s__(
@ -148,7 +151,9 @@ export default {
"
>
<template #link="{ content }">
<gl-link href="/" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.compactionAlgorithmHelpPage" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>

View File

@ -3,6 +3,7 @@ import {
GlAlert,
GlButton,
GlCollapsibleListbox,
GlDisclosureDropdown,
GlIcon,
GlLink,
GlLoadingIcon,
@ -24,13 +25,16 @@ import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_jo
import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import getCiJobTokenScopeAllowlistQuery from '../graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
import getAuthLogCountQuery from '../graphql/queries/get_auth_log_count.query.graphql';
import removeAutopopulatedEntriesMutation from '../graphql/mutations/remove_autopopulated_entries.mutation.graphql';
import {
JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
JOB_TOKEN_REMOVE_AUTOPOPULATED_ENTRIES_MODAL,
} from '../constants';
import TokenAccessTable from './token_access_table.vue';
import NamespaceForm from './namespace_form.vue';
import AutopopulateAllowlistModal from './autopopulate_allowlist_modal.vue';
import RemoveAutopopulatedEntriesModal from './remove_autopopulated_entries_modal.vue';
export default {
i18n: {
@ -55,6 +59,7 @@ export default {
'CICD|Are you sure you want to remove %{namespace} from the job token allowlist?',
),
removeNamespaceModalActionText: s__('CICD|Remove group or project'),
removeAutopopulatedEntries: s__('CICD|Remove all auto-added allowlist entries'),
},
inboundJobTokenScopeOptions: [
{
@ -81,11 +86,13 @@ export default {
GlAlert,
GlButton,
GlCollapsibleListbox,
GlDisclosureDropdown,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
CrudComponent,
RemoveAutopopulatedEntriesModal,
TokenAccessTable,
GlFormRadioGroup,
NamespaceForm,
@ -170,9 +177,11 @@ export default {
data() {
return {
authLogCount: 0,
allowlistLoadingMessage: '',
inboundJobTokenScopeEnabled: null,
isUpdating: false,
isUpdatingJobTokenScope: false,
groupsAndProjectsWithAccess: { groups: [], projects: [] },
autopopulationErrorMessage: null,
projectName: '',
namespaceToEdit: null,
namespaceToRemove: null,
@ -183,6 +192,12 @@ export default {
authLogExceedsLimit() {
return this.projectCount + this.groupCount + this.authLogCount > this.projectAllowlistLimit;
},
isAllowlistLoading() {
return (
this.$apollo.queries.groupsAndProjectsWithAccess.loading ||
this.allowlistLoadingMessage.length > 0
);
},
isJobTokenPoliciesEnabled() {
return this.glFeatures.addPoliciesToCiJobToken;
},
@ -198,6 +213,17 @@ export default {
canAutopopulateAuthLog() {
return this.glFeatures.authenticationLogsMigrationForAllowlist;
},
disclosureDropdownOptions() {
return [
{
text: this.$options.i18n.removeAutopopulatedEntries,
variant: 'danger',
action: () => {
this.selectedAction = JOB_TOKEN_REMOVE_AUTOPOPULATED_ENTRIES_MODAL;
},
},
];
},
groupCount() {
return this.groupsAndProjectsWithAccess.groups.length;
},
@ -210,14 +236,14 @@ export default {
projectCountTooltip() {
return n__('%d project has access', '%d projects have access', this.projectCount);
},
isAllowlistLoading() {
return this.$apollo.queries.groupsAndProjectsWithAccess.loading;
},
removeNamespaceModalTitle() {
return sprintf(this.$options.i18n.removeNamespaceModalTitle, {
namespace: this.namespaceToRemove?.fullPath,
});
},
showRemoveAutopopulatedEntriesModal() {
return this.selectedAction === JOB_TOKEN_REMOVE_AUTOPOPULATED_ENTRIES_MODAL;
},
showAutopopulateModal() {
return this.selectedAction === JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG;
},
@ -238,7 +264,7 @@ export default {
}));
},
async updateCIJobTokenScope() {
this.isUpdating = true;
this.isUpdatingJobTokenScope = true;
try {
const {
@ -268,7 +294,7 @@ export default {
this.inboundJobTokenScopeEnabled = !this.inboundJobTokenScopeEnabled;
createAlert({ message: error.message });
} finally {
this.isUpdating = false;
this.isUpdatingJobTokenScope = false;
}
},
async removeItem() {
@ -291,6 +317,42 @@ export default {
this.refetchGroupsAndProjects();
return Promise.resolve();
},
async removeAutopopulatedEntries() {
this.hideSelectedAction();
this.autopopulationErrorMessage = null;
this.allowlistLoadingMessage = s__(
'CICD|Removing auto-added allowlist entries. Please wait while the action completes.',
);
try {
const {
data: {
ciJobTokenScopeClearAllowlistAutopopulations: { errors },
},
} = await this.$apollo.mutate({
mutation: removeAutopopulatedEntriesMutation,
variables: {
projectPath: this.fullPath,
},
});
if (errors.length) {
this.autopopulationErrorMessage = errors[0].message;
return;
}
this.refetchAllowlist();
this.$toast.show(
s__('CICD|Authentication log entries were successfully removed from the allowlist.'),
);
} catch (error) {
this.autopopulationErrorMessage = s__(
'CICD|An error occurred while removing the auto-added log entries. Please try again.',
);
} finally {
this.allowlistLoadingMessage = '';
}
},
refetchAllowlist() {
this.$apollo.queries.groupsAndProjectsWithAccess.refetch();
this.hideSelectedAction();
@ -328,117 +390,133 @@ export default {
@hide="hideSelectedAction"
@refetch-allowlist="refetchAllowlist"
/>
<gl-loading-icon v-if="$apollo.queries.inboundJobTokenScopeEnabled.loading" size="md" />
<template v-else>
<div class="gl-font-bold">
{{ $options.i18n.radioGroupTitle }}
</div>
<div class="gl-mb-3">
<gl-sprintf :message="$options.i18n.radioGroupDescription">
<template #link="{ content }">
<gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</div>
<gl-form-radio-group
v-if="!enforceAllowlist"
v-model="inboundJobTokenScopeEnabled"
:options="$options.inboundJobTokenScopeOptions"
stacked
/>
<gl-alert
v-if="!inboundJobTokenScopeEnabled && !enforceAllowlist"
variant="warning"
class="gl-my-3"
:dismissible="false"
:show-icon="false"
>
{{ $options.i18n.settingDisabledMessage }}
</gl-alert>
<gl-button
v-if="!enforceAllowlist"
variant="confirm"
class="gl-mt-3"
data-testid="save-ci-job-token-scope-changes-btn"
:loading="isUpdating"
@click="updateCIJobTokenScope"
>
{{ $options.i18n.saveButtonTitle }}
</gl-button>
<crud-component
:title="$options.i18n.cardHeaderTitle"
:description="$options.i18n.cardHeaderDescription"
:toggle-text="!canAutopopulateAuthLog ? $options.i18n.addGroupOrProject : undefined"
class="gl-mt-5"
@hideForm="hideSelectedAction"
>
<template v-if="canAutopopulateAuthLog" #actions="{ showForm }">
<gl-collapsible-listbox
v-model="selectedAction"
:items="$options.crudFormActions"
:toggle-text="$options.i18n.add"
data-testid="form-selector"
size="small"
@select="selectAction($event, showForm)"
/>
<remove-autopopulated-entries-modal
:show-modal="showRemoveAutopopulatedEntriesModal"
@hide="hideSelectedAction"
@remove-entries="removeAutopopulatedEntries"
/>
<div class="gl-font-bold">
{{ $options.i18n.radioGroupTitle }}
</div>
<div class="gl-mb-3">
<gl-sprintf :message="$options.i18n.radioGroupDescription">
<template #link="{ content }">
<gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">{{
content
}}</gl-link>
</template>
<template #count>
<gl-loading-icon v-if="isAllowlistLoading" data-testid="count-loading-icon" />
<template v-else>
<span
v-gl-tooltip.d0="groupCountTooltip"
class="gl-cursor-default"
data-testid="group-count"
>
<gl-icon name="group" /> {{ groupCount }}
</span>
<span
v-gl-tooltip.d0="projectCountTooltip"
class="gl-ml-2 gl-cursor-default"
data-testid="project-count"
>
<gl-icon name="project" /> {{ projectCount }}
</span>
</template>
</template>
<template #form="{ hideForm }">
<namespace-form
:namespace="namespaceToEdit"
@saved="refetchGroupsAndProjects"
@close="hideForm"
/>
</template>
<template #default="{ showForm }">
<token-access-table
:items="allowlist"
:loading="isAllowlistLoading"
:show-policies="isJobTokenPoliciesEnabled"
@editItem="showNamespaceForm($event, showForm)"
@removeItem="namespaceToRemove = $event"
/>
<confirm-action-modal
v-if="namespaceToRemove"
modal-id="inbound-token-access-remove-confirm-modal"
:title="removeNamespaceModalTitle"
:action-fn="removeItem"
:action-text="$options.i18n.removeNamespaceModalActionText"
@close="namespaceToRemove = null"
</gl-sprintf>
</div>
<gl-form-radio-group
v-if="!enforceAllowlist"
v-model="inboundJobTokenScopeEnabled"
:options="$options.inboundJobTokenScopeOptions"
stacked
/>
<gl-alert
v-if="!inboundJobTokenScopeEnabled && !enforceAllowlist"
variant="warning"
class="gl-my-3"
:dismissible="false"
:show-icon="false"
>
{{ $options.i18n.settingDisabledMessage }}
</gl-alert>
<gl-button
v-if="!enforceAllowlist"
variant="confirm"
class="gl-mt-3"
data-testid="save-ci-job-token-scope-changes-btn"
:loading="isUpdatingJobTokenScope"
@click="updateCIJobTokenScope"
>
{{ $options.i18n.saveButtonTitle }}
</gl-button>
<gl-alert
v-if="autopopulationErrorMessage"
variant="danger"
class="gl-my-5"
:dismissible="false"
data-testid="autopopulation-alert"
>
{{ autopopulationErrorMessage }}
</gl-alert>
<crud-component
:title="$options.i18n.cardHeaderTitle"
:description="$options.i18n.cardHeaderDescription"
:toggle-text="!canAutopopulateAuthLog ? $options.i18n.addGroupOrProject : undefined"
class="gl-mt-5"
@hideForm="hideSelectedAction"
>
<template v-if="canAutopopulateAuthLog" #actions="{ showForm }">
<gl-collapsible-listbox
v-model="selectedAction"
:items="$options.crudFormActions"
:toggle-text="$options.i18n.add"
data-testid="form-selector"
size="small"
@select="selectAction($event, showForm)"
/>
<gl-disclosure-dropdown
category="tertiary"
icon="ellipsis_v"
no-caret
:items="disclosureDropdownOptions"
/>
</template>
<template #count>
<gl-loading-icon v-if="isAllowlistLoading" data-testid="count-loading-icon" />
<template v-else>
<span
v-gl-tooltip.d0="groupCountTooltip"
class="gl-cursor-default"
data-testid="group-count"
>
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
<template #namespace>
<code>{{ namespaceToRemove.fullPath }}</code>
</template>
</gl-sprintf>
</confirm-action-modal>
<gl-icon name="group" /> {{ groupCount }}
</span>
<span
v-gl-tooltip.d0="projectCountTooltip"
class="gl-ml-2 gl-cursor-default"
data-testid="project-count"
>
<gl-icon name="project" /> {{ projectCount }}
</span>
</template>
</crud-component>
</template>
</template>
<template #form="{ hideForm }">
<namespace-form
:namespace="namespaceToEdit"
@saved="refetchGroupsAndProjects"
@close="hideForm"
/>
</template>
<template #default="{ showForm }">
<token-access-table
:items="allowlist"
:loading="isAllowlistLoading"
:loading-message="allowlistLoadingMessage"
:show-policies="isJobTokenPoliciesEnabled"
@editItem="showNamespaceForm($event, showForm)"
@removeItem="namespaceToRemove = $event"
/>
<confirm-action-modal
v-if="namespaceToRemove"
modal-id="inbound-token-access-remove-confirm-modal"
:title="removeNamespaceModalTitle"
:action-fn="removeItem"
:action-text="$options.i18n.removeNamespaceModalActionText"
@close="namespaceToRemove = null"
>
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
<template #namespace>
<code>{{ namespaceToRemove.fullPath }}</code>
</template>
</gl-sprintf>
</confirm-action-modal>
</template>
</crud-component>
</div>
</template>

View File

@ -0,0 +1,72 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
name: 'RemoveAutopopulatedEntriesModal',
components: {
GlModal,
},
inject: ['fullPath'],
props: {
showModal: {
type: Boolean,
required: true,
},
},
apollo: {},
computed: {
modalOptions() {
return {
actionPrimary: {
text: __('Remove entries'),
attributes: {
variant: 'danger',
},
},
actionSecondary: {
text: __('Cancel'),
attributes: {
variant: 'default',
},
},
};
},
},
methods: {
hideModal() {
this.$emit('hide');
},
removeEntries() {
this.$emit('remove-entries');
},
},
};
</script>
<template>
<gl-modal
modal-id="remove-autopopulated-allowlist-entries-modal"
:visible="showModal"
:title="s__('CICD|Remove all auto-added allowlist entries')"
:action-primary="modalOptions.actionPrimary"
:action-secondary="modalOptions.actionSecondary"
@primary.prevent="removeEntries"
@secondary="hideModal"
@canceled="hideModal"
@hidden="hideModal"
>
<p>
{{
s__(
'CICD|This action removes all groups and projects that were auto-added from the authentication log.',
)
}}
</p>
<p>
{{
s__('CICD|Removing these entries could cause authentication failures or disrupt pipelines.')
}}
</p>
</gl-modal>
</template>

View File

@ -40,6 +40,11 @@ export default {
required: false,
default: false,
},
loadingMessage: {
type: String,
required: false,
default: '',
},
// This can be removed after outbound_token_access.vue is removed, which is a deprecated feature. We need to hide
// policies for that component, but show them on inbound_token_access.vue.
showPolicies: {
@ -95,6 +100,13 @@ export default {
<gl-table :items="items" :fields="fields" :busy="loading" class="gl-mb-0" stacked="md">
<template #table-busy>
<gl-loading-icon size="md" />
<p
v-if="loadingMessage.length > 0"
class="gl-mt-5 gl-text-center"
data-testid="loading-message"
>
{{ loadingMessage }}
</p>
</template>
<template #cell(fullPath)="{ item }">
<div class="gl-inline-flex gl-items-center">

View File

@ -148,3 +148,5 @@ export const JOB_TOKEN_POLICIES = keyBy(
export const JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT = 'JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT';
export const JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG = 'JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG';
export const JOB_TOKEN_REMOVE_AUTOPOPULATED_ENTRIES_MODAL =
'JOB_TOKEN_REMOVE_AUTOPOPULATED_ENTRIES_MODAL';

View File

@ -0,0 +1,6 @@
mutation CiJobTokenScopeClearAllowlistAutopopulations($projectPath: ID!) {
ciJobTokenScopeClearAllowlistAutopopulations(input: { projectPath: $projectPath }) {
status
errors
}
}

View File

@ -65,6 +65,7 @@ query getWorkItems(
webPath
}
closedAt
userDiscussionsCount
confidential
createdAt
iid
@ -126,6 +127,7 @@ query getWorkItems(
webPath
}
closedAt
userDiscussionsCount
confidential
createdAt
iid

View File

@ -386,3 +386,7 @@ input[type='search'] {
}
}
/* stylelint-enable property-no-vendor-prefix */
.description.term p:last-child {
@apply gl-m-0;
}

View File

@ -3,11 +3,11 @@
- position = index + 1
.search-result-row
= link_to project_milestone_path(milestone.project, milestone), class: 'gl-font-bold gl-text-default', data: { event_tracking: 'click_search_result', event_label: @scope, event_value: position } do
= link_to project_milestone_path(milestone.project, milestone), class: 'gl-font-bold', data: { event_tracking: 'click_search_result', event_label: @scope, event_value: position } do
%span.term.str-truncated= simple_search_highlight_and_truncate(milestone.title, @search_term)
- if milestone.project_milestone?
.gl-mt-2= gl_badge_tag milestone.project.full_name, { variant: :muted }, { class: 'gl-whitespace-normal gl-text-left' }
.gl-mt-2.gl-text-subtle.gl-whitespace-normal.gl-text-left.gl-text-sm= milestone.project.full_name
- if milestone.description.present?
.description.term

View File

@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/513796
milestone: '17.9'
group: group::package registry
type: beta
default_enabled: false
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: use_typhoeus_elasticsearch_adapter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76879
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348607
milestone: '14.7'
type: development
group: group::global search
default_enabled: false

View File

@ -115,7 +115,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- Including [troubleshooting](../postgresql/replication_and_failover_troubleshooting.md)
`gitlab-ctl patroni check-leader` and PgBouncer errors.
- [Developer database documentation](../../development/feature_development.md#database-guides),
- [Developer database documentation](../../development/database/_index.md),
some of which is absolutely not for production use. Including:
- Understanding EXPLAIN plans.

View File

@ -944,3 +944,60 @@ The opposite configuration (`docker:24.0.5-dind` service and Docker Engine on th
19.06.x or older) works without problems. For the best strategy, you should to frequently test and update
job environment versions to the newest. This brings new features, improved security and - for this specific
case - makes the upgrade on the underlying Docker Engine on the runner's host transparent for the job.
### Error: `failed to verify certificate: x509: certificate signed by unknown authority`
This error can appear when Docker commands like `docker build` or `docker pull` are executed in a Docker-in-Docker
environment where custom or private certificates are used (for example, Zscaler certificates):
```plaintext
error pulling image configuration: download failed after attempts=6: tls: failed to verify certificate: x509: certificate signed by unknown authority
```
This error occurs because Docker commands in a Docker-in-Docker environment
use two separate containers:
- The **build container** runs the Docker client (`/usr/bin/docker`) and executes your job's script commands.
- The **service container** (often named `svc`) runs the Docker daemon that processes most Docker commands.
When your organization uses custom certificates, both containers need these certificates.
Without proper certificate configuration in both containers, Docker operations that connect to external
registries or services will fail with certificate errors.
To resolve this issue:
1. Store your root certificate as a [CI/CD variable](../variables/_index.md#define-a-cicd-variable-in-the-ui) named `CA_CERTIFICATE`.
The certificate should be in this format:
```plaintext
-----BEGIN CERTIFICATE-----
(certificate content)
-----END CERTIFICATE-----
```
1. Configure your pipeline to install the certificate in the service container before starting the Docker daemon. For example:
```yaml
image_build:
stage: build
image:
name: docker:19.03
variables:
DOCKER_HOST: tcp://localhost:2375
DOCKER_TLS_CERTDIR: ""
CA_CERTIFICATE: "$CA_CERTIFICATE"
services:
- name: docker:19.03-dind
command:
- /bin/sh
- -c
- |
echo "$CA_CERTIFICATE" > /usr/local/share/ca-certificates/custom-ca.crt && \
update-ca-certificates && \
dockerd-entrypoint.sh || exit
script:
- docker info
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
- docker build -t "${DOCKER_REGISTRY}/my-app:${CI_COMMIT_REF_NAME}" .
- docker push "${DOCKER_REGISTRY}/my-app:${CI_COMMIT_REF_NAME}"
```

View File

@ -0,0 +1,71 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Data Retention Guidelines for Feature Development
## Overview
Data retention is a critical aspect of feature development at GitLab. As we build and maintain features, we must consider the lifecycle of the data we collect and store. This document outlines the guidelines for incorporating data retention considerations into feature development from the outset.
## Why data retention matters
- **System performance**: Time-based data organization enables better query optimization and efficient data access patterns, leading to faster response times and improved system scalability.
- **Infrastructure cost**: Strategic storage management through data lifecycle policies reduces infrastructure costs for primary storage, backups, and disaster recovery systems.
- **Engineering efficiency**: Designing features with data retention in mind from the start makes development faster and more reliable by establishing clear data lifecycles, reducing technical debt and faster data migrations.
## Guidelines for feature development
### 1. Early planning
When designing new features, consider data retention requirements during the initial planning phase:
- Document the types of data being persisted. Is this user-facing data?
Is it generated internally to make processing more efficient?
Is it derived/cache data?
- Identify the business purpose and required retention period for each data type.
- Define the product justification and customer usage pattern of older data.
How do people interact with older data as opposed to newer data?
How does the value change over time?
- Consider regulatory requirements that might affect data retention (such as Personally Identifiable Information).
- Plan for data removal or archival mechanisms.
### 2. Design for data lifecycle
Features should be designed with the understanding that data is not permanent:
- Avoid assumptions about infinite data availability.
- Implement graceful handling of missing or archived data.
- Design user interfaces to clearly communicate data availability periods.
- Design data structures for longer-term storage that is optimized to be viewed in a longer-term context.
- Consider implementing "time to live" (TTL) mechanisms where appropriate, especially for derived/cache data
that can be gracefully reproduced on-demand.
### 3. Documentation recommendations
Each feature implementation must include:
- Clear documentation of data retention periods (on GitLab.com and default values, if any)
and business reasoning/justification
- Description of data removal/archival mechanisms.
- Impact analysis of data removal on dependent features.
## Implementation checklist
Before submitting a merge request for a new feature:
- [ ] Document data retention requirements.
- [ ] Design data models with data removal in mind.
- [ ] Implement data removal/archival mechanisms.
- [ ] Test feature behavior with missing/archived data.
- [ ] Include retention periods in user documentation.
- [ ] Consider impact on dependent features.
- [ ] Consider impact on backups/restores and export/import.
- [ ] Consider impact on replication (eg Geo).
## Related links
- [Large tables limitations](../development/database/large_tables_limitations.md)

View File

@ -122,3 +122,4 @@ Disadvantages
- [Database size limits](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/database_size_limits/#solutions)
- [Adding database indexes](adding_database_indexes.md)
- [Database layout and access patterns](layout_and_access_patterns.md#data-model-trade-offs)
- [Data retention guidelines for feature development](../data_retention_policies.md)

View File

@ -137,9 +137,11 @@ The following integration guides are internal. Some integrations require access
- [JSON guidelines](json.md) for how to handle JSON in a performant manner.
- [GraphQL API optimizations](api_graphql_styleguide.md#optimizations) for how to optimize GraphQL code.
## Database guides
## Data stores guides
See [database guidelines](database/_index.md).
- [Database guidelines](database/_index.md).
- [Data retention policies](data_retention_policies.md)
- [Gitaly guidelines](gitaly.md)
## Testing guides

View File

@ -65,6 +65,21 @@ To enable [exact code search](../../user/search/exact_code_search.md) in GitLab:
1. Select the **Enable indexing for exact code search** and **Enable exact code search** checkboxes.
1. Select **Save changes**.
## Check indexing status
Prerequisites:
- You must have administrator access to the instance.
Indexing performance depends on the CPU and memory limits on the Zoekt indexer nodes.
To check indexing status, in the Rails console, run the following command:
```ruby
Search::Zoekt::Index.group(:state).count
Search::Zoekt::Repository.group(:state).count
Search::Zoekt::Task.group(:state).count
```
## Delete offline nodes automatically
Prerequisites:
@ -97,6 +112,13 @@ To index all root namespaces automatically:
1. Select the **Index root namespaces automatically** checkbox.
1. Select **Save changes**.
When you enable this setting, GitLab creates indexing tasks for all projects in:
- All groups and subgroups
- Any new root namespace
After a project is indexed, GitLab creates only incremental indexing when a repository change is detected.
When you disable this setting:
- Existing root namespaces remain indexed.

View File

@ -35,10 +35,12 @@ If there are any problems, you can:
Before migrating by using direct transfer, see the following prerequisites.
### Network
### Network and storage space
- The network connection between instances or GitLab.com must support HTTPS.
- Firewalls must not block the connection between the source and destination GitLab instances.
- The source and destination GitLab instances must have enough free space in the `/tmp` directory
to create and extract archives of transferred projects and groups.
### Versions

View File

@ -184,7 +184,7 @@ To disable diff previews for all projects in a group:
{{< history >}}
- Notifications to inherited group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/463016) in GitLab 17.7 [with a flag](../../administration/feature_flags.md) named `pat_expiry_inherited_members_notification`. Disabled by default.
- Notifications to inherited group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/463016) in GitLab 17.7 [with a flag](../../administration/feature_flags.md) named `extended_expiry_webhook_execution_setting`. Disabled by default.
{{< /history >}}

View File

@ -351,7 +351,7 @@ are then redirected to sign in through the identity provider.
GitLab.com uses the SAML **NameID** to identify users. The **NameID** is:
- A required field in the SAML response.
- Case sensitive.
- Case-insensitive.
The **NameID** must:

View File

@ -274,10 +274,13 @@ Users with the Owner role for a top-level group can reassign contributions and m
from placeholder users to existing active (non-bot) users.
On the destination instance, users with the Owner role for a top-level group can:
- Request users to accept reassignment of contributions and membership [in the UI](#request-reassignment-in-ui).
The reassignment process starts only after the selected user [accepts the reassignment request](#accept-contribution-reassignment),
which is sent to them by email.
- Choose not to reassign contributions and memberships, and [keep them with placeholder users](#keep-as-placeholder).
- Request users to review reassignment of contributions and memberships [in the UI](#request-reassignment-in-ui)
or [through a CSV file](#request-reassignment-by-using-a-csv-file).
For a large number of placeholder users, you should use a CSV file.
In both cases, users receive a request by email to accept or reject the reassignment.
The reassignment starts only after the selected user
[accepts the reassignment request](#accept-contribution-reassignment).
- Choose not to reassign contributions and memberships and [keep them assigned to placeholder users](#keep-as-placeholder).
All the contributions initially assigned to a single placeholder user can only be reassigned to a single active regular
user on the destination instance. The contributions assigned to a single placeholder user cannot be split among multiple
@ -353,6 +356,56 @@ Contributions of only one placeholder user can be reassigned to an active non-bo
Before a user accepts the reassignment, you can [cancel the request](#cancel-reassignment-request).
#### Request reassignment by using a CSV file
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/455901) in GitLab 17.10 [with a flag](../../../administration/feature_flags.md) named `importer_user_mapping_reassignment_csv`. Disabled by default.
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}
Prerequisites:
- You must have the Owner role for the group.
For a large number of placeholder users, you might want to
reassign contributions and memberships by using a CSV file.
You can download a prefilled CSV template with the following information.
For example:
| Source host | Import type | Source user identifier | Source user name | Source username |
|----------------------|-------------|------------------------|------------------|-----------------|
| `gitlab.example.com` | `gitlab` | `alice` | `Alice Coder` | `a.coer` |
Do not update **Source host**, **Import type**, or **Source user identifier**.
This information locates the corresponding database record
after you've uploaded the completed CSV file.
**Source user name** and **Source username** identify the source user
and are not used after you've uploaded the CSV file.
To request reassignment of contributions and memberships by using a CSV file:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Manage > Members**.
1. Select the **Placeholders** tab.
1. Select **Reassign with CSV**.
1. Download the prefilled CSV template.
1. In **GitLab username** or **GitLab public email**, enter the username or email address
of the GitLab user on the destination instance.
You can use only public email addresses for reassignment.
1. Upload the completed CSV file.
1. Select **Reassign**.
Users receive an email to review and accept any contributions reassigned to them.
#### Keep as placeholder
You might not want to reassign contributions and memberships to users on the destination instance. For example, you

View File

@ -170,7 +170,7 @@ To set this default:
{{< history >}}
- [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/463016) 60 day and 30 days triggers to project and group access tokens webhooks in GitLab 17.9 [with a flag](../../../administration/feature_flags.md) named `pat_expiry_inherited_members_notification`. Disabled by default.
- [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/463016) 60 day and 30 days triggers to project and group access tokens webhooks in GitLab 17.9 [with a flag](../../../administration/feature_flags.md) named `extended_expiry_webhook_execution_setting`. Disabled by default.
{{< /history >}}

View File

@ -112,57 +112,64 @@ For more information, see [`image_pull_secrets`](settings.md#image_pull_secrets)
{{< /history >}}
Prerequisites:
- Ensure the container images used in the devfile support [arbitrary user IDs](_index.md#arbitrary-user-ids).
Sudo access for a workspace does not mean that the container image used
in a [devfile](_index.md#devfile) can run with a user ID of `0`.
A development environment often requires sudo permissions to
install, configure, and use dependencies during runtime.
You can configure secure sudo access for a workspace with:
Development environments often require sudo permissions to install, configure, and use dependencies
during runtime. You can configure sudo access for a workspace with:
- [Sysbox](#with-sysbox)
- [Kata Containers](#with-kata-containers)
- [User namespaces](#with-user-namespaces)
Prerequisites:
- Your container images must support [arbitrary user IDs](_index.md#arbitrary-user-ids).
Even with sudo access configured, container images used in a [devfile](_index.md#devfile)
cannot run with a user ID of `0`.
### With Sysbox
[Sysbox](https://github.com/nestybox/sysbox) is a container runtime that improves container isolation
and enables containers to run the same workloads as virtual machines.
To configure sudo access for a workspace with Sysbox:
To configure sudo access with Sysbox:
1. In the Kubernetes cluster, [install Sysbox](https://github.com/nestybox/sysbox#installation).
1. In the GitLab agent for workspaces:
- Set [`default_runtime_class`](settings.md#default_runtime_class) to the runtime class
of Sysbox (for example, `sysbox-runc`).
- Set [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
- Set [`annotations`](settings.md#annotations) to `{"io.kubernetes.cri-o.userns-mode": "auto:size=65536"}`.
1. In your Kubernetes cluster, [install Sysbox](https://github.com/nestybox/sysbox#installation).
1. Configure the GitLab agent for workspaces:
- Set the default runtime class. In [`default_runtime_class`](settings.md#default_runtime_class),
enter the runtime class for Sysbox. For example, `sysbox-runc`.
- Enable privilege escalation.
Set [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
- Configure the annotations required by Sysbox. Set [`annotations`](settings.md#annotations) to
`{"io.kubernetes.cri-o.userns-mode": "auto:size=65536"}`.
### With Kata Containers
[Kata Containers](https://github.com/kata-containers/kata-containers) is a standard implementation of lightweight
virtual machines that perform like containers but provide the workload isolation and security of virtual machines.
[Kata Containers](https://github.com/kata-containers/kata-containers) is a standard implementation
of lightweight virtual machines that perform like containers but provide the workload isolation and
security of virtual machines.
To configure sudo access for a workspace with Kata Containers:
To configure sudo access with Kata Containers:
1. In the Kubernetes cluster, [install Kata Containers](https://github.com/kata-containers/kata-containers/tree/main/docs/install).
1. In the GitLab agent for workspaces:
- Set [`default_runtime_class`](settings.md#default_runtime_class) to one of the runtime classes
of Kata Containers (for example, `kata-qemu`).
- Set [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
1. In your Kubernetes cluster, [install Kata Containers](https://github.com/kata-containers/kata-containers/tree/main/docs/install).
1. Configure the GitLab agent for workspaces:
- Set the default runtime class. In [`default_runtime_class`](settings.md#default_runtime_class),
enter the runtime class for Kata Containers. For example, `kata-qemu`.
- Enable privilege escalation.
Set [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
### With user namespaces
[User namespaces](https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/) isolate the user
running inside the container from the user on the host.
[User namespaces](https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/) isolate
container users from host users.
To configure sudo access for a workspace with user namespaces:
To configure sudo access with user namespaces:
1. In the Kubernetes cluster, [configure user namespaces](https://kubernetes.io/blog/2024/04/22/userns-beta/).
1. In the GitLab agent for workspaces, set [`use_kubernetes_user_namespaces`](settings.md#use_kubernetes_user_namespaces)
and [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
1. In your Kubernetes cluster, [configure user namespaces](https://kubernetes.io/blog/2024/04/22/userns-beta/).
1. Configure the GitLab agent for workspaces:
- Set [`use_kubernetes_user_namespaces`](settings.md#use_kubernetes_user_namespaces) to `true`.
- Set [`allow_privilege_escalation`](settings.md#allow_privilege_escalation) to `true`.
## Build and run containers in a workspace

View File

@ -31,7 +31,7 @@ module ActiveContext
def elasticsearch_config
{
adapter: :net_http,
adapter: :typhoeus,
urls: options[:url],
transport_options: {
request: {

View File

@ -49,7 +49,7 @@ module ActiveContext
def opensearch_config
{
adapter: :net_http,
adapter: :typhoeus,
urls: options[:url],
transport_options: {
request: {

View File

@ -37,7 +37,7 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Client do
it 'includes all expected keys with correct values' do
expect(elasticsearch_config).to include(
adapter: :net_http,
adapter: :typhoeus,
urls: 'http://localhost:9200',
transport_options: {
request: {

View File

@ -32,6 +32,21 @@ RSpec.describe ActiveContext::Databases::Opensearch::Client do
end
end
describe '#opensearch_config' do
it 'returns correct configuration hash' do
config = client.send(:opensearch_config)
expect(config).to include(
urls: options[:url],
randomize_hosts: true
)
expect(config[:transport_options][:request]).to include(
timeout: options[:client_request_timeout],
open_timeout: described_class::OPEN_TIMEOUT
)
end
end
describe '#aws_credentials' do
context 'when static credentials are provided' do
let(:options) do

View File

@ -245,9 +245,7 @@ module Gitlab
end
def ensure_directory
unless Dir.exist?(default_directory)
FileUtils.mkdir_p(default_directory)
end
FileUtils.mkdir_p(default_directory)
end
def current_path

View File

@ -194,7 +194,7 @@ module Gitlab
def export_migration_details(migration_name, attributes)
directory = result_dir.join(migration_name)
FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
FileUtils.mkdir_p(directory)
File.write(directory.join(MIGRATION_DETAILS_FILE_NAME), attributes.to_json)
end

View File

@ -131,7 +131,7 @@ module Gitlab
# 15 tries will never complete within the maximum time with exponential
# backoff. So our limit is the runtime, not the number of tries.
Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1, tries: 15) do
FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir)
FileUtils.rm_rf(tmp_dir)
end
rescue StandardError => e
raise CleanupError, e

View File

@ -65,7 +65,7 @@ module Gitlab
end
def ensure_lock_files_path!
FileUtils.mkdir_p(lock_files_path) unless Dir.exist?(lock_files_path)
FileUtils.mkdir_p(lock_files_path)
end
def lock_files_path

View File

@ -60,7 +60,7 @@ module Gitlab
next if Gitlab::Utils::FileInfo.linked?(source_child)
if File.directory?(source_child)
FileUtils.mkdir_p(target_child, mode: DEFAULT_DIR_MODE) unless File.exist?(target_child)
FileUtils.mkdir_p(target_child, mode: DEFAULT_DIR_MODE)
recursive_merge(source_child, target_child)
else
FileUtils.mv(source_child, target_child)

View File

@ -41,7 +41,7 @@ module Gitlab
end
def cleanup!(path)
File.unlink(path) if File.exist?(path)
FileUtils.rm_f(path)
rescue Errno::ENOENT
# Path does not exist: Ignore. We already check `File.exist?`. Rescue to be extra safe.
end

View File

@ -143,7 +143,7 @@ namespace :gitlab do
end
def clone_repository(url, directory)
FileUtils.rm_rf(directory) if Dir.exist?(directory)
FileUtils.rm_rf(directory)
system("git clone #{url} --depth=1 --branch=master #{directory}")
end

View File

@ -80,7 +80,7 @@ namespace :tanuki_emoji do
puts "Importing emojis into: #{emoji_dir} ..."
# Re-create the assets folder and copy emojis renaming them to use name instead of unicode hex
FileUtils.rm_rf(emoji_dir) if Dir.exist?(emoji_dir)
FileUtils.rm_rf(emoji_dir)
FileUtils.mkdir_p(emoji_dir, mode: 0700)
TanukiEmoji.index.all.find_each do |emoji|

View File

@ -9078,6 +9078,9 @@ msgstr ""
msgid "Batch size"
msgstr ""
msgid "Batch size of namespaces for initial indexing"
msgstr ""
msgid "Batched Job|Background migrations"
msgstr ""
@ -11240,6 +11243,9 @@ msgstr ""
msgid "CICD|An error occurred while adding the authentication log entries. Please try again."
msgstr ""
msgid "CICD|An error occurred while removing the auto-added log entries. Please try again."
msgstr ""
msgid "CICD|Are you sure you want to remove %{namespace} from the job token allowlist?"
msgstr ""
@ -11255,6 +11261,9 @@ msgstr ""
msgid "CICD|Authentication log entries were successfully added to the allowlist."
msgstr ""
msgid "CICD|Authentication log entries were successfully removed from the allowlist."
msgstr ""
msgid "CICD|Authorized groups and projects"
msgstr ""
@ -11354,9 +11363,18 @@ msgstr ""
msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "CICD|Remove all auto-added allowlist entries"
msgstr ""
msgid "CICD|Remove group or project"
msgstr ""
msgid "CICD|Removing auto-added allowlist entries. Please wait while the action completes."
msgstr ""
msgid "CICD|Removing these entries could cause authentication failures or disrupt pipelines."
msgstr ""
msgid "CICD|Select the groups and projects authorized to use a CI/CD job token to authenticate requests to this project. %{linkStart}Learn more%{linkEnd}."
msgstr ""
@ -11387,6 +11405,9 @@ msgstr ""
msgid "CICD|There was a problem fetching authorization logs count."
msgstr ""
msgid "CICD|This action removes all groups and projects that were auto-added from the authentication log."
msgstr ""
msgid "CICD|Unprotected branches will not have access to the cache from protected branches."
msgstr ""
@ -47964,6 +47985,9 @@ msgstr ""
msgid "Remove email participants"
msgstr ""
msgid "Remove entries"
msgstr ""
msgid "Remove favicon"
msgstr ""

View File

@ -63,7 +63,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.4.0",
"@gitlab/svgs": "3.123.0",
"@gitlab/ui": "108.4.1",
"@gitlab/ui": "108.6.0",
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
"@gitlab/web-ide": "^0.0.1-dev-20250211142744",

View File

@ -278,6 +278,7 @@ class ApplicationSettingsAnalysis
vertex_ai_project
web_ide_oauth_application_id
zoekt_cpu_to_tasks_ratio
zoekt_rollout_batch_size
zoekt_indexing_enabled
zoekt_search_enabled
zoekt_settings

View File

@ -87,12 +87,29 @@ describe('AutopopulateAllowlistModal component', () => {
);
});
// TODO: Test for help link
// See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181294
it('renders help link', () => {
expect(findLink().text()).toBe('What is the compaction algorithm?');
expect(findLink().attributes('href')).toBe(
'/help/ci/jobs/ci_job_token#auto-populate-a-projects-allowlist',
);
});
});
it.each`
modalEvent | emittedEvent
${'canceled'} | ${'hide'}
${'hidden'} | ${'hide'}
${'secondary'} | ${'hide'}
`(
'emits the $emittedEvent event when $modalEvent event is triggered',
({ modalEvent, emittedEvent }) => {
expect(wrapper.emitted(emittedEvent)).toBeUndefined();
findModal().vm.$emit(modalEvent);
expect(wrapper.emitted(emittedEvent)).toHaveLength(1);
},
);
});
describe('when mutation is running', () => {

View File

@ -1,4 +1,9 @@
import { GlAlert, GlLoadingIcon, GlFormRadioGroup } from '@gitlab/ui';
import {
GlAlert,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
GlFormRadioGroup,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -12,6 +17,7 @@ import {
} from '~/token_access/constants';
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import RemoveAutopopulatedEntriesModal from '~/token_access/components/remove_autopopulated_entries_modal.vue';
import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
@ -19,6 +25,7 @@ import inboundGetCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbou
import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import getAuthLogCountQuery from '~/token_access/graphql/queries/get_auth_log_count.query.graphql';
import getCiJobTokenScopeAllowlistQuery from '~/token_access/graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
import removeAutopopulatedEntriesMutation from '~/token_access/graphql/mutations/remove_autopopulated_entries.mutation.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ConfirmActionModal from '~/vue_shared/components/confirm_action_modal.vue';
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
@ -31,6 +38,7 @@ import {
inboundRemoveNamespaceSuccess,
inboundUpdateScopeSuccessResponse,
mockAuthLogsCountResponse,
mockRemoveAutopopulatedEntriesResponse,
} from './mock_data';
const projectPath = 'root/my-repo';
@ -63,13 +71,22 @@ describe('TokenAccess component', () => {
const inboundUpdateScopeSuccessResponseHandler = jest
.fn()
.mockResolvedValue(inboundUpdateScopeSuccessResponse);
const removeAutopopulatedEntriesMutationHandler = jest
.fn()
.mockResolvedValue(mockRemoveAutopopulatedEntriesResponse());
const removeAutopopulatedEntriesMutationErrorHandler = jest
.fn()
.mockResolvedValue(mockRemoveAutopopulatedEntriesResponse({ errorMessage: message }));
const failureHandler = jest.fn().mockRejectedValue(error);
const mockToastShow = jest.fn();
const findAutopopulateAllowlistModal = () => wrapper.findComponent(AutopopulateAllowlistModal);
const findAutopopulationAlert = () => wrapper.findByTestId('autopopulation-alert');
const findAllowlistOptions = () => wrapper.findComponent(GlDisclosureDropdown);
const findAllowlistOption = (index) =>
wrapper.findAllComponents(GlDisclosureDropdownItem).at(index).find('button');
const findFormSelector = () => wrapper.findByTestId('form-selector');
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findToggleFormBtn = () => wrapper.findByTestId('crud-form-toggle');
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
const findNamespaceForm = () => wrapper.findComponent(NamespaceForm);
@ -78,6 +95,8 @@ describe('TokenAccess component', () => {
const findGroupCount = () => wrapper.findByTestId('group-count');
const findProjectCount = () => wrapper.findByTestId('project-count');
const findConfirmActionModal = () => wrapper.findComponent(ConfirmActionModal);
const findRemoveAutopopulatedEntriesModal = () =>
wrapper.findComponent(RemoveAutopopulatedEntriesModal);
const findTokenAccessTable = () => wrapper.findComponent(TokenAccessTable);
const createComponent = (
@ -88,6 +107,7 @@ describe('TokenAccess component', () => {
enforceAllowlist = false,
projectAllowlistLimit = 2,
stubs = {},
isLoading = false,
} = {},
) => {
wrapper = shallowMountExtended(InboundTokenAccess, {
@ -110,24 +130,30 @@ describe('TokenAccess component', () => {
},
});
return waitForPromises();
if (!isLoading) {
return waitForPromises();
}
return Promise.resolve();
};
describe('loading state', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
createComponent(
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
],
]);
{ isLoading: true },
);
expect(findLoadingIcon().exists()).toBe(true);
await nextTick();
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
});
@ -448,8 +474,12 @@ describe('TokenAccess component', () => {
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationHandler],
],
{ authenticationLogsMigrationForAllowlist: true, stubs: { CrudComponent } },
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
),
);
@ -496,6 +526,132 @@ describe('TokenAccess component', () => {
expect(findFormSelector().props('selected')).toBe(null);
});
});
describe('remove autopopulated entries', () => {
const triggerRemoveEntries = () => {
findAllowlistOption(0).trigger('click');
findRemoveAutopopulatedEntriesModal().vm.$emit('remove-entries');
};
it('additional actions are available in the disclosure dropdown', () => {
expect(findAllowlistOptions().exists()).toBe(true);
});
it('"Remove only entries auto-added" renders the remove autopopulated entries modal', async () => {
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(false);
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
it('shows loading state while remove autopopulated entries mutation is processing', async () => {
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
triggerRemoveEntries();
await nextTick();
expect(findCountLoadingIcon().exists()).toBe(true);
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Removing auto-added allowlist entries. Please wait while the action completes.',
);
});
it('calls the remove autopopulated entries mutation and refetches allowlist', async () => {
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(1);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
});
it('shows toast message when mutation is successful', async () => {
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(mockToastShow).toHaveBeenCalledWith(
'Authentication log entries were successfully removed from the allowlist.',
);
});
it('shows error alert when mutation returns an error', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationErrorHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe('An error occurred');
});
it('shows error alert when mutation fails', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, failureHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe(
'An error occurred while removing the auto-added log entries. Please try again.',
);
});
it('modal can be re-opened again after it closes', async () => {
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
findRemoveAutopopulatedEntriesModal().vm.$emit('hide');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(false);
findAllowlistOption(0).trigger('click');
await nextTick();
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
});
});
describe.each`

View File

@ -323,3 +323,13 @@ export const mockAutopopulateAllowlistError = {
},
},
};
export const mockRemoveAutopopulatedEntriesResponse = ({ errorMessage } = {}) => ({
data: {
ciJobTokenScopeClearAllowlistAutopopulations: {
status: 'complete',
errors: errorMessage ? [{ message: errorMessage }] : [],
__typename: 'CiJobTokenScopeClearAllowlistAutopopulationsPayload',
},
},
});

View File

@ -0,0 +1,63 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RemoveAutopopulatedEntriesModal from '~/token_access/components/remove_autopopulated_entries_modal.vue';
const projectName = 'My project';
const fullPath = 'root/my-repo';
Vue.use(VueApollo);
describe('RemoveAutopopulatedEntriesModal component', () => {
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const createComponent = ({ props } = {}) => {
wrapper = shallowMountExtended(RemoveAutopopulatedEntriesModal, {
provide: {
fullPath,
},
propsData: {
projectName,
showModal: true,
...props,
},
});
};
describe('template', () => {
beforeEach(() => {
createComponent();
});
it.each`
modalEvent | emittedEvent
${'canceled'} | ${'hide'}
${'hidden'} | ${'hide'}
${'secondary'} | ${'hide'}
`(
'emits the $emittedEvent event when $modalEvent event is triggered',
({ modalEvent, emittedEvent }) => {
expect(wrapper.emitted(emittedEvent)).toBeUndefined();
findModal().vm.$emit(modalEvent);
expect(wrapper.emitted(emittedEvent)).toHaveLength(1);
},
);
});
describe('when clicking on the primary button', () => {
it('emits the remove-entries event', () => {
createComponent();
expect(wrapper.emitted('remove-entries')).toBeUndefined();
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(wrapper.emitted('remove-entries')).toHaveLength(1);
});
});
});

View File

@ -22,6 +22,7 @@ describe('Token access table', () => {
const findName = () => wrapper.findByTestId('token-access-name');
const findPolicies = () => findAllTableRows().at(0).findAll('td').at(1);
const findAutopopulatedIcon = () => wrapper.findByTestId('autopopulated-icon');
const findLoadingMessage = () => wrapper.findByTestId('loading-message');
describe.each`
type | items
@ -86,6 +87,17 @@ describe('Token access table', () => {
createComponent({ items: mockGroups, loading: true });
expect(findTable().findComponent(GlLoadingIcon).props('size')).toBe('md');
expect(findLoadingMessage().exists()).toBe(false);
});
it('shows loading message when available', () => {
createComponent({
items: mockGroups,
loading: true,
loadingMessage: 'Removing auto-populated entries...',
});
expect(findLoadingMessage().text()).toBe('Removing auto-populated entries...');
});
});

View File

@ -5347,6 +5347,7 @@ export const groupWorkItemsQueryResponse = {
title: 'a group level work item',
updatedAt: '',
webUrl: 'web/url',
userDiscussionsCount: 0,
widgets: [
{
__typename: 'WorkItemWidgetAssignees',

View File

@ -172,7 +172,7 @@ RSpec.describe Gitlab::Gpg do
it 'does not fail if the homedir was deleted while running' do
expect do
described_class.using_tmp_keychain do
FileUtils.remove_entry(described_class.current_home_dir)
FileUtils.rm_rf(described_class.current_home_dir)
end
end.not_to raise_error
end
@ -204,8 +204,8 @@ RSpec.describe Gitlab::Gpg do
end
before do
# Stub all the other calls for `remove_entry`
allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
# Stub all the other calls for `rm_rf`
allow(FileUtils).to receive(:rm_rf).with(any_args).and_call_original
end
it "tries for #{seconds} or 15 times" do
@ -216,15 +216,15 @@ RSpec.describe Gitlab::Gpg do
it 'tries at least 2 times to remove the tmp dir before raising', :aggregate_failures do
expect(Retriable).to receive(:sleep).at_least(:twice)
expect(FileUtils).to receive(:remove_entry).with(tmp_dir).at_least(:twice).and_raise('Deletion failed')
expect(FileUtils).to receive(:rm_rf).with(tmp_dir).at_least(:twice).and_raise('Deletion failed')
expect { described_class.using_tmp_keychain {} }.to raise_error(described_class::CleanupError)
end
it 'does not attempt multiple times when the deletion succeeds' do
expect(Retriable).to receive(:sleep).once
expect(FileUtils).to receive(:remove_entry).with(tmp_dir).once.and_raise('Deletion failed')
expect(FileUtils).to receive(:remove_entry).with(tmp_dir).and_call_original
expect(FileUtils).to receive(:rm_rf).with(tmp_dir).once.and_raise('Deletion failed')
expect(FileUtils).to receive(:rm_rf).with(tmp_dir).and_call_original
expect { described_class.using_tmp_keychain {} }.not_to raise_error

View File

@ -2,38 +2,68 @@
require 'spec_helper'
RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout, feature_category: :importers do
let!(:tmpdir) { Dir.mktmpdir }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
RSpec.describe 'gitlab:update_templates rake task', :silence_stdout, feature_category: :importers do
before do
Rake.application.rake_require 'tasks/gitlab/update_templates'
admin = create(:admin)
create(:key, user: admin)
end
allow(Gitlab::ProjectTemplate)
.to receive(:archive_directory)
.and_return(Pathname.new(tmpdir))
describe '.update_project_templates' do
let!(:tmpdir) { Dir.mktmpdir }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
# Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard
stub_request(:get, %r{^https://.*/api/v4/projects/#{template.uri_encoded_project_path}/repository/commits\?page=1&per_page=1})
.to_return(
status: 200,
body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json,
headers: { 'Content-Type' => 'application/json' }
before do
admin = create(:admin)
create(:key, user: admin)
allow(Gitlab::ProjectTemplate)
.to receive(:archive_directory)
.and_return(Pathname.new(tmpdir))
# Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard
stub_request(:get, %r{^https://.*/api/v4/projects/#{template.uri_encoded_project_path}/repository/commits\?page=1&per_page=1})
.to_return(
status: 200,
body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
after do
FileUtils.rm_rf(tmpdir)
end
it 'updates valid project templates' do
expect(Gitlab::TaskHelpers).to receive(:run_command!).with(anything).exactly(6).times.and_call_original
expect(Gitlab::TaskHelpers).to receive(:run_command!).with(%w[git push -u origin master])
expect { run_rake_task('gitlab:update_project_templates', [template.name]) }
.to change { Dir.entries(tmpdir) }
.by(["#{template.name}.tar.gz"])
end
end
describe '#clone_repository' do
let(:repo_url) { "https://test.com/example/text.git" }
let(:template) do
Struct.new(:repo_url, :cleanup_regex).new(
repo_url,
/(\.{1,2}|LICENSE|Global|\.test)\z/
)
end
end
after do
FileUtils.rm_rf(tmpdir)
end
let(:dir) { Rails.root.join('vendor/text').to_s }
let(:kernel_system_call_params) { "git clone #{repo_url} --depth=1 --branch=master #{dir}" }
it 'updates valid project templates' do
expect(Gitlab::TaskHelpers).to receive(:run_command!).with(anything).exactly(6).times.and_call_original
expect(Gitlab::TaskHelpers).to receive(:run_command!).with(%w[git push -u origin master])
before do
stub_const('TEMPLATE_DATA', [template])
allow(main_object).to receive(:system).with(kernel_system_call_params).and_return(false)
allow(FileUtils).to receive(:rm_rf).and_call_original
end
expect { run_rake_task('gitlab:update_project_templates', [template.name]) }
.to change { Dir.entries(tmpdir) }
.by(["#{template.name}.tar.gz"])
it 'calls FileUtils.rm_rf to remove directory' do
expect(FileUtils).to receive(:rm_rf).with(dir)
run_rake_task('gitlab:update_templates')
end
end
end

View File

@ -1436,10 +1436,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.123.0.tgz#1fa3b1a709755ff7c8ef67e18c0442101655ebf0"
integrity sha512-yjVn+utOTIKk8d9JlvGo6EgJ4TQ+CKpe3RddflAqtsQqQuL/2MlVdtaUePybxYzWIaumFuh5LouQ6BrWyw1niQ==
"@gitlab/ui@108.4.1":
version "108.4.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-108.4.1.tgz#d45dbcc3a9f568694f53eebb795544157ddbf44f"
integrity sha512-o3mcR/AQ5tkKd3mMClMvsJIHhLWzU6n1L0m3bp4Yr4zaNtRG3c8gEGF7MR6pMhlnyb5r/FQk2wigxHaUuOSblQ==
"@gitlab/ui@108.6.0":
version "108.6.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-108.6.0.tgz#55c87776b1f247250dc33ab7921e27c415675842"
integrity sha512-azX2X0Qzs9Jt1ZruGlFWa9JAJuCO8Bix+uWMMWYqCQYCk4HLOT2t7jI0Kt8vUTwweA/IsmT6VPID2aKxFeJR0g==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"