Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-06 12:09:00 +00:00
parent c203c40cda
commit 9fc7cdf0b7
34 changed files with 736 additions and 88 deletions

View file

@ -1,6 +1,7 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
@ -24,7 +25,7 @@ import {
expandedPanelPayloadFromUrl,
convertVariablesForURL,
} from '../utils';
import { metricStates } from '../constants';
import { metricStates, keyboardShortcutKeys } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
export default {
@ -149,6 +150,7 @@ export default {
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
isRearrangingPanels: false,
originalDocumentTitle: document.title,
hoveredPanel: '',
};
},
computed: {
@ -214,9 +216,13 @@ export default {
},
created() {
window.addEventListener('keyup', this.onKeyup);
Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut);
},
destroyed() {
window.removeEventListener('keyup', this.onKeyup);
Mousetrap.unbind(Object.values(keyboardShortcutKeys));
},
mounted() {
if (!this.hasMetrics) {
@ -326,6 +332,56 @@ export default {
return isNumberOfPanelsEven || !isLastPanel;
},
/**
* TODO: Investigate this to utilize the eventBus from Vue
* The intentation behind this cleanup is to allow for better tests
* as well as use the correct eventBus facilities that are compatible
* with Vue 3
* https://gitlab.com/gitlab-org/gitlab/-/issues/225583
*/
//
runShortcut(e) {
const panel = this.$refs[this.hoveredPanel];
if (!panel) return;
const [panelInstance] = panel;
let actionToRun = '';
switch (e.key) {
case keyboardShortcutKeys.EXPAND:
actionToRun = 'onExpandFromKeyboardShortcut';
break;
case keyboardShortcutKeys.VISIT_LOGS:
actionToRun = 'visitLogsPageFromKeyboardShortcut';
break;
case keyboardShortcutKeys.SHOW_ALERT:
actionToRun = 'showAlertModalFromKeyboardShortcut';
break;
case keyboardShortcutKeys.DOWNLOAD_CSV:
actionToRun = 'downloadCsvFromKeyboardShortcut';
break;
case keyboardShortcutKeys.CHART_COPY:
actionToRun = 'copyChartLinkFromKeyboardShotcut';
break;
default:
actionToRun = 'onExpandFromKeyboardShortcut';
break;
}
panelInstance[actionToRun]();
},
setHoveredPanel(groupKey, graphIndex) {
this.hoveredPanel = `dashboard-panel-${groupKey}-${graphIndex}`;
},
clearHoveredPanel() {
this.hoveredPanel = '';
},
},
i18n: {
goBackLabel: s__('Metrics|Go back (Esc)'),
@ -407,6 +463,8 @@ export default {
'draggable-enabled': isRearrangingPanels,
'col-lg-6': isPanelHalfWidth(graphIndex, groupData.panels.length),
}"
@mouseover="setHoveredPanel(groupData.key, graphIndex)"
@mouseout="clearHoveredPanel"
>
<div class="position-relative draggable-panel js-draggable-panel">
<div
@ -420,6 +478,7 @@ export default {
</div>
<dashboard-panel
:ref="`dashboard-panel-${groupData.key}-${graphIndex}`"
:settings-path="settingsPath"
:clipboard-text="generatePanelUrl(groupData.group, graphData)"
:graph-data="graphData"

View file

@ -2,6 +2,7 @@
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import {
GlResizeObserverDirective,
GlIcon,
@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { isSafeURL } from '~/lib/utils/url_utility';
const events = {
timeRangeZoom: 'timerangezoom',
@ -244,6 +244,9 @@ export default {
this.hasMetricsInDb
);
},
alertModalId() {
return `alert-modal-${this.graphData.id}`;
},
},
mounted() {
this.refreshTitleTooltip();
@ -282,6 +285,11 @@ export default {
onExpand() {
this.$emit(events.expand);
},
onExpandFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.onExpand();
}
},
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
@ -292,6 +300,34 @@ export default {
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
showAlertModal() {
this.$root.$emit('bv::show::modal', this.alertModalId);
},
showAlertModalFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.showAlertModal();
}
},
visitLogsPage() {
if (this.logsPathWithTimeRange) {
visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
}
},
visitLogsPageFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.visitLogsPage();
}
},
downloadCsvFromKeyboardShortcut() {
if (this.csvText && this.isContextualMenuShown) {
this.$refs.downloadCsvLink.$el.firstChild.click();
}
},
copyChartLinkFromKeyboardShotcut() {
if (this.clipboardText && this.isContextualMenuShown) {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
},
panelTypes,
};
@ -313,7 +349,7 @@ export default {
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
:modal-id="`alert-modal-${graphData.id}`"
:modal-id="alertModalId"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@ -328,7 +364,7 @@ export default {
ref="contextualMenu"
data-qa-selector="prometheus_graph_widgets"
>
<div class="d-flex align-items-center">
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
<gl-dropdown
v-gl-tooltip
toggle-class="shadow-none border-0"
@ -383,7 +419,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${graphData.id}`"
v-gl-modal="alertModalId"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}

View file

@ -251,3 +251,17 @@ export const VARIABLE_TYPES = {
* before passing the data to the backend.
*/
export const VARIABLE_PREFIX = 'var-';
/**
* All of the actions inside each panel dropdown can be accessed
* via keyboard shortcuts than can be activated via mouse hovers
* and or focus via tabs.
*/
export const keyboardShortcutKeys = {
EXPAND: 'e',
VISIT_LOGS: 'l',
SHOW_ALERT: 'a',
DOWNLOAD_CSV: 'd',
CHART_COPY: 'c',
};

View file

@ -27,7 +27,7 @@ export default {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
return this.author.avatarUrl || this.author.avatar_url;
return this.author.avatarUrl || this.author.avatar_url || gl.mrWidgetData.defaultAvatarUrl;
},
},
};
@ -40,6 +40,6 @@ export default {
class="author-link inline"
>
<img :src="avatarUrl" class="avatar avatar-inline s16" />
<span v-if="showAuthorName" class="author"> {{ author.name }} </span>
<span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a>
</template>

View file

@ -8,6 +8,7 @@ export default () => {
if (gl.mrWidget) return;
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url;
const vm = new Vue(MrWidgetOptions);

View file

@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base
include Impersonation
include Gitlab::Logging::CloudflareHelper
include Gitlab::Utils::StrongMemoize
include ControllerWithFeatureCategory
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
module ControllerWithFeatureCategory
extend ActiveSupport::Concern
include Gitlab::ClassAttributes
class_methods do
def feature_category(category, config = {})
validate_config!(config)
category_config = Config.new(category, config[:only], config[:except], config[:if], config[:unless])
# Add the config to the beginning. That way, the last defined one takes precedence.
feature_category_configuration.unshift(category_config)
end
def feature_category_for_action(action)
category_config = feature_category_configuration.find { |config| config.matches?(action) }
category_config&.category || superclass_feature_category_for_action(action)
end
private
def validate_config!(config)
invalid_keys = config.keys - [:only, :except, :if, :unless]
if invalid_keys.any?
raise ArgumentError, "unknown arguments: #{invalid_keys} "
end
if config.key?(:only) && config.key?(:except)
raise ArgumentError, "cannot configure both `only` and `except`"
end
end
def feature_category_configuration
class_attributes[:feature_category_config] ||= []
end
def superclass_feature_category_for_action(action)
return unless superclass.respond_to?(:feature_category_for_action)
superclass.feature_category_for_action(action)
end
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module ControllerWithFeatureCategory
class Config
attr_reader :category
def initialize(category, only, except, if_proc, unless_proc)
@category = category.to_sym
@only, @except = only&.map(&:to_s), except&.map(&:to_s)
@if_proc, @unless_proc = if_proc, unless_proc
end
def matches?(action)
included?(action) && !excluded?(action) &&
if_proc?(action) && !unless_proc?(action)
end
private
attr_reader :only, :except, :if_proc, :unless_proc
def if_proc?(action)
if_proc.nil? || if_proc.call(action)
end
def unless_proc?(action)
unless_proc.present? && unless_proc.call(action)
end
def included?(action)
only.nil? || only.include?(action)
end
def excluded?(action)
except.present? && except.include?(action)
end
end
end

View file

@ -45,6 +45,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
feature_category :source_code_management,
unless: -> (action) { action.ends_with?("_reports") }
feature_category :code_testing,
only: [:test_reports, :coverage_reports, :terraform_reports]
feature_category :accessibility_testing,
only: [:accessibility_reports]
def index
@merge_requests = @issuables

View file

@ -18,6 +18,7 @@ module AlertManagement
return error_invalid_status unless status_key
if alert.update(status_event: status_event)
resolve_todos if resolved?
success
else
error(alert.errors.full_messages.to_sentence)
@ -28,12 +29,16 @@ module AlertManagement
attr_reader :alert, :user, :status
delegate :project, to: :alert
delegate :project, :resolved?, to: :alert
def allowed?
user.can?(:update_alert_management_alert, project)
end
def resolve_todos
TodoService.new.resolve_todos_for_target(alert, user)
end
def status_key
strong_memoize(:status_key) do
AlertManagement::Alert::STATUSES.key(status)

View file

@ -2,6 +2,7 @@
module WorkerAttributes
extend ActiveSupport::Concern
include Gitlab::ClassAttributes
# Resource boundaries that workers can declare through the
# `resource_boundary` attribute
@ -30,24 +31,24 @@ module WorkerAttributes
}.stringify_keys.freeze
class_methods do
def feature_category(value)
def feature_category(value, *extras)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
worker_attributes[:feature_category] = value
class_attributes[:feature_category] = value
end
# Special case: mark this work as not associated with a feature category
# this should be used for cross-cutting concerns, such as mailer workers.
def feature_category_not_owned!
worker_attributes[:feature_category] = :not_owned
class_attributes[:feature_category] = :not_owned
end
def get_feature_category
get_worker_attribute(:feature_category)
get_class_attribute(:feature_category)
end
def feature_category_not_owned?
get_worker_attribute(:feature_category) == :not_owned
get_feature_category == :not_owned
end
# This should be set to :high for jobs that need to be run
@ -61,11 +62,11 @@ module WorkerAttributes
def urgency(urgency)
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
worker_attributes[:urgency] = urgency
class_attributes[:urgency] = urgency
end
def get_urgency
worker_attributes[:urgency] || :low
class_attributes[:urgency] || :low
end
# Set this attribute on a job when it will call to services outside of the
@ -73,85 +74,64 @@ module WorkerAttributes
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
# details
def worker_has_external_dependencies!
worker_attributes[:external_dependencies] = true
class_attributes[:external_dependencies] = true
end
# Returns a truthy value if the worker has external dependencies.
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
# for details
def worker_has_external_dependencies?
worker_attributes[:external_dependencies]
class_attributes[:external_dependencies]
end
def worker_resource_boundary(boundary)
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
worker_attributes[:resource_boundary] = boundary
class_attributes[:resource_boundary] = boundary
end
def get_worker_resource_boundary
worker_attributes[:resource_boundary] || :unknown
class_attributes[:resource_boundary] || :unknown
end
def idempotent!
worker_attributes[:idempotent] = true
class_attributes[:idempotent] = true
end
def idempotent?
worker_attributes[:idempotent]
class_attributes[:idempotent]
end
def weight(value)
worker_attributes[:weight] = value
class_attributes[:weight] = value
end
def get_weight
worker_attributes[:weight] ||
class_attributes[:weight] ||
NAMESPACE_WEIGHTS[queue_namespace] ||
1
end
def tags(*values)
worker_attributes[:tags] = values
class_attributes[:tags] = values
end
def get_tags
Array(worker_attributes[:tags])
Array(class_attributes[:tags])
end
def deduplicate(strategy, options = {})
worker_attributes[:deduplication_strategy] = strategy
worker_attributes[:deduplication_options] = options
class_attributes[:deduplication_strategy] = strategy
class_attributes[:deduplication_options] = options
end
def get_deduplicate_strategy
worker_attributes[:deduplication_strategy] ||
class_attributes[:deduplication_strategy] ||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
end
def get_deduplication_options
worker_attributes[:deduplication_options] || {}
end
protected
# Returns a worker attribute declared on this class or its parent class.
# This approach allows declared attributes to be inherited by
# child classes.
def get_worker_attribute(name)
worker_attributes[name] || superclass_worker_attributes(name)
end
private
def worker_attributes
@attributes ||= {}
end
def superclass_worker_attributes(name)
return unless superclass.include? WorkerAttributes
superclass.get_worker_attribute(name)
class_attributes[:deduplication_options] || {}
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Resolve user's todo when an alert is resolved
merge_request: 35700
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Fix missing avatar in MR widget
merge_request: 36034
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add keyboard shortcuts to metrics dashboard
merge_request: 32804
author:
type: added

View file

@ -16,6 +16,28 @@ This section contains a journal of recent validation tests and links to the rele
The following are GitLab upgrade validation tests we performed.
### June 2020
[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/223284):
- Description: Tested upgrading from GitLab 12.9.10 to 12.10.12 package in a multi-server
configuration. Monitored for downtime using the looping pipeline and HAProxy stats dashboards.
- Outcome: Partial success because we observed downtime during the upgrade of the primary and secondary sites.
- Follow up issues/actions:
- [Fix zero-downtime upgrade process/instructions for multi-node Geo deployments](https://gitlab.com/gitlab-org/gitlab/-/issues/225684)
- [Geo:check Rake task: Exclude AuthorizedKeysCommand check if node not running Puma/Unicorn](https://gitlab.com/gitlab-org/gitlab/-/issues/225454)
- [Update instructions in the next upgrade issue to include monitoring HAProxy dashboards](https://gitlab.com/gitlab-org/gitlab/-/issues/225359)
[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/208104):
- Description: Tested upgrading from GitLab 12.8.1 to 12.9.10 package in a multi-server
configuration.
- Outcome: Partial success because we did not run the looping pipeline during the demo to validate
zero-downtime.
- Follow up issues:
- [Clarify hup Puma/Unicorn should include deploy node](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5460)
- [Investigate MR creation failure after upgrade to 12.9.10](https://gitlab.com/gitlab-org/gitlab/-/issues/223282) Closed as false positive.
### February 2020
[Upgrade Geo multi-node installation](https://gitlab.com/gitlab-org/gitlab/-/issues/201837):

View file

@ -11799,6 +11799,11 @@ type SecurityReportSummarySection {
"""
scannedResourcesCount: Int
"""
Path to download all the scanned resources in CSV format
"""
scannedResourcesCsvPath: String
"""
Total number of vulnerabilities
"""

View file

@ -34498,6 +34498,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scannedResourcesCsvPath",
"description": "Path to download all the scanned resources in CSV format",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitiesCount",
"description": "Total number of vulnerabilities",

View file

@ -1701,6 +1701,7 @@ Represents a section of a summary of a security report
| Name | Type | Description |
| --- | ---- | ---------- |
| `scannedResourcesCount` | Int | Total number of scanned resources |
| `scannedResourcesCsvPath` | String | Path to download all the scanned resources in CSV format |
| `vulnerabilitiesCount` | Int | Total number of vulnerabilities |
## SentryDetailedError

View file

@ -33,7 +33,7 @@ Kubernetes-specific environment variables are detailed in the
| `CI` | all | 0.4 | Mark that job is executed in CI environment |
| `CI_API_V4_URL` | 11.7 | all | The GitLab API v4 root URL |
| `CI_BUILDS_DIR` | all | 11.10 | Top-level directory where builds are executed. |
| `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch before a merge request. Only populated when there is a merge request associated with the pipeline. |
| `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch. Is always `0000000000000000000000000000000000000000` in pipelines for merge requests. |
| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
| `CI_COMMIT_REF_NAME` | 9.0 | all | The branch or tag name for which project is built |
@ -71,8 +71,8 @@ Kubernetes-specific environment variables are detailed in the
| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is available. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) |
| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of username(s) of assignee(s) for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_ID` | 11.6 | all | The ID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_IID` | 11.6 | all | The IID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_ID` | 11.6 | all | The project-level ID of the merge request. Only available if [the pipelines are for merge requests](../merge_request_pipelines/index.md) and the merge request is created. |
| `CI_MERGE_REQUEST_IID` | 11.6 | all | The instance-level IID of the merge request. Only available If [the pipelines are for merge requests](../merge_request_pipelines/index.md) and the merge request is created. |
| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_PROJECT_ID` | 11.6 | all | The ID of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
@ -94,7 +94,7 @@ Kubernetes-specific environment variables are detailed in the
| `CI_PAGES_URL` | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
| `CI_PIPELINE_ID` | 8.10 | all | The unique ID of the current pipeline that GitLab CI/CD uses internally |
| `CI_PIPELINE_IID` | 11.0 | all | The unique ID of the current pipeline scoped to project |
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, `external`, `pipeline` (renamed to `cross_project_pipeline` since 13.0), `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, and `parent_pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `schedule`, `api`, `external`, `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, `parent_pipeline`, [`trigger`, or `pipeline`](../triggers/README.md#authentication-tokens) (renamed to `cross_project_pipeline` since 13.0). For pipelines created before GitLab 9.5, this will show as `unknown`. |
| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |

View file

@ -1341,7 +1341,7 @@ docker build:
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- changes:
- Dockerfile
- Dockerfile
when: manual
allow_failure: true
```
@ -1367,7 +1367,7 @@ job:
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- exists:
- Dockerfile
- Dockerfile
```
You can also use glob patterns to match multiple files in any directory within
@ -1380,7 +1380,7 @@ job:
script: bundle exec rspec
rules:
- exists:
- spec/**.rb
- spec/**.rb
```
NOTE: **Note:**
@ -1427,8 +1427,8 @@ docker build:
rules:
- if: '$VAR == "string value"'
changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
- Dockerfile
- docker/scripts/*
- Dockerfile
- docker/scripts/*
when: manual
# - when: never would be redundant here, this is implied any time rules are listed.
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -44,10 +44,10 @@ see the details and the URL(s) affected.
![DAST Widget Clicked](img/dast_single_v13_0.png)
[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing)
uses the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
uses the popular open source tool [OWASP Zed Attack Proxy](https://www.zaproxy.org/)
to perform an analysis on your running web application.
By default, DAST executes [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan)
By default, DAST executes [ZAP Baseline Scan](https://www.zaproxy.org/docs/docker/baseline-scan/)
and performs passive scanning only. It won't actively attack your application.
However, DAST can be [configured](#full-scan)
to also perform an *active scan*: attack your application and produce a more extensive security report.
@ -599,6 +599,44 @@ security reports without requiring internet access.
Alternatively, you can use the variable `SECURE_ANALYZERS_PREFIX` to override the base registry address of the `dast` image.
## On-Demand Scans
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218465) in GitLab 13.2.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's able to be enabled or disabled per-project.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-on-demand-scans).
Passive DAST scans may be run on demand against a target website, outside the DevOps lifecycle. These scans will
always be associated with the default or `master` branch of your project and the results can be seen in the project dashboard.
![DAST On-Demand Scan](img/dast_on_demand_v13_2.png)
### Enable or disable On-Demand Scans
On-Demand Scans is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance. On-Demand Scans can be enabled or disabled per-project
To enable it:
```ruby
# Instance-wide
Feature.enable(:security_on_demand_scans_feature_flag)
# or by project
Feature.enable(:security_on_demand_scans_feature_flag, Project.find(<project id>))
```
To disable it:
```ruby
# Instance-wide
Feature.disable(:security_on_demand_scans_feature_flag)
# or by project
Feature.disable(:security_on_demand_scans_feature_flag, Project.find(<project id>))
```
## Reports
The DAST tool outputs a report file in JSON format by default. However, this tool can also generate reports in

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Gitlab
module ClassAttributes
extend ActiveSupport::Concern
class_methods do
protected
# Returns an attribute declared on this class or its parent class.
# This approach allows declared attributes to be inherited by
# child classes.
def get_class_attribute(name)
class_attributes[name] || superclass_attributes(name)
end
private
def class_attributes
@class_attributes ||= {}
end
def superclass_attributes(name)
return unless superclass.include? Gitlab::ClassAttributes
superclass.get_class_attribute(name)
end
end
end
end

View file

@ -32,6 +32,10 @@ module Gitlab
action = "#{controller.action_name}"
# Try to get the feature category, but don't fail when the controller is
# not an ApplicationController.
feature_category = controller.class.try(:feature_category_for_action, action).to_s
# Devise exposes a method called "request_format" that does the below.
# However, this method is not available to all controllers (e.g. certain
# Doorkeeper controllers). As such we use the underlying code directly.
@ -45,7 +49,7 @@ module Gitlab
action = "#{action}.#{suffix}"
end
{ controller: controller.class.name, action: action }
{ controller: controller.class.name, action: action, feature_category: feature_category }
end
def labels_from_endpoint

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
require "fast_spec_helper"
require "rspec-parameterized"
require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config"
RSpec.describe ControllerWithFeatureCategory::Config do
describe "#matches?" do
using RSpec::Parameterized::TableSyntax
where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do
nil | nil | nil | nil | "action" | true
[:included] | nil | nil | nil | "action" | false
[:included] | nil | nil | nil | "included" | true
nil | [:excluded] | nil | nil | "excluded" | false
nil | nil | true | nil | "action" | true
[:included] | nil | true | nil | "action" | false
[:included] | nil | true | nil | "included" | true
nil | [:excluded] | true | nil | "excluded" | false
nil | nil | false | nil | "action" | false
[:included] | nil | false | nil | "action" | false
[:included] | nil | false | nil | "included" | false
nil | [:excluded] | false | nil | "excluded" | false
nil | nil | nil | true | "action" | false
[:included] | nil | nil | true | "action" | false
[:included] | nil | nil | true | "included" | false
nil | [:excluded] | nil | true | "excluded" | false
nil | nil | nil | false | "action" | true
[:included] | nil | nil | false | "action" | false
[:included] | nil | nil | false | "included" | true
nil | [:excluded] | nil | false | "excluded" | false
nil | nil | true | false | "action" | true
[:included] | nil | true | false | "action" | false
[:included] | nil | true | false | "included" | true
nil | [:excluded] | true | false | "excluded" | false
nil | nil | false | true | "action" | false
[:included] | nil | false | true | "action" | false
[:included] | nil | false | true | "included" | false
nil | [:excluded] | false | true | "excluded" | false
end
with_them do
let(:config) do
if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc }
unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc }
described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc)
end
specify { expect(config.matches?(test_action)).to be(expected) }
end
end
end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative "../../../app/controllers/concerns/controller_with_feature_category"
require_relative "../../../app/controllers/concerns/controller_with_feature_category/config"
RSpec.describe ControllerWithFeatureCategory do
describe ".feature_category_for_action" do
let(:base_controller) do
Class.new do
include ControllerWithFeatureCategory
end
end
let(:controller) do
Class.new(base_controller) do
feature_category :baz
feature_category :foo, except: %w(update edit)
feature_category :bar, only: %w(index show)
feature_category :quux, only: %w(destroy)
feature_category :quuz, only: %w(destroy)
end
end
let(:subclass) do
Class.new(controller) do
feature_category :qux, only: %w(index)
end
end
it "is nil when nothing was defined" do
expect(base_controller.feature_category_for_action("hello")).to be_nil
end
it "returns the expected category", :aggregate_failures do
expect(controller.feature_category_for_action("update")).to eq(:baz)
expect(controller.feature_category_for_action("hello")).to eq(:foo)
expect(controller.feature_category_for_action("index")).to eq(:bar)
end
it "returns the closest match for categories defined in subclasses" do
expect(subclass.feature_category_for_action("index")).to eq(:qux)
expect(subclass.feature_category_for_action("show")).to eq(:bar)
end
it "returns the last defined feature category when multiple match" do
expect(controller.feature_category_for_action("destroy")).to eq(:quuz)
end
it "raises an error when using including and excluding the same action" do
expect do
Class.new(base_controller) do
feature_category :hello, only: [:world], except: [:world]
end
end.to raise_error(%r(cannot configure both `only` and `except`))
end
it "raises an error when using unknown arguments" do
expect do
Class.new(base_controller) do
feature_category :hello, hello: :world
end
end.to raise_error(%r(unknown arguments))
end
end
end

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Every controller" do
context "feature categories" do
let_it_be(:feature_categories) do
YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
end
let_it_be(:controller_actions) do
# This will return tuples of all controller actions defined in the routes
# Only for controllers inheriting ApplicationController
# Excluding controllers from gems (OAuth, Sidekiq)
Rails.application.routes.routes
.map { |route| route.required_defaults.presence }
.compact
.select { |route| route[:controller].present? && route[:action].present? }
.map { |route| [constantize_controller(route[:controller]), route[:action]] }
.reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) }
end
let_it_be(:routes_without_category) do
controller_actions.map do |controller, action|
"#{controller}##{action}" unless controller.feature_category_for_action(action)
end.compact
end
it "has feature categories" do
pending("We'll work on defining categories for all controllers: "\
"https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463")
expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category"
end
it "completed controllers don't get new routes without categories" do
completed_controllers = [Projects::MergeRequestsController].map(&:to_s)
newly_introduced_missing_category = routes_without_category.select do |route|
completed_controllers.any? { |controller| route.start_with?(controller) }
end
expect(newly_introduced_missing_category).to be_empty
end
it "recognizes the feature categories" do
routes_unknown_category = controller_actions.map do |controller, action|
used_category = controller.feature_category_for_action(action)
next unless used_category
next if used_category == :not_owned
["#{controller}##{action}", used_category] unless feature_categories.include?(used_category)
end.compact
expect(routes_unknown_category).to be_empty, "#{routes_unknown_category.first(10)} had an unknown category"
end
it "doesn't define or exclude categories on removed actions", :aggregate_failures do
controller_actions.group_by(&:first).each do |controller, controller_action|
existing_actions = controller_action.map(&:last)
used_actions = actions_defined_in_feature_category_config(controller)
non_existing_used_actions = used_actions - existing_actions
expect(non_existing_used_actions).to be_empty,
"#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist"
end
end
end
def constantize_controller(name)
"#{name.camelize}Controller".constantize
rescue NameError
nil # some controllers, like the omniauth ones are dynamic
end
def actions_defined_in_feature_category_config(controller)
feature_category_configs = controller.send(:class_attributes)[:feature_category_config]
feature_category_configs.map do |config|
Array(config.send(:only)) + Array(config.send(:except))
end.flatten.uniq.map(&:to_s)
end
end

View file

@ -1168,6 +1168,34 @@ describe('Dashboard', () => {
});
});
describe('keyboard shortcuts', () => {
const currentDashboard = dashboardGitResponse[1].path;
const panelRef = 'dashboard-panel-response-metrics-aws-elb-4-1'; // skip expanded panel
// While the recommendation in the documentation is to test
// with a data-testid attribute, I want to make sure that
// the dashboard panels have a ref attribute set.
const getDashboardPanel = () => wrapper.find({ ref: panelRef });
beforeEach(() => {
setupStoreWithData(store);
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard,
});
createShallowWrapper({ hasMetrics: true });
wrapper.setData({ hoveredPanel: panelRef });
return wrapper.vm.$nextTick();
});
it('contains a ref attribute inside a DashboardPanel component', () => {
const dashboardPanel = getDashboardPanel();
expect(dashboardPanel.exists()).toBe(true);
});
});
describe('add custom metrics', () => {
const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' });

View file

@ -1,39 +1,61 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
window.gl = window.gl || {};
describe('MrWidgetAuthor', () => {
let vm;
let wrapper;
let oldWindowGl;
const mockAuthor = {
name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
beforeEach(() => {
const Component = Vue.extend(MrWidgetAuthor);
vm = mountComponent(Component, {
author: {
name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl:
'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
oldWindowGl = window.gl;
window.gl = {
mrWidgetData: {
defaultAvatarUrl: 'no_avatar.png',
},
};
wrapper = shallowMount(MrWidgetAuthor, {
propsData: {
author: mockAuthor,
},
});
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
window.gl = oldWindowGl;
});
it('renders link with the author web url', () => {
expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root');
expect(wrapper.attributes('href')).toBe('http://localhost:3000/root');
});
it('renders image with avatar url', () => {
expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(
expect(wrapper.find('img').attributes('src')).toBe(
'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
);
});
it('renders image with default avatar url when no avatarUrl is present in author', async () => {
wrapper.setProps({
author: {
...mockAuthor,
avatarUrl: null,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find('img').attributes('src')).toBe('no_avatar.png');
});
it('renders author name', () => {
expect(vm.$el.textContent.trim()).toEqual('Administrator');
expect(wrapper.find('span').text()).toBe('Administrator');
});
});

View file

@ -8,6 +8,7 @@ import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
describe('MRWidgetAutoMergeEnabled', () => {
let vm;
let oldWindowGl;
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
@ -16,6 +17,13 @@ describe('MRWidgetAutoMergeEnabled', () => {
const Component = Vue.extend(autoMergeEnabledComponent);
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
oldWindowGl = window.gl;
window.gl = {
mrWidgetData: {
defaultAvatarUrl: 'no_avatar.png',
},
};
vm = mountComponent(Component, {
mr: {
shouldRemoveSourceBranch: false,
@ -35,6 +43,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
afterEach(() => {
vm.$destroy();
window.gl = oldWindowGl;
});
describe('computed', () => {

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::ClassAttributes do
let(:klass) do
Class.new do
include Gitlab::ClassAttributes
def self.get_attribute(name)
get_class_attribute(name)
end
def self.set_attribute(name, value)
class_attributes[name] = value
end
end
end
let(:subclass) { Class.new(klass) }
describe ".get_class_attribute" do
it "returns values set on the class" do
klass.set_attribute(:foo, :bar)
expect(klass.get_attribute(:foo)).to eq(:bar)
end
it "returns values set on a superclass" do
klass.set_attribute(:foo, :bar)
expect(subclass.get_attribute(:foo)).to eq(:bar)
end
it "returns values from the subclass over attributes from a superclass" do
klass.set_attribute(:foo, :baz)
subclass.set_attribute(:foo, :bar)
expect(subclass.get_attribute(:foo)).to eq(:bar)
end
end
end

View file

@ -93,23 +93,23 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
context 'when request goes to ActionController' do
let(:request) { double(:request, format: double(:format, ref: :html)) }
let(:controller_class) { double(:controller_class, name: 'TestController') }
before do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show', request: request)
controller = double(:controller, class: controller_class, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
it 'tags a transaction with the name and action of a controller' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
end
context 'when the request content type is not :html' do
let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: '' })
end
end
@ -117,7 +117,14 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
it 'does not append the MIME type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
end
end
context 'when the feature category is known' do
it 'includes it in the feature category label' do
expect(controller_class).to receive(:feature_category_for_action).with('show').and_return(:source_code_management)
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: "source_code_management" })
end
end
end

View file

@ -227,7 +227,8 @@ RSpec.describe API::Repositories do
end
describe "GET /projects/:id/repository/archive(.:format)?:sha" do
let(:route) { "/projects/#{project.id}/repository/archive" }
let(:project_id) { CGI.escape(project.full_path) }
let(:route) { "/projects/#{project_id}/repository/archive" }
before do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
@ -246,7 +247,7 @@ RSpec.describe API::Repositories do
end
it 'returns the repository archive archive.zip' do
get api("/projects/#{project.id}/repository/archive.zip", user)
get api("/projects/#{project_id}/repository/archive.zip", user)
expect(response).to have_gitlab_http_status(:ok)
@ -257,7 +258,7 @@ RSpec.describe API::Repositories do
end
it 'returns the repository archive archive.tar.bz2' do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
get api("/projects/#{project_id}/repository/archive.tar.bz2", user)
expect(response).to have_gitlab_http_status(:ok)
@ -277,7 +278,7 @@ RSpec.describe API::Repositories do
it 'rate limits user when thresholds hit' do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
get api("/projects/#{project_id}/repository/archive.tar.bz2", user)
expect(response).to have_gitlab_http_status(:too_many_requests)
end
@ -302,6 +303,13 @@ RSpec.describe API::Repositories do
end
end
context 'when unauthenticated and project path has dots' do
it_behaves_like 'repository archive' do
let(:project) { create(:project, :public, :repository, path: 'path.with.dot') }
let(:current_user) { nil }
end
end
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:request) { get api(route) }

View file

@ -25,7 +25,7 @@ RSpec.describe AlertManagement::UpdateAlertStatusService do
end
end
let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value }
let(:new_status) { AlertManagement::Alert::STATUSES[:acknowledged] }
let(:can_update) { true }
subject(:response) { service.execute }
@ -45,6 +45,23 @@ RSpec.describe AlertManagement::UpdateAlertStatusService do
expect { response }.to change { alert.acknowledged? }.to(true)
end
context 'resolving status' do
let(:new_status) { AlertManagement::Alert::STATUSES[:resolved] }
it 'updates the status' do
expect { response }.to change { alert.resolved? }.to(true)
end
context 'user has a pending todo' do
let(:user) { create(:user) }
let!(:todo) { create(:todo, :pending, target: alert, user: user, project: alert.project) }
it 'resolves the todo' do
expect { response }.to change { todo.reload.state }.from('pending').to('done')
end
end
end
context 'when user has no permissions' do
let(:can_update) { false }