Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-10 21:09:24 +00:00
parent 577bb49691
commit 02c3b2af44
46 changed files with 631 additions and 143 deletions

View file

@ -6,7 +6,7 @@ import pipelinesMixin from '~/pipelines/mixins/pipelines';
import eventHub from '~/pipelines/event_hub';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesPaginationApiMixin from '~/pipelines/mixins/pipelines_pagination_api_mixin';
export default {
components: {
@ -16,7 +16,7 @@ export default {
GlModal,
GlLink,
},
mixins: [pipelinesMixin, CIPaginationMixin],
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
props: {
endpoint: {
type: String,

View file

@ -2,9 +2,9 @@
import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__ } from '~/locale';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
import emptyState from './empty_state.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
@ -33,7 +33,7 @@ export default {
directives: {
'gl-modal': GlModalDirective,
},
mixins: [CIPaginationMixin, environmentsMixin],
mixins: [EnvironmentsPaginationApiMixin, environmentsMixin],
props: {
endpoint: {
type: String,

View file

@ -1,7 +1,7 @@
<script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
@ -14,7 +14,7 @@ export default {
StopEnvironmentModal,
},
mixins: [environmentsMixin, CIPaginationMixin],
mixins: [environmentsMixin, EnvironmentsPaginationApiMixin],
props: {
endpoint: {

View file

@ -1,6 +1,5 @@
/**
* API callbacks for pagination and tabs
* shared between Pipelines and Environments table.
*
* Components need to have `scope`, `page` and `requestData`
*/

View file

@ -6,7 +6,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesPaginationApiMixin from '../../mixins/pipelines_pagination_api_mixin';
import pipelinesMixin from '../../mixins/pipelines';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
@ -22,7 +22,7 @@ export default {
PipelinesFilteredSearch,
GlIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin],
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
props: {
store: {
type: Object,

View file

@ -5,6 +5,7 @@ import {
GlTooltipDirective,
GlFriendlyWrap,
GlIcon,
GlLink,
GlButton,
GlPagination,
} from '@gitlab/ui';
@ -16,6 +17,7 @@ export default {
components: {
GlIcon,
GlFriendlyWrap,
GlLink,
GlButton,
GlPagination,
TestCaseDetails,
@ -97,11 +99,9 @@ export default {
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap
v-if="testCase.file"
:symbols="$options.wrapSymbols"
:text="testCase.file"
/>
<gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
</gl-link>
<gl-button
v-if="testCase.file"
v-gl-tooltip

View file

@ -0,0 +1,66 @@
/**
* API callbacks for pagination and tabs
*
* Components need to have `scope`, `page` and `requestData`
*/
import { validateParams } from '~/pipelines/utils';
import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
export default {
methods: {
onChangeTab(scope) {
if (this.scope === scope) {
return;
}
let params = {
scope,
page: '1',
};
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
let params = {
page: Number(page).toString(),
};
if (this.scope) {
params.scope = this.scope;
}
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
onChangeWithFilter(params) {
return { ...params, ...validateParams(this.requestData) };
},
updateInternalState(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters)
.map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
})
.join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
},
},
};

View file

@ -58,8 +58,9 @@ const createLegacyPipelinesDetailApp = (mediator) => {
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});

View file

@ -1,4 +1,4 @@
import { addIconStatus, formattedTime } from './utils';
import { addIconStatus, formatFilePath, formattedTime } from './utils';
export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
@ -17,7 +17,13 @@ export const getSuiteTests = (state) => {
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
return testCases.map(addIconStatus).slice(start, start + perPage);
return testCases
.map((testCase) => ({
...testCase,
filePath: testCase.file ? `${state.blobPath}/${formatFilePath(testCase.file)}` : null,
}))
.map(addIconStatus)
.slice(start, start + perPage);
};
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;

View file

@ -1,4 +1,5 @@
export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath,
summaryEndpoint,
suiteEndpoint,
testReports: {},

View file

@ -1,6 +1,15 @@
import { __, sprintf } from '../../../locale';
import { TestStatus } from '../../constants';
/**
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
* @param {String} file
* @returns {String} - formatted value
*/
export function formatFilePath(file) {
return file.replace(/^\.?\/*/, '');
}
export function iconForTestStatus(status) {
switch (status) {
case TestStatus.SUCCESS:

View file

@ -3,20 +3,37 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { uniq } from 'lodash';
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
const origExpansions = new Map();
const findSettingsSection = (sectionSelector, node) => {
return node.parentElement.closest(sectionSelector);
};
const resetSections = ({ sectionSelector, expandSection, collapseSection }) => {
document.querySelectorAll(sectionSelector).forEach((section, index) => {
section.classList.remove(HIDE_CLASS);
if (index === 0) {
const restoreExpansionState = ({ expandSection, collapseSection }) => {
origExpansions.forEach((isExpanded, section) => {
if (isExpanded) {
expandSection(section);
} else {
collapseSection(section);
}
});
origExpansions.clear();
};
const saveExpansionState = (sections, { isExpanded }) => {
// If we've saved expansions before, don't override it.
if (origExpansions.size > 0) {
return;
}
sections.forEach((section) => origExpansions.set(section, isExpanded(section)));
};
const resetSections = ({ sectionSelector }) => {
document.querySelectorAll(sectionSelector).forEach((section) => {
section.classList.remove(HIDE_CLASS);
});
};
const clearHighlights = () => {
@ -85,6 +102,12 @@ export default {
type: String,
required: true,
},
isExpandedFn: {
type: Function,
required: false,
// default to a function that returns false
default: () => () => false,
},
},
data() {
return {
@ -97,6 +120,7 @@ export default {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
isExpanded: this.isExpandedFn,
};
this.searchTerm = value;
@ -104,7 +128,11 @@ export default {
clearResults(displayOptions);
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
displayResults(displayOptions, search(this.searchRoot, value));
} else {
restoreExpansionState(displayOptions);
}
},
expandSection(section) {

View file

@ -1,6 +1,5 @@
import Vue from 'vue';
import $ from 'jquery';
import { expandSection, closeSection } from '~/settings_panels';
import { expandSection, closeSection, isExpanded } from '~/settings_panels';
import SearchSettings from '~/search_settings/components/search_settings.vue';
const mountSearch = ({ el }) =>
@ -12,10 +11,11 @@ const mountSearch = ({ el }) =>
props: {
searchRoot: document.querySelector('#content-body'),
sectionSelector: '.js-search-settings-section, section.settings',
isExpandedFn: isExpanded,
},
on: {
collapse: (section) => closeSection($(section)),
expand: (section) => expandSection($(section)),
collapse: closeSection,
expand: expandSection,
},
}),
});

View file

@ -1,7 +1,22 @@
import $ from 'jquery';
import { __ } from './locale';
export function expandSection($section) {
/**
* Returns true if the given section is expanded or not
*
* For legacy consistency, it supports both jQuery and DOM elements
*
* @param {jQuery | Element} section
*/
export function isExpanded(sectionArg) {
const section = sectionArg instanceof $ ? sectionArg[0] : sectionArg;
return section.classList.contains('expanded');
}
export function expandSection(sectionArg) {
const $section = $(sectionArg);
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse'));
// eslint-disable-next-line @gitlab/no-global-event-off
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
@ -13,7 +28,9 @@ export function expandSection($section) {
}
}
export function closeSection($section) {
export function closeSection(sectionArg) {
const $section = $(sectionArg);
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand'));
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
@ -26,7 +43,7 @@ export function closeSection($section) {
export function toggleSection($section) {
$section.removeClass('no-animate');
if ($section.hasClass('expanded')) {
if (isExpanded($section)) {
closeSection($section);
} else {
expandSection($section);
@ -38,7 +55,7 @@ export default function initSettingsPanels() {
const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
if (!$section.hasClass('expanded')) {
if (!isExpanded($section)) {
$section.find('.settings-content').on('scroll.expandSection', () => {
$section.removeClass('no-animate');
expandSection($section);

View file

@ -326,7 +326,7 @@
color: $gl-text-color-secondary;
}
.badge.badge-pill + span:not(.badge.badge-pill) {
.badge.badge-pill + span:not(.badge):not(.badge-pill) {
// Expects up to 3 digits on the badge
margin-right: 40px;
}

View file

@ -903,7 +903,7 @@ table a code {
padding: 0;
background-color: #4f4f4f;
}
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {

View file

@ -902,7 +902,7 @@ table a code {
padding: 0;
background-color: #dbdbdb;
}
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {

View file

@ -1174,7 +1174,7 @@ table a code {
padding: 0;
background-color: #dbdbdb;
}
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {

View file

@ -1,11 +1,10 @@
<!-- Matomo -->
- matomo_disable_cookies = extra_config.has_key?('matomo_disable_cookies') && extra_config.matomo_disable_cookies
= javascript_tag do
:plain
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
#{matomo_disable_cookies ? '_paq.push(["disableCookies"])' : ""};
#{extra_config.matomo_disable_cookies ? '_paq.push(["disableCookies"])' : ""};
(function() {
var u="//#{extra_config.matomo_url}/";
_paq.push(['setTrackerUrl', u+'matomo.php']);

View file

@ -82,5 +82,6 @@
#js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json) } }
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(@project, @pipeline.sha) } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project

View file

@ -59,7 +59,7 @@
= f.hidden_field 'milestone_id', value: milestone[:id], id: nil
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- if @project.group.present? && issuable_sidebar[:supports_iterations]
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:supports_time_tracking]
#issuable-time-tracker.block

View file

@ -0,0 +1,5 @@
---
title: Add link to test case file in pipeline test report
merge_request: 53650
author:
type: added

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292601
milestone: '13.8'
type: development
group: group::release
default_enabled: false
default_enabled: true

View file

@ -819,6 +819,7 @@ Settings.forti_token_cloud['enabled'] = false if Settings.forti_token_cloud['ena
Settings['extra'] ||= Settingslogic.new({})
Settings.extra['matomo_site_id'] ||= Settings.extra['piwik_site_id'] if Settings.extra['piwik_site_id'].present?
Settings.extra['matomo_url'] ||= Settings.extra['piwik_url'] if Settings.extra['piwik_url'].present?
Settings.extra['matomo_disable_cookies'] = false if Settings.extra['matomo_disable_cookies'].nil?
#
# Rack::Attack settings

View file

@ -811,3 +811,38 @@ You can set them globally or per-job in the [`variables`](../yaml/README.md#vari
## System calls not available on GitLab.com shared runners
GitLab.com shared runners run on CoreOS. This means that you cannot use some system calls, like `getlogin`, from the C standard library.
## Artifact and cache settings
> Introduced in GitLab Runner 13.9.
Artifact and cache settings control the compression ratio of artifacts and caches.
Use these settings to specify the size of the archive produced by a job.
- On a slow network, uploads might be faster for smaller archives.
- On a fast network where bandwidth and storage are not a concern, uploads might be faster using the fastest compression ratio, despite the archive produced being larger.
For [GitLab Pages](../../user/project/pages/index.md) to serve
[HTTP Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests), artifacts
should use the `ARTIFACT_COMPRESSION_LEVEL: fastest` setting, as only uncompressed zip archives
support this feature.
A meter can also be enabled to provide the rate of transfer for uploads and downloads.
```yaml
variables:
# output upload and download progress every 2 seconds
TRANSFER_METER_FREQUENCY: "2s"
# Use fast compression for artifacts, resulting in larger archives
ARTIFACT_COMPRESSION_LEVEL: "fast"
# Use no compression for caches
CACHE_COMPRESSION_LEVEL: "fastest"
```
| Variable | Description |
|---------------------------------|--------------------------------------------------------|
| `TRANSFER_METER_FREQUENCY` | Specify how often to print the meter's transfer rate. It can be set to a duration (for example, `1s` or `1m30s`). A duration of `0` disables the meter (default). When a value is set, the pipeline shows a progress meter for artifact and cache uploads and downloads. |
| `ARTIFACT_COMPRESSION_LEVEL` | To adjust compression ratio, set to `fastest`, `fast`, `default`, `slow`, or `slowest`. This setting works with the fastzip archiver only, so the GitLab Runner feature flag [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) must also be enabled. |
| `CACHE_COMPRESSION_LEVEL` | To adjust compression ratio, set to `fastest`, `fast`, `default`, `slow`, or `slowest`. This setting works with the fastzip archiver only, so the GitLab Runner feature flag [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) must also be enabled. |

View file

@ -4358,6 +4358,9 @@ You can use [CI/CD variables](../variables/README.md) to configure runner Git be
- [`GIT_FETCH_EXTRA_FLAGS`](../runners/README.md#git-fetch-extra-flags)
- [`GIT_DEPTH`](../runners/README.md#shallow-cloning) (shallow cloning)
- [`GIT_CLONE_PATH`](../runners/README.md#custom-build-directories) (custom build directories)
- [`TRANSFER_METER_FREQUENCY`](../runners/README.md#artifact-and-cache-settings) (artifact/cache meter update frequency)
- [`ARTIFACT_COMPRESSION_LEVEL`](../runners/README.md#artifact-and-cache-settings) (artifact archiver compression level)
- [`CACHE_COMPRESSION_LEVEL`](../runners/README.md#artifact-and-cache-settings) (cache archiver compression level)
You can also use variables to configure how many times a runner
[attempts certain stages of job execution](../runners/README.md#job-stages-attempts).

View file

@ -1,6 +1,6 @@
---
stage: none
group: unassigned
stage: Fulfillment
group: License
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View file

@ -8,6 +8,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This page will cover architectural patterns and tips for developers to follow to prevent [transient bugs.](https://about.gitlab.com/handbook/engineering/quality/issue-triage/#transient-bugs)
## Common root causes
We've noticed a few root causes that come up frequently when addressing transient bugs.
- Needs better state management in the backend or frontend.
- Frontend code needs improvements.
- Lack of test coverage.
- Race conditions.
## Frontend
### Don't rely on response order

View file

@ -43,7 +43,7 @@ receive alert payloads in JSON format. You can always
1. Toggle the **Active** alert setting to display the **URL** and **Authorization Key**
for the webhook configuration.
### HTTP Endpoints **PREMIUM**
### HTTP Endpoints **(PREMIUM)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4442) in GitLab Premium 13.6.
@ -72,7 +72,7 @@ side of the integrations list.
### External Prometheus integration
For GitLab versions 13.1 and greater, please read
For GitLab versions 13.1 and greater, read
[External Prometheus Instances](../metrics/alerts.md#external-prometheus-instances)
to configure alerts for this integration.

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -144,6 +144,19 @@ To search for users, enter your criteria in the search field. The user search is
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
#### User impersonation
An administrator can "impersonate" any other user, including other administrator users.
This allows the administrator to "see what the user sees," and take actions on behalf of the user.
You can impersonate a user in the following ways:
- Through the UI, by selecting **Admin Area > Overview > Users > [Select a user] > Impersonate**.
- With the API, using [impersonation tokens](../../api/README.md#impersonation-tokens).
All impersonation activities are [captured with audit events](../../administration/audit_events.md#impersonation-data).
![user impersonation button](img/impersonate_user_button_v13_8.png)
#### Users statistics
The **Users statistics** page provides an overview of user accounts by role. These statistics are

View file

@ -776,7 +776,7 @@ A site profile contains the following:
- **Profile name**: A name you assign to the site to be scanned.
- **Target URL**: The URL that DAST runs against.
## Site profile validation
### Site profile validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233020) in GitLab 13.8.
@ -799,37 +799,51 @@ To create a site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Select **Manage** in the **DAST Profiles** row.
1. Select **New Profile > Site Profile**.
1. Type in a unique **Profile name** and **Target URL** then select **Save profile**.
1. Select **New > Site Profile**.
1. Complete the fields then select **Save profile**.
The site profile is created.
### Edit a site profile
To edit an existing site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Select **Manage** in the **DAST Profiles** row.
1. Select **Edit** in the row of the profile to edit.
1. Edit the **Profile name** and **Target URL**, then select **Save profile**.
1. In the **DAST Profiles** row select **Manage**.
1. Select the **Site Profiles** tab.
1. In the profile's row select the **More actions** (**{ellipsis_v}**) menu, then select **Edit**.
1. Edit the fields then select **Save profile**.
The site profile is updated with the edited details.
### Delete a site profile
To delete an existing site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Select **Manage** in the **DAST Profiles** row.
1. Select **{remove}** (Delete profile) in the row of the profile to delete.
1. In the **DAST Profiles** row select **Manage**.
1. Select the **Site Profiles** tab.
1. In the profile's row select the **More actions** (**{ellipsis_v}**) menu, then select **Delete**.
1. Select **Delete** to confirm the deletion.
The site profile is deleted.
### Validate a site profile
Prerequisites:
- A site profile.
To validate a site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Select **Manage** in the **DAST Profiles** row.
1. Select **Validate target site** beside the profile to validate.
1. In the **DAST Profiles** row select **Manage**.
1. Select the **Site Profiles** tab.
1. In the profile's row select **Validate** or **Retry validation**.
1. Select the validation method.
1. For **Text file validation**:
1. Download the validation file listed in **Step 2**.
1. Upload the validation file to the host. You can upload the file to the location in
1. Upload the validation file to the host. Upload the file to the location in
**Step 3** or any location you prefer.
1. Select **Validate**.
1. For **Header validation**:
@ -840,22 +854,23 @@ To validate a site profile:
The site is validated and an active scan can run against it.
If a validated site profile's target URL is edited, the site is no longer validated.
If a validated site profile's target URL is edited, the site's validation status is revoked.
### Revoke a site validation
### Revoke a site profile's validation status
To revoke validation from a site profile:
Note that all site profiles with the same URL have their validation status revoked.
To revoke a site profile's validation status:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Select **Manage** in the **DAST Profiles** row.
1. In the **DAST Profiles** row select **Manage**.
1. Select **Revoke validation** beside the validated profile.
1. Select **Revoke validation**.
The site profile's validation is revoked. An active scan cannot be run against it or any other profile with the same URL.
The site profile's validation status is revoked.
#### Validated site profile headers
The following are code samples of how you could provide the required site profile header in your
The following are code samples of how you can provide the required site profile header in your
application.
##### Ruby on Rails example for on-demand scan
@ -900,27 +915,26 @@ app.get('/dast-website-target', function(req, res) {
## Scanner profile
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222767) in GitLab 13.4.
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/225804) in GitLab 13.5: scan mode, AJAX spider, debug messages.
A scanner profile defines the scanner settings used to run an on-demand scan:
- **Profile name:** A name you give the scanner profile. For example, "Spider_15".
- **Scan mode:** A passive scan monitors all HTTP messages (requests and responses) sent to the target. An active scan attacks the target to find potential vulnerabilities.
- **Spider timeout:** The maximum number of minutes allowed for the spider to traverse the site.
- **Target timeout:** The maximum number of seconds DAST waits for the site to be available before
starting the scan.
- **Scan mode:** A passive scan monitors all HTTP messages (requests and responses) sent to the target. An active scan attacks the target to find potential vulnerabilities.
- **AJAX spider:** Run the AJAX spider, in addition to the traditional spider, to crawl the target site.
- **Debug messages:** Include debug messages in the DAST console output.
Scan mode, AJAX spider, Debug messages are [added in GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/225804)
### Create a scanner profile
To create a scanner profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Click **Manage** in the **DAST Profiles** row.
1. Click **New Profile > Scanner Profile**.
1. Enter a unique **Profile name**, the desired **Spider timeout**, and the **Target timeout**.
1. In the **DAST Profiles** row select **Manage**.
1. Select **New > Scanner Profile**.
1. Complete the form. For details of each field, see [Scanner profile](#scanner-profile).
1. Click **Save profile**.
### Edit a scanner profile
@ -929,7 +943,12 @@ To edit a scanner profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Click **Manage** in the **DAST Profiles** row.
1. Click **Edit** in the scanner profile's row.
1. Select the **Scanner Profiles** tab.
1. In the scanner's row select the **More actions** (**{ellipsis_v}**) menu, then select **Edit**.
1. Edit the form.
1. Select **Save profile**.
The scanner profile is updated with the edited details.
### Delete a scanner profile
@ -937,7 +956,11 @@ To delete a scanner profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. Click **Manage** in the **DAST Profiles** row.
1. Click **{remove}** (Delete profile) in the scanner profile's row.
1. Select the **Scanner Profiles** tab.
1. In the scanner's row select the **More actions** (**{ellipsis_v}**) menu, then select **Delete**.
1. Select **Delete**.
The scanner profile is deleted.
## Reports

View file

@ -387,3 +387,13 @@ If you encounter this error while adding a Kubernetes cluster, ensure you're
properly pasting the service token. Some shells may add a line break to the
service token, making it invalid. Ensure that there are no line breaks by
pasting your token into an editor and removing any additional spaces.
You may also experience this error if your certificate is not valid. To check that your certificate's
subject alternative names contain the correct domain for your cluster's API, run this:
```shell
echo | openssl s_client -showcerts -connect kubernetes.example.com:443 2>/dev/null |
openssl x509 -inform pem -noout -text
```
Note that the `-connect` argument expects a `host:port` combination. For example, `https://kubernetes.example.com` would be `kubernetes.example.com:443`.

View file

@ -475,6 +475,16 @@ terminal.
Read the [Release CLI documentation](https://gitlab.com/gitlab-org/release-cli/-/blob/master/docs/index.md)
for details.
## Release Metrics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259703) in GitLab Premium 13.9.
Group-level release metrics are available by navigating to **Group > Analytics > CI/CD**.
These metrics include:
- Total number of releases in the group
- Percentage of projects in the group that have at least one release
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module Database
module Migrations
Observation = Struct.new(
:migration,
:walltime,
:success
)
class Instrumentation
attr_reader :observations
def initialize
@observations = []
end
def observe(migration, &block)
observation = Observation.new(migration)
observation.success = true
exception = nil
observation.walltime = Benchmark.realtime do
yield
rescue => e
exception = e
observation.success = false
end
record_observation(observation)
raise exception if exception
observation
end
private
def record_observation(observation)
@observations << observation
end
end
end
end
end

View file

@ -231,5 +231,37 @@ namespace :gitlab do
puts "Found user created projects. Database active"
exit 0
end
desc 'Run migrations with instrumentation'
task :migration_testing, [:result_file] => :environment do |_, args|
result_file = args[:result_file] || raise("Please specify result_file argument")
raise "File exists already, won't overwrite: #{result_file}" if File.exist?(result_file)
verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, true
ctx = ActiveRecord::Base.connection.migration_context
existing_versions = ctx.get_all_versions.to_set
pending_migrations = ctx.migrations.reject do |migration|
existing_versions.include?(migration.version)
end
instrumentation = Gitlab::Database::Migrations::Instrumentation.new
pending_migrations.each do |migration|
instrumentation.observe(migration.version) do
ActiveRecord::Migrator.new(:up, ctx.migrations, ctx.schema_migration, migration.version).run
end
end
ensure
if instrumentation
File.open(result_file, 'wb+') do |io|
io << instrumentation.observations.to_json
end
end
ActiveRecord::Base.clear_cache!
ActiveRecord::Migration.verbose = verbose_was
end
end
end

View file

@ -12929,7 +12929,7 @@ msgstr ""
msgid "Free Trial"
msgstr ""
msgid "Free Trial of GitLab.com Gold"
msgid "Free Trial of GitLab.com Ultimate"
msgstr ""
msgid "Freeze end"

View file

@ -1,6 +1,10 @@
import { getJSONFixture } from 'helpers/fixtures';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
import {
iconForTestStatus,
formatFilePath,
formattedTime,
} from '~/pipelines/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const defaultState = {
blobPath: '/test/blob/path',
testReports,
selectedSuiteIndex: 0,
pageInfo: {
@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
};
const emptyState = {
blobPath: '',
testReports: {},
selectedSuite: null,
pageInfo: {
@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
const expected = testReports.test_suites[0].test_cases
.map((x) => ({
...x,
filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
formattedTime: formattedTime(x.execution_time),
icon: iconForTestStatus(x.status),
}))

View file

@ -1,6 +1,20 @@
import { formattedTime } from '~/pipelines/stores/test_reports/utils';
import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Test reports utils', () => {
describe('formatFilePath', () => {
it.each`
file | expected
${'./test.js'} | ${'test.js'}
${'/test.js'} | ${'test.js'}
${'.//////////////test.js'} | ${'test.js'}
${'test.js'} | ${'test.js'}
${'mock/path./test.js'} | ${'mock/path./test.js'}
${'./mock/path./test.js'} | ${'mock/path./test.js'}
`('should format $file to be $expected', ({ file, expected }) => {
expect(formatFilePath(file)).toBe(expected);
});
});
describe('formattedTime', () => {
describe('when time is smaller than a second', () => {
it('should return time in milliseconds fixed to 2 decimals', () => {

View file

@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
import { TestStatus } from '~/pipelines/constants';
import skippedTestCases from './mock_data';
@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
const testCases = testSuite.test_cases;
const blobPath = '/test/blob/path';
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
const allCaseRows = () => wrapper.findAll('.js-case-row');
const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
const findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite, perPage = 20) => {
store = new Vuex.Store({
state: {
blobPath,
testReports: {
test_suites: [suite],
},
@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
it('renders the file name for the test with a copy button', () => {
const { file } = testCases[0];
const relativeFile = formatFilePath(file);
const filePath = `${blobPath}/${relativeFile}`;
const row = findCaseRowAtIndex(0);
const fileLink = findLinkForRow(row);
const button = row.find(GlButton);
expect(fileLink.attributes('href')).toBe(filePath);
expect(row.text()).toContain(file);
expect(button.exists()).toBe(true);
expect(button.attributes('data-clipboard-text')).toBe(file);

View file

@ -2,6 +2,7 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SearchSettings from '~/search_settings/components/search_settings.vue';
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
describe('search_settings/components/search_settings.vue', () => {
const ROOT_ID = 'content-body';
@ -9,6 +10,8 @@ describe('search_settings/components/search_settings.vue', () => {
const SEARCH_TERM = 'Delete project';
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
let wrapper;
const buildWrapper = () => {
@ -16,10 +19,15 @@ describe('search_settings/components/search_settings.vue', () => {
propsData: {
searchRoot: document.querySelector(`#${ROOT_ID}`),
sectionSelector: SECTION_SELECTOR,
isExpandedFn: isExpanded,
},
// Add real listeners so we can simplify and strengthen some tests.
listeners: {
expand: expandSection,
collapse: closeSection,
},
});
};
const sections = () => Array.from(document.querySelectorAll(SECTION_SELECTOR));
const sectionsCount = () => sections().length;
const visibleSectionsCount = () =>
@ -39,7 +47,10 @@ describe('search_settings/components/search_settings.vue', () => {
<section id="${GENERAL_SETTINGS_ID}" class="settings">
<span>General</span>
</section>
<section id="${ADVANCED_SETTINGS_ID}" class="settings">
<section id="${ADVANCED_SETTINGS_ID}" class="settings expanded">
<span>Advanced</span>
</section>
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
</section>
</div>
@ -52,17 +63,6 @@ describe('search_settings/components/search_settings.vue', () => {
wrapper.destroy();
});
it('expands first section and collapses the rest', () => {
clearSearch();
const [firstSection, ...otherSections] = sections();
expect(wrapper.emitted()).toEqual({
expand: [[firstSection]],
collapse: otherSections.map((x) => [x]),
});
});
it('hides sections that do not match the search term', () => {
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
search(SEARCH_TERM);
@ -72,12 +72,11 @@ describe('search_settings/components/search_settings.vue', () => {
});
it('expands section that matches the search term', () => {
const section = document.querySelector(`#${ADVANCED_SETTINGS_ID}`);
const section = document.querySelector(`#${EXTRA_SETTINGS_ID}`);
search(SEARCH_TERM);
// Last called because expand is always called once to reset the page state
expect(wrapper.emitted().expand[1][0]).toBe(section);
expect(wrapper.emitted('expand')).toEqual([[section]]);
});
it('highlight elements that match the search term', () => {
@ -86,21 +85,64 @@ describe('search_settings/components/search_settings.vue', () => {
expect(highlightedElementsCount()).toBe(1);
});
describe('when search term is cleared', () => {
beforeEach(() => {
search(SEARCH_TERM);
describe('default', () => {
it('test setup starts with expansion state', () => {
expect(sections().map(isExpanded)).toEqual([false, true, false]);
});
it('displays all sections', () => {
expect(visibleSectionsCount()).toBe(1);
clearSearch();
expect(visibleSectionsCount()).toBe(sectionsCount());
});
describe('when searched and cleared', () => {
beforeEach(() => {
search('Test');
clearSearch();
});
it('removes the highlight from all elements', () => {
expect(highlightedElementsCount()).toBe(1);
clearSearch();
expect(highlightedElementsCount()).toBe(0);
it('displays all sections', () => {
expect(visibleSectionsCount()).toBe(sectionsCount());
});
it('removes the highlight from all elements', () => {
expect(highlightedElementsCount()).toBe(0);
});
it('should preserve original expansion state', () => {
expect(sections().map(isExpanded)).toEqual([false, true, false]);
});
it('should preserve state by emitting events', () => {
const [first, mid, last] = sections();
expect(wrapper.emitted()).toEqual({
expand: [[mid]],
collapse: [[first], [last]],
});
});
describe('after multiple searches and clear', () => {
beforeEach(() => {
search('Test');
search(SEARCH_TERM);
clearSearch();
});
it('should preserve last expansion state', () => {
expect(sections().map(isExpanded)).toEqual([false, true, false]);
});
});
describe('after user expands and collapses, search, and clear', () => {
beforeEach(() => {
const [first, mid] = sections();
closeSection(mid);
expandSection(first);
search(SEARCH_TERM);
clearSearch();
});
it('should preserve last expansion state', () => {
expect(sections().map(isExpanded)).toEqual([true, false, false]);
});
});
});
});
});

View file

@ -1,4 +1,3 @@
import $ from 'jquery';
import { setHTMLFixture } from 'helpers/fixtures';
import mount from '~/search_settings/mount';
import { expandSection, closeSection } from '~/settings_panels';
@ -24,13 +23,13 @@ describe('search_settings/mount', () => {
const section = { name: 'section' };
app.$refs.searchSettings.$emit('expand', section);
expect(expandSection).toHaveBeenCalledWith($(section));
expect(expandSection).toHaveBeenCalledWith(section);
});
it('calls settings_panel.closeSection when collapse event is emitted', () => {
const section = { name: 'section' };
app.$refs.searchSettings.$emit('collapse', section);
expect(closeSection).toHaveBeenCalledWith($(section));
expect(closeSection).toHaveBeenCalledWith(section);
});
});

View file

@ -1,5 +1,5 @@
import $ from 'jquery';
import initSettingsPanels from '~/settings_panels';
import initSettingsPanels, { isExpanded } from '~/settings_panels';
describe('Settings Panels', () => {
preloadFixtures('groups/edit.html');
@ -20,11 +20,11 @@ describe('Settings Panels', () => {
// Our test environment automatically expands everything so we need to clear that out first
panel.classList.remove('expanded');
expect(panel.classList.contains('expanded')).toBe(false);
expect(isExpanded(panel)).toBe(false);
initSettingsPanels();
expect(panel.classList.contains('expanded')).toBe(true);
expect(isExpanded(panel)).toBe(true);
});
});
@ -35,11 +35,11 @@ describe('Settings Panels', () => {
initSettingsPanels();
expect(panel.classList.contains('expanded')).toBe(true);
expect(isExpanded(panel)).toBe(true);
$(trigger).click();
expect(panel.classList.contains('expanded')).toBe(false);
expect(isExpanded(panel)).toBe(false);
expect(trigger.textContent).toEqual(originalText);
});
});

View file

@ -0,0 +1,76 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Instrumentation do
describe '#observe' do
subject { described_class.new }
let(:migration) { 1234 }
it 'executes the given block' do
expect { |b| subject.observe(migration, &b) }.to yield_control
end
context 'on successful execution' do
subject { described_class.new.observe(migration) {} }
it 'records walltime' do
expect(subject.walltime).not_to be_nil
end
it 'records success' do
expect(subject.success).to be_truthy
end
it 'records the migration version' do
expect(subject.migration).to eq(migration)
end
end
context 'upon failure' do
subject { described_class.new.observe(migration) { raise 'something went wrong' } }
it 'raises the exception' do
expect { subject }.to raise_error(/something went wrong/)
end
context 'retrieving observations' do
subject { instance.observations.first }
before do
instance.observe(migration) { raise 'something went wrong' }
rescue
# ignore
end
let(:instance) { described_class.new }
it 'records walltime' do
expect(subject.walltime).not_to be_nil
end
it 'records failure' do
expect(subject.success).to be_falsey
end
it 'records the migration version' do
expect(subject.migration).to eq(migration)
end
end
end
context 'sequence of migrations with failures' do
subject { described_class.new }
let(:migration1) { double('migration1', call: nil) }
let(:migration2) { double('migration2', call: nil) }
it 'records observations for all migrations' do
subject.observe('migration1') {}
subject.observe('migration2') { raise 'something went wrong' } rescue nil
expect(subject.observations.size).to eq(2)
end
end
end
end

View file

@ -297,6 +297,57 @@ RSpec.describe 'gitlab:db namespace rake task' do
end
end
describe '#migrate_with_instrumentation' do
subject { run_rake_task('gitlab:db:migration_testing', "[#{filename}]") }
let(:ctx) { double('ctx', migrations: all_migrations, schema_migration: double, get_all_versions: existing_versions) }
let(:instrumentation) { instance_double(Gitlab::Database::Migrations::Instrumentation, observations: observations) }
let(:existing_versions) { [1] }
let(:all_migrations) { [double('migration1', version: 1), pending_migration] }
let(:pending_migration) { double('migration2', version: 2) }
let(:filename) { 'results-file.json'}
let(:buffer) { StringIO.new }
let(:observations) { %w[some data] }
before do
allow(ActiveRecord::Base.connection).to receive(:migration_context).and_return(ctx)
allow(Gitlab::Database::Migrations::Instrumentation).to receive(:new).and_return(instrumentation)
allow(ActiveRecord::Migrator).to receive_message_chain('new.run').with(any_args).with(no_args)
allow(instrumentation).to receive(:observe).and_yield
allow(File).to receive(:open).with(filename, 'wb+').and_yield(buffer)
end
it 'fails when given no filename argument' do
expect { run_rake_task('gitlab:db:migration_testing') }.to raise_error(/specify result_file/)
end
it 'fails when the given file already exists' do
expect(File).to receive(:exist?).with(filename).and_return(true)
expect { subject }.to raise_error(/File exists/)
end
it 'instruments the pending migration' do
expect(instrumentation).to receive(:observe).with(2).and_yield
subject
end
it 'executes the pending migration' do
expect(ActiveRecord::Migrator).to receive_message_chain('new.run').with(:up, ctx.migrations, ctx.schema_migration, pending_migration.version).with(no_args)
subject
end
it 'writes observations out to JSON file' do
subject
expect(buffer.string).to eq(observations.to_json)
end
end
def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}")

View file

@ -92,7 +92,8 @@ RSpec.describe 'layouts/_head' do
before do
stub_config(extra: {
matomo_url: matomo_host,
matomo_site_id: 12345
matomo_site_id: 12345,
matomo_disable_cookies: false
})
end
@ -101,43 +102,18 @@ RSpec.describe 'layouts/_head' do
expect(rendered).to match(/<script.*>.*var u="\/\/#{matomo_host}\/".*<\/script>/m)
expect(rendered).to match(%r(<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>))
expect(rendered).not_to include('_paq.push(["disableCookies"])')
end
context 'matomo_disable_cookies' do
context 'when true' do
before do
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: true })
end
it 'disables cookies' do
render
expect(rendered).to include('_paq.push(["disableCookies"])')
end
context 'when matomo_disable_cookies is true' do
before do
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: true })
end
context 'when false' do
before do
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: false })
end
it 'disables cookies' do
render
it 'does not disable cookies' do
render
expect(rendered).not_to include('_paq.push(["disableCookies"])')
end
end
context 'when absent' do
before do
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345 })
end
it 'does not disable cookies' do
render
expect(rendered).not_to include('_paq.push(["disableCookies"])')
end
expect(rendered).to include('_paq.push(["disableCookies"])')
end
end
end