Commit 5991d7f6 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0dc8cc71 65af2d47
......@@ -44,6 +44,22 @@ PreCommit:
# on_warn: fail # Treat all warnings as failures
ScssLint:
enabled: true
MarkdownLint:
enabled: true
description: 'Lint documentation for Markdown errors'
required_executable: 'node_modules/.bin/markdownlint'
flags: ['--config', '.markdownlint.json', 'doc/**/*.md']
install_command: 'yarn install'
include:
- 'doc/**/*.md'
Vale:
enabled: true
description: 'Lint documentation for grammatical and formatting errors'
required_executable: 'vale'
flags: ['--config', '.vale.ini', '--minAlertLevel', 'error', 'doc']
install_command: 'brew install vale # (or use another package manager)'
include:
- 'doc/**/*.md'
CommitMsg:
TextWidth:
......
......@@ -64,10 +64,10 @@ export default {
this.groupId,
term,
{
search_namespaces: true,
with_issues_enabled: true,
with_shared: false,
include_subgroups: true,
order_by: 'similarity',
...additionalAttrs,
},
projects => {
......
import { __ } from '~/locale';
export default IssuableTokenKeys => {
const wipToken = {
formattedKey: __('WIP'),
key: 'wip',
type: 'string',
param: '',
symbol: '',
icon: 'admin',
tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: true,
capitalizeTokenValue: true,
const draftToken = {
token: {
formattedKey: __('Draft'),
key: 'draft',
type: 'string',
param: '',
symbol: '',
icon: 'admin',
tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
capitalizeTokenValue: true,
},
conditions: [
{
url: 'wip=yes',
// eslint-disable-next-line @gitlab/require-i18n-strings
replacementUrl: 'draft=yes',
tokenKey: 'draft',
value: __('Yes'),
operator: '=',
},
{
url: 'wip=no',
// eslint-disable-next-line @gitlab/require-i18n-strings
replacementUrl: 'draft=no',
tokenKey: 'draft',
value: __('No'),
operator: '=',
},
{
url: 'not[wip]=yes',
replacementUrl: 'not[draft]=yes',
tokenKey: 'draft',
value: __('Yes'),
operator: '!=',
},
{
url: 'not[wip]=no',
replacementUrl: 'not[draft]=no',
tokenKey: 'draft',
value: __('No'),
operator: '!=',
},
],
};
IssuableTokenKeys.tokenKeys.push(wipToken);
IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken);
IssuableTokenKeys.tokenKeys.push(draftToken.token);
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
IssuableTokenKeys.conditions.push(...draftToken.conditions);
const targetBranchToken = {
formattedKey: __('Target-Branch'),
......
......@@ -106,7 +106,7 @@ export default class AvailableDropdownMappings {
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
wip: {
draft: {
reference: null,
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-wip'),
......
......@@ -72,10 +72,6 @@ export default {
type: String,
required: true,
},
addDashboardDocumentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
......@@ -409,7 +405,6 @@ export default {
v-if="showHeader"
ref="prometheusGraphsHeader"
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
:add-dashboard-documentation-path="addDashboardDocumentationPath"
:default-branch="defaultBranch"
:rearrange-panels-available="rearrangePanelsAvailable"
:custom-metrics-available="customMetricsAvailable"
......
......@@ -107,10 +107,6 @@ export default {
type: Object,
required: true,
},
addDashboardDocumentationPath: {
type: String,
required: true,
},
},
data() {
return {
......@@ -128,6 +124,7 @@ export default {
'canAccessOperationsSettings',
'operationsSettingsPath',
'currentDashboard',
'addDashboardDocumentationPath',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
isOutOfTheBoxDashboard() {
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlCard, GlForm, GlFormGroup, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import {
GlCard,
GlForm,
GlFormGroup,
GlFormTextarea,
GlButton,
GlSprintf,
GlAlert,
} from '@gitlab/ui';
import DashboardPanel from './dashboard_panel.vue';
const initialYml = `title:
......@@ -18,6 +26,7 @@ export default {
GlFormGroup,
GlFormTextarea,
GlButton,
GlSprintf,
GlAlert,
DashboardPanel,
},
......@@ -31,6 +40,8 @@ export default {
'panelPreviewIsLoading',
'panelPreviewError',
'panelPreviewGraphData',
'projectPath',
'addDashboardDocumentationPath',
]),
},
methods: {
......@@ -43,45 +54,91 @@ export default {
</script>
<template>
<div>
<gl-card>
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|Define and preview panel') }}</h2>
</template>
<template #default>
<gl-form @submit.prevent="onSubmit">
<gl-form-group
:label="s__('Metrics|Panel YAML')"
:description="s__('Metrics|Define panel YAML to preview panel.')"
label-for="panel-yml-input"
<div class="gl-display-flex gl-mx-n3">
<gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|1. Define and preview panel') }}</h2>
</template>
<template #default>
<p>{{ s__('Metrics|Define panel YAML below to preview panel.') }}</p>
<gl-form @submit.prevent="onSubmit">
<gl-form-group :label="s__('Metrics|Panel YAML')" label-for="panel-yml-input">
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
/>
</gl-form-group>
<div class="gl-text-right">
<gl-button
ref="clipboardCopyBtn"
variant="success"
category="secondary"
:data-clipboard-text="yml"
@click="$toast.show(s__('Metrics|Panel YAML copied'))"
>
{{ s__('Metrics|Copy YAML') }}
</gl-button>
<gl-button
type="submit"
variant="success"
:disabled="panelPreviewIsLoading"
class="js-no-auto-disable"
>
{{ s__('Metrics|Preview panel') }}
</gl-button>
</div>
</gl-form>
</template>
</gl-card>
<gl-card
class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3"
body-class="gl-display-flex gl-flex-direction-column"
>
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">
{{ s__('Metrics|2. Paste panel YAML into dashboard') }}
</h2>
</template>
<template #default>
<div
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-justify-content-center"
>
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
/>
</gl-form-group>
<p>
{{ s__('Metrics|Copy and paste the panel YAML into your dashboard YAML file.') }}
<br />
<gl-sprintf
:message="
s__(
'Metrics|Dashboard files can be found in %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
)
"
>
<template #code="{content}">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</div>
<div class="gl-text-right">
<gl-button
ref="clipboardCopyBtn"
variant="success"
ref="viewDocumentationBtn"
category="secondary"
:data-clipboard-text="yml"
@click="$toast.show(s__('Metrics|Panel YAML copied'))"
variant="info"
target="_blank"
:href="addDashboardDocumentationPath"
>
{{ s__('Metrics|Copy YAML') }}
{{ s__('Metrics|View documentation') }}
</gl-button>
<gl-button
type="submit"
variant="success"
:disabled="panelPreviewIsLoading"
class="js-no-auto-disable"
>
{{ s__('Metrics|Preview panel') }}
<gl-button ref="openRepositoryBtn" variant="success" :href="projectPath">
{{ s__('Metrics|Open repository') }}
</gl-button>
</div>
</gl-form>
</template>
</gl-card>
</template>
</gl-card>
</div>
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
{{ panelPreviewError }}
......
......@@ -80,6 +80,7 @@ export default () => ({
projectPath: null,
operationsSettingsPath: '',
logsPath: invalidUrl,
addDashboardDocumentationPath: '',
// static paths
customDashboardBasePath: '',
......
......@@ -32,6 +32,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
logsPath,
currentEnvironmentName,
customDashboardBasePath,
addDashboardDocumentationPath,
...dataProps
} = dataset;
......@@ -54,6 +55,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
logsPath,
currentEnvironmentName,
customDashboardBasePath,
addDashboardDocumentationPath,
},
dataProps,
};
......
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
export default {
i18n: {
sourceText: s__('PackageRegistry|Source project located at %{link}'),
licenseText: s__('PackageRegistry|License information located at %{link}'),
recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
appGroup: s__('PackageRegistry|App group: %{group}'),
appName: s__('PackageRegistry|App name: %{name}'),
},
components: {
DetailsRow,
GlLink,
GlSprintf,
},
props: {
packageEntity: {
type: Object,
required: true,
},
},
computed: {
conanRecipe() {
return generateConanRecipe(this.packageEntity);
},
showMetadata() {
const visibilityConditions = {
[PackageType.NUGET]: this.packageEntity.nuget_metadatum,
[PackageType.CONAN]: this.packageEntity.conan_metadatum,
[PackageType.MAVEN]: this.packageEntity.maven_metadatum,
};
return visibilityConditions[this.packageEntity.package_type];
},
},
};
</script>
<template>
<div v-if="showMetadata">
<h3 class="gl-font-lg gl-mt-5" data-testid="title">{{ __('Additional Metadata') }}</h3>
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
<template v-if="packageEntity.nuget_metadatum">
<details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{
packageEntity.nuget_metadatum.project_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
<details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{
packageEntity.nuget_metadatum.license_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
</template>
<details-row
v-else-if="packageEntity.conan_metadatum"
icon="information-o"
padding="gl-p-4"
data-testid="conan-recipe"
>
<gl-sprintf :message="$options.i18n.recipeText">
<template #recipe>{{ conanRecipe }}</template>
</gl-sprintf>
</details-row>
<template v-else-if="packageEntity.maven_metadatum">
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
<gl-sprintf :message="$options.i18n.appName">
<template #name>
<strong>{{ packageEntity.maven_metadatum.app_name }}</strong>
</template>
</gl-sprintf>
</details-row>
<details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
<gl-sprintf :message="$options.i18n.appGroup">
<template #group>
<strong>{{ packageEntity.maven_metadatum.app_group }}</strong>
</template>
</gl-sprintf>
</details-row>
</template>
</div>
</div>
</template>
......@@ -12,6 +12,7 @@ import {
GlTable,
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import PackageActivity from './activity.vue';
import PackageHistory from './package_history.vue';
......@@ -25,6 +26,7 @@ import PypiInstallation from './pypi_installation.vue';
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import PackageListRow from '../../shared/components/package_list_row.vue';
import DependencyRow from './dependency_row.vue';
import AdditionalMetadata from './additional_metadata.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue';
......@@ -32,7 +34,6 @@ import { generatePackageInfo } from '../utils';
import { __, s__ } from '~/locale';
import { PackageType, TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
import { mapActions, mapState } from 'vuex';
export default {
name: 'PackagesApp',
......@@ -59,6 +60,7 @@ export default {
PackageListRow,
DependencyRow,
PackageHistory,
AdditionalMetadata,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -253,9 +255,12 @@ export default {
<package-activity />
</template>
<package-history v-else :package-entity="packageEntity" :project-name="projectName" />
<template v-else>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<additional-metadata :package-entity="packageEntity" />
</template>
<h3 class="gl-font-lg">{{ __('Files') }}</h3>
<h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
<gl-table
:fields="$options.filesTableHeaderFields"
:items="filesTableRows"
......
......@@ -19,7 +19,7 @@ export default {
</script>
<template>
<timeline-entry-item class="system-note note-wrapper gl-my-6!">
<timeline-entry-item class="system-note note-wrapper gl-mb-6!">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>
......
......@@ -44,8 +44,8 @@ export default {
<template>
<div class="issuable-discussion">
<h3 class="gl-ml-6" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-my-4" data-testid="timeline">
<h3 class="gl-font-lg gl-my-3" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<history-element icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
......
......@@ -54,11 +54,11 @@ const projectSelect = () => {
this.groupId,
query.term,
{
search_namespaces: true,
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
order_by: 'similarity',
},
projectsCallback,
);
......
......@@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import DetailsRow from './details_row.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
......
......@@ -10,13 +10,29 @@ export default {
type: String,
required: true,
},
padding: {
type: String,
default: 'gl-py-2',
required: false,
},
dashed: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
borderClass() {
return this.dashed ? 'gl-border-b-solid gl-border-gray-100 gl-border-b-1' : '';
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
:class="[padding, borderClass]"
>
<gl-icon :name="icon" class="gl-mr-4" />
<span>
......
......@@ -4,6 +4,7 @@ import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from './issue_milestone.vue';
import IssueAssignees from './issue_assignees.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
......@@ -15,6 +16,8 @@ export default {
CiIcon,
GlIcon,
GlTooltip,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueDueDate,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -120,8 +123,21 @@ export default {
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
<slot name="dueDate"></slot>
<slot name="weight"></slot>
<span v-if="weight > 0" class="order-md-1">
<issue-weight
:weight="weight"
class="item-weight gl-display-flex gl-align-items-center"
tag-name="span"
/>
</span>
<span v-if="dueDate" class="order-md-1">
<issue-due-date
:date="dueDate"
tooltip-placement="top"
css-class="item-due-date gl-display-flex gl-align-items-center"
/>
</span>
<issue-assignees
v-if="hasAssignees"
......
......@@ -41,13 +41,13 @@ export const timeRanges = [
interval: INTERVALS.hour,
},
{
label: __('1 week'),
label: __('7 days'),
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
name: 'oneWeek',
interval: INTERVALS.day,
},
{
label: __('1 month'),
label: __('30 days'),
duration: { seconds: 60 * 60 * 24 * 30 },
name: 'oneMonth',
interval: INTERVALS.day,
......
......@@ -311,10 +311,6 @@
content: '\f1b3';
}
.fa-times-circle::before {
content: '\f057';
}
.fa-skype::before {
content: '\f17e';
}
......
......@@ -249,7 +249,7 @@ input[type='checkbox']:hover {
.search-clear {
position: absolute;
right: 10px;
top: 10px;
top: 9px;
padding: 0;
color: $gray-darkest;
line-height: 0;
......
......@@ -535,7 +535,7 @@ module Ci
.append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false, masked: true)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false, masked: true)
.append(key: 'CI_REGISTRY_USER', value: ::Gitlab::Auth::CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_USER', value: ::Gitlab::Auth::CI_JOB_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false, masked: true)
.append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
.concat(deploy_token_variables)
......
......@@ -33,8 +33,14 @@ module Terraform
super || StateUploader.default_store
end
def local?
file_store == ObjectStorage::Store::LOCAL
end
def locked?
self.lock_xid.present?
end
end
end
Terraform::State.prepend_if_ee('EE::Terraform::State')
......@@ -22,6 +22,7 @@ module Packages
package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum
package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum
package_detail[:composer_metadatum] = @package.composer_metadatum if @package.composer_metadatum
package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links))
package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info
......
......@@ -46,6 +46,8 @@ module Clusters
releases = []
artifact.each_blob do |blob|
next if blob.empty?
releases.concat(Gitlab::Kubernetes::Helm::Parsers::ListV2.new(blob).releases)
end
......
......@@ -13,24 +13,25 @@ class SubmitUsagePingService
percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
percentage_service_desk_issues].freeze
SubmissionError = Class.new(StandardError)
def execute
return false unless Gitlab::CurrentSettings.usage_ping_enabled?
return false if User.single_user&.requires_usage_stats_consent?
return unless Gitlab::CurrentSettings.usage_ping_enabled?
return if User.single_user&.requires_usage_stats_consent?
payload = Gitlab::UsageData.to_json(force_refresh: true)
raise SubmissionError.new('Usage data is blank') if payload.blank?
response = Gitlab::HTTP.post(
URL,
body: Gitlab::UsageData.to_json(force_refresh: true),
body: payload,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
store_metrics(response)
raise SubmissionError.new("Unsuccessful response code: #{response.code}") unless response.success?
true
rescue Gitlab::HTTP::Error => e
Gitlab::AppLogger.info("Unable to contact GitLab, Inc.: #{e}")
false
store_metrics(response)
end
private
......
......@@ -169,10 +169,6 @@ module ObjectStorage
object_store_options.connection.to_hash.deep_symbolize_keys
end
def consolidated_settings?
object_store_options.fetch('consolidated_settings', false)
end
def remote_store_path
object_store_options.remote_directory
end
......@@ -193,14 +189,18 @@ module ObjectStorage
File.join(self.root, TMP_UPLOAD_PATH)
end
def object_store_config
ObjectStorage::Config.new(object_store_options)
end
def workhorse_remote_upload_options(has_length:, maximum_size: nil)
return unless self.object_store_enabled?
return unless self.direct_upload_enabled?
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id)
direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path,
has_length: has_length, maximum_size: maximum_size, consolidated_settings: consolidated_settings?)
direct_upload = ObjectStorage::DirectUpload.new(self.object_store_config, upload_path,
has_length: has_length, maximum_size: maximum_size)
direct_upload.to_hash.merge(ID: id)
end
......@@ -283,6 +283,10 @@ module ObjectStorage
self.class.object_store_credentials
end
def fog_attributes
@fog_attributes ||= self.class.object_store_config.fog_attributes
end
# Set ACL of uploaded objects to not-public (fog-aws)[1] or no ACL at all
# (fog-google). Value is ignored by other supported backends (fog-aliyun,
# fog-openstack, fog-rackspace)
......
......@@ -72,10 +72,10 @@
.btn-group
- if runner.active?
= link_to pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= icon('pause')
= sprite_icon('pause')
- else
= link_to resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
= icon('play')
= sprite_icon('play')
- if runner.belongs_to_more_than_one_project?
.btn-group
.btn.btn-danger.has-tooltip{ 'aria-label' => 'Remove', 'data-container' => 'body', 'data-original-title' => _('Multi-project Runners cannot be removed'), 'data-placement' => 'top', disabled: 'disabled' }
......
......@@ -9,6 +9,7 @@
.col-lg-8.gl-mb-3
= form_for @hook, as: :hook, url: polymorphic_path([@project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Add webhook', class: 'btn btn-success'
.gl-display-flex.gl-justify-content-end
= f.submit 'Add webhook', class: 'btn btn-success'
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
......@@ -11,7 +11,7 @@
= search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
= icon("search", class: "search-icon")
%button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" }
= icon("times-circle")
= sprite_icon('clear', size: 16)
%span.sr-only
= _("Clear search")
- unless params[:snippets].eql? 'true'
......
# frozen_string_literal: true
class GitlabUsagePingWorker # rubocop:disable Scalability/IdempotentWorker
LEASE_KEY = 'gitlab_usage_ping_worker:ping'
LEASE_TIMEOUT = 86400
include ApplicationWorker
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include Gitlab::ExclusiveLeaseHelpers
feature_category :collection
# Retry for up to approximately three hours then give up.
sidekiq_options retry: 10, dead: false
sidekiq_options retry: 3, dead: false
sidekiq_retry_in { |count| (count + 1) * 8.hours.to_i }
def perform
# Multiple Sidekiq workers could run this. We should only do this at most once a day.
return unless try_obtain_lease
# Splay the request over a minute to avoid thundering herd problems.
sleep(rand(0.0..60.0).round(3))
SubmitUsagePingService.new.execute
end
private
in_lock(LEASE_KEY, ttl: LEASE_TIMEOUT) do
# Splay the request over a minute to avoid thundering herd problems.
sleep(rand(0.0..60.0).round(3))
def try_obtain_lease
Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain
SubmitUsagePingService.new.execute
end
end
end
---
title: Move button in Settings > Webhooks to the right
merge_request: 38650
author:
type: other
---
title: Replace fa-play/pause icons with svg
merge_request: 38535
author:
type: other
---
title: Change date time picker units
merge_request: 38232
author:
type: changed
---
title: Replace times-circle with GitLab SVG clear icon
merge_request: 38409
author:
type: other
---
title: Add support for specifying AWS S3 Server Side Encryption (AWS-KMS)
merge_request: 38240
author:
type: added
......@@ -218,6 +218,9 @@ production: &base
# region: us-east-1
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# endpoint: 'https://s3.amazonaws.com' # default: nil - Useful for S3 compliant services such as DigitalOcean Spaces
# storage_options:
# server_side_encryption: AES256 # AES256, aws:kms
# server_side_encryption_kms_key_id: # Amazon Resource Name. See https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html
# objects:
# artifacts:
# bucket: artifacts
......
# frozen_string_literal: true
require "carrierwave/storage/fog"
# This pulls in https://github.com/carrierwaveuploader/carrierwave/pull/2504 to support
# sending AWS S3 encryption headers when copying objects.
module CarrierWave
module Storage
class Fog < Abstract
class File
def copy_to(new_path)
connection.copy_object(@uploader.fog_directory, file.key, @uploader.fog_directory, new_path, copy_to_options)
CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
end
def copy_to_options
acl_header.merge(@uploader.fog_attributes)
end
end
end
end
end
......@@ -25,6 +25,7 @@ ActiveSupport::Inflector.inflections do |inflect|
project_registry
project_statistics
system_note_metadata
terraform_state_registry
vulnerabilities_feedback
vulnerability_feedback
)
......
......@@ -13,6 +13,7 @@ class ObjectStoreSettings
object_store['direct_upload'] = false if object_store['direct_upload'].nil?
object_store['background_upload'] = true if object_store['background_upload'].nil?
object_store['proxy_download'] = false if object_store['proxy_download'].nil?
object_store['storage_options'] ||= {}
# Convert upload connection settings to use string keys, to make Fog happy
object_store['connection']&.deep_stringify_keys!
......@@ -37,6 +38,8 @@ class ObjectStoreSettings
# region: gdk
# endpoint: 'http://127.0.0.1:9000'
# path_style: true
# storage_options:
# server_side_encryption: AES256
# proxy_download: true
# objects:
# artifacts:
......@@ -49,7 +52,7 @@ class ObjectStoreSettings
#
# Settings.artifacts['object_store'] = {
# "enabled" => true,
# "connection"=> {
# "connection" => {
# "provider" => "AWS",
# "aws_access_key_id" => "minio",
# "aws_secret_access_key" => "gdk-minio",
......@@ -57,6 +60,9 @@ class ObjectStoreSettings
# "endpoint" => "http://127.0.0.1:9000",
# "path_style" => true
# },
# "storage_options" => {
# "server_side_encryption" => "AES256"
# },
# "direct_upload" => true,
# "background_upload" => false,
# "proxy_download" => false,
......@@ -73,6 +79,9 @@ class ObjectStoreSettings
# "endpoint" => "http://127.0.0.1:9000",
# "path_style" => true
# },
# "storage_options" => {
# "server_side_encryption" => "AES256"
# },
# "direct_upload" => true,
# "background_upload" => false,
# "proxy_download" => true,
......@@ -91,12 +100,13 @@ class ObjectStoreSettings
return unless use_consolidated_settings?
main_config = settings['object_store']
common_config = main_config.slice('enabled', 'connection', 'proxy_download')
common_config = main_config.slice('enabled', 'connection', 'proxy_download', 'storage_options')
# Convert connection settings to use string keys, to make Fog happy
common_config['connection']&.deep_stringify_keys!
# These are no longer configurable if common config is used
common_config['direct_upload'] = true
common_config['background_upload'] = false
common_config['storage_options'] ||= {}
SUPPORTED_TYPES.each do |store_type|
overrides = main_config.dig('objects', store_type) || {}
......
# frozen_string_literal: true
class AddVerificationStateToTerraformStates < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
change_table(:terraform_states) do |t|
t.column :verification_retry_at, :datetime_with_timezone
t.column :verified_at, :datetime_with_timezone
t.integer :verification_retry_count, limit: 2
t.binary :verification_checksum, using: 'verification_checksum::bytea'
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20200710153009_add_verification_failure_limit_and_index_to_terraform_states
t.text :verification_failure
# rubocop:enable Migration/AddLimitToTextColumns
end
end
end
# frozen_string_literal: true
class AddVerificationFailureLimitAndIndexToTerraformStates < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :terraform_states, :verification_failure, where: "(verification_failure IS NOT NULL)", name: "terraform_states_verification_failure_partial"
add_concurrent_index :terraform_states, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "terraform_states_verification_checksum_partial"
add_text_limit :terraform_states, :verification_failure, 255
end
def down
remove_concurrent_index :terraform_states, :verification_failure
remove_concurrent_index :terraform_states, :verification_checksum
remove_text_limit :terraform_states, :verification_failure
end
end
0e4151d0fa03777383015a9efa6ce7ff6b2ef548978853da313500bf29448530
\ No newline at end of file
338199b853aa81cd1cc961eb6d8be313d794f885b182be8e7e3b227a3eca5be5
\ No newline at end of file
......@@ -15672,7 +15672,13 @@ CREATE TABLE public.terraform_states (
locked_at timestamp with time zone,
locked_by_user_id bigint,
uuid character varying(32) NOT NULL,
name character varying(255)
name character varying(255),
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
verification_retry_count smallint,
verification_checksum bytea,
verification_failure text,
CONSTRAINT check_21a47163ea CHECK ((char_length(verification_failure) <= 255))
);
CREATE SEQUENCE public.terraform_states_id_seq
......@@ -20925,6 +20931,10 @@ CREATE UNIQUE INDEX taggings_idx ON public.taggings USING btree (tag_id, taggabl
CREATE UNIQUE INDEX term_agreements_unique_index ON public.term_agreements USING btree (user_id, term_id);
CREATE INDEX terraform_states_verification_checksum_partial ON public.terraform_states USING btree (verification_checksum) WHERE (verification_checksum IS NOT NULL);
CREATE INDEX terraform_states_verification_failure_partial ON public.terraform_states USING btree (verification_failure) WHERE (verification_failure IS NOT NULL);
CREATE INDEX tmp_build_stage_position_index ON public.ci_builds USING btree (stage_id, stage_idx) WHERE (stage_idx IS NOT NULL);
CREATE INDEX tmp_idx_on_user_id_where_bio_is_filled ON public.users USING btree (id) WHERE ((COALESCE(bio, ''::character varying))::text IS DISTINCT FROM ''::text);
......
......@@ -387,6 +387,7 @@ reverify
Rubix
Rubocop
Rubular
ruleset
runbook
runbooks
runit
......
......@@ -2314,6 +2314,56 @@ type DastScannerProfileCreatePayload {
id: ID
}
"""
Represents a DAST Site Profile.
"""
type DastSiteProfile {
"""
ID of the site profile
"""
id: ID!
"""
The name of the site profile
"""
profileName: String
"""
The URL of the target to be scanned
"""
targetUrl: String
"""
Permissions for the current user on the resource
"""
userPermissions: DastSiteProfilePermissions!
"""
The current validation status of the site profile
"""
validationStatus: DastSiteProfileValidationStatusEnum
}
"""
The connection type for DastSiteProfile.
"""
type DastSiteProfileConnection {
"""
A list of edges.
"""
edges: [DastSiteProfileEdge]
"""
A list of nodes.
"""
nodes: [DastSiteProfile]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
Autogenerated input type of DastSiteProfileCreate
"""
......@@ -2394,11 +2444,58 @@ type DastSiteProfileDeletePayload {
errors: [String!]!
}
"""
An edge in a connection.
"""
type DastSiteProfileEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DastSiteProfile
}
"""
Identifier of DastSiteProfile
"""
scalar DastSiteProfileID
"""
Check permissions for the current user on site profile
"""
type DastSiteProfilePermissions {
"""
Indicates the user can perform `create_on_demand_dast_scan` on this resource
"""
createOnDemandDastScan: Boolean!
}
enum DastSiteProfileValidationStatusEnum {
"""
Site validation process finished but failed
"""
FAILED_VALIDATION
"""
Site validation process is in progress
"""
INPROGRESS_VALIDATION
"""
Site validation process finished successfully
"""
PASSED_VALIDATION
"""
Site validation process has not started
"""
PENDING_VALIDATION
}
"""
Autogenerated input type of DeleteAnnotation
"""
......@@ -9575,6 +9672,31 @@ type Project {
"""
createdAt: Time
"""
DAST Site Profiles associated with the project
"""
dastSiteProfiles(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DastSiteProfileConnection
"""
Short description of the project
"""
......
......@@ -402,6 +402,18 @@ Autogenerated return type of DastScannerProfileCreate
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | ID | ID of the scanner profile. |
## DastSiteProfile
Represents a DAST Site Profile.
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the site profile |
| `profileName` | String | The name of the site profile |
| `targetUrl` | String | The URL of the target to be scanned |
| `userPermissions` | DastSiteProfilePermissions! | Permissions for the current user on the resource |
| `validationStatus` | DastSiteProfileValidationStatusEnum | The current validation status of the site profile |
## DastSiteProfileCreatePayload
Autogenerated return type of DastSiteProfileCreate
......@@ -421,6 +433,14 @@ Autogenerated return type of DastSiteProfileDelete
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## DastSiteProfilePermissions
Check permissions for the current user on site profile
| Name | Type | Description |
| --- | ---- | ---------- |
| `createOnDemandDastScan` | Boolean! | Indicates the user can perform `create_on_demand_dast_scan` on this resource |
## DeleteAnnotationPayload
Autogenerated return type of DeleteAnnotation
......
......@@ -10,26 +10,23 @@ we suggest investigating to see if a plugin exists. For instance here is the
## Pre-commit static analysis
You're strongly advised to install
[Overcommit](https://github.com/sds/overcommit) to automatically check for
You should install [`overcommit`](https://github.com/sds/overcommit) to automatically check for
static analysis offenses before committing locally.
In your GitLab source directory run:
After installing `overcommit`, run the following in your GitLab source directory:
```shell
make -C tooling/overcommit
```
Then before a commit is created, Overcommit will automatically check for
RuboCop (and other checks) offenses on every modified file.
Then before a commit is created, `overcommit` automatically checks for RuboCop (and other checks)
offenses on every modified file.
This saves you time as you don't have to wait for the same errors to be detected
by the CI.
This saves you time as you don't have to wait for the same errors to be detected by CI/CD.
Overcommit relies on a pre-commit hook to prevent commits that violate its ruleset.
If you wish to override this behavior, it can be done by passing the ENV variable
`OVERCOMMIT_DISABLE`; i.e. `OVERCOMMIT_DISABLE=1 git rebase master` to rebase while
disabling the Git hook.
`overcommit` relies on a pre-commit hook to prevent commits that violate its ruleset. To override
this behavior, pass the `OVERCOMMIT_DISABLE` environment variable. For example,
`OVERCOMMIT_DISABLE=1 git rebase master` to rebase while disabling the Git hook.
## Ruby, Rails, RSpec
......
......@@ -624,6 +624,7 @@ You can use markdownlint:
- [On the command line](https://github.com/igorshubovych/markdownlint-cli#markdownlint-cli--).
- [Within a code editor](#configure-editors).
- [In a `pre-commit` hook](#configure-pre-commit-hooks).
#### Vale
......@@ -650,6 +651,9 @@ You can use Vale:
- [On the command line](https://errata-ai.gitbook.io/vale/getting-started/usage).
- [Within a code editor](#configure-editors).
- [In a `pre-commit` hook](#configure-pre-commit-hooks). Vale only reports errors in the
`pre-commit` hook (the same configuration as the CI/CD pipelines), and does not report suggestions
or warnings.
#### Install linters
......@@ -703,6 +707,22 @@ To configure Vale within your editor, install one of the following as appropriat
We don't use [Vale Server](https://errata-ai.github.io/vale/#using-vale-with-a-text-editor-or-another-third-party-application).
#### Configure pre-commit hooks
Git [pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) allow Git users to
run tests or other processes before committing to a branch, with the ability to not commit to the branch if
failures occur with these tests.
[`overcommit`](https://github.com/sds/overcommit) is a Git hooks manager, making configuring,
installing, and removing Git hooks easy.
Sample configuration for `overcommit` is available in the
[`.overcommit.yml.example`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.overcommit.yml.example)
file for the [`gitlab`](https://gitlab.com/gitlab-org/gitlab) project.
To set up `overcommit` for documentation linting, see
[Pre-commit static analysis](../contributing/style_guides.md#pre-commit-static-analysis).
#### Disable Vale tests
You can disable a specific Vale linting rule or all Vale linting rules for any portion of a
......
......@@ -270,8 +270,12 @@ Use sentence case. For example:
#### UI text
When including user interface text, like button labels or menu items, use the same capitalization that's in the UI.
Standards for this content are listed in the [Pajamas Design System Content section](https://design.gitlab.com/content/punctuation).
When referring to specific user interface text, like a button label or menu item, use the same capitalization that is displayed in the UI.
Standards for this content are listed in the [Pajamas Design System Content section](https://design.gitlab.com/content/punctuation) and typically
match what is called for in this Documentation Style Guide.
If you think there is a mistake in the way the UI text is styled,
create an issue or an MR to propose a change to the UI text.
#### Feature names
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Sortable from 'sortablejs';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import sortableConfig from 'ee/sortable/sortable_config';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -14,8 +12,6 @@ export default {
},
components: {
GlLoadingIcon,
IssueDueDate,
IssueWeight,
RelatedIssuableItem,
},
props: {
......@@ -132,29 +128,15 @@ export default {
:assignees="issue.assignees"
:created-at="issue.createdAt"
:closed-at="issue.closedAt"
:weight="issue.weight"
:due-date="issue.dueDate"
:can-remove="canAdmin"
:can-reorder="canReorder"
:path-id-separator="pathIdSeparator"
event-namespace="relatedIssue"
class="qa-related-issuable-item"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
>
<span v-if="issue.weight > 0" slot="weight" class="order-md-1">
<issue-weight
:weight="issue.weight"
class="item-weight d-flex align-items-center"
tag-name="span"
/>
</span>
<span v-if="issue.dueDate" slot="dueDate" class="order-md-1">
<issue-due-date
:date="issue.dueDate"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center"
/>
</span>
</related-issuable-item>
/>
</li>
</ul>
</div>
......
......@@ -164,7 +164,7 @@ export default {
{{ pendingApprovalsText(rule) }}
</td>
<td class="d-none d-sm-table-cell js-approved-by">
<user-avatar-list :items="rule.approved_by" :img-size="24" />
<user-avatar-list :items="rule.approved_by" :img-size="24" empty-text="" />
</td>
</tr>
</tbody>
......
import { isAbsolute, isSafeURL } from '~/lib/utils/url_utility';
import { REGEXES } from './constants';
window.isAbsolute = isAbsolute;
window.isSafeURL = isSafeURL;
// Get the issue in the format expected by the descendant components of related_issues_block.vue.
export const getFormattedIssue = issue => ({
...issue,
......
......@@ -2,5 +2,6 @@
module Security
class DashboardController < ::Security::ApplicationController
layout 'instance_security'
end
end
......@@ -60,6 +60,12 @@ module EE
description: 'Find iterations',
resolver: ::Resolvers::IterationsResolver
field :dast_site_profiles,
::Types::DastSiteProfileType.connection_type,
null: true,
description: 'DAST Site Profiles associated with the project',
resolve: -> (obj, _args, _ctx) { obj.dast_site_profiles.with_dast_site }
def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project)
end
......
# frozen_string_literal: true
module Types
class DastSiteProfileType < BaseObject
graphql_name 'DastSiteProfile'
description 'Represents a DAST Site Profile.'
authorize :create_on_demand_dast_scan
expose_permissions Types::PermissionTypes::DastSiteProfile
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the site profile'
field :profile_name, GraphQL::STRING_TYPE, null: true,
description: 'The name of the site profile',
resolve: -> (obj, _args, _ctx) { obj.name }
field :target_url, GraphQL::STRING_TYPE, null: true,
description: 'The URL of the target to be scanned',
resolve: -> (obj, _args, _ctx) { obj.dast_site.url }
field :validation_status, Types::DastSiteProfileValidationStatusEnum, null: true,
description: 'The current validation status of the site profile',
resolve: -> (_obj, _args, _ctx) { Types::DastSiteProfileValidationStatusEnum.enum['pending_validation'] }
end
end
# frozen_string_literal: true
module Types
class DastSiteProfileValidationStatusEnum < BaseEnum
value 'PENDING_VALIDATION', description: 'Site validation process has not started'
value 'INPROGRESS_VALIDATION', description: 'Site validation process is in progress'
value 'PASSED_VALIDATION', description: 'Site validation process finished successfully'
value 'FAILED_VALIDATION', description: 'Site validation process finished but failed'
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class DastSiteProfile < BasePermissionType
graphql_name 'DastSiteProfilePermissions'
description 'Check permissions for the current user on site profile'
abilities :create_on_demand_dast_scan
end
end
end
......@@ -22,10 +22,10 @@ module Geo::ReplicableRegistry
def declarative_policy_class
'Geo::RegistryPolicy'
end
end
def registry_consistency_worker_enabled?
replicator_class.enabled?
def registry_consistency_worker_enabled?
replicator_class.enabled?
end
end
def replicator_class
......
......@@ -233,9 +233,12 @@ module EE
self.tracing_setting.try(:external_url)
end
def latest_pipeline_with_security_reports
all_pipelines.newest_first(ref: default_branch).with_reports(::Ci::JobArtifact.security_reports).first ||
all_pipelines.newest_first(ref: default_branch).with_legacy_security_reports.first
def latest_pipeline_with_security_reports(only_successful: false)
pipeline_scope = all_pipelines.newest_first(ref: default_branch)
pipeline_scope = pipeline_scope.success if only_successful
pipeline_scope.with_reports(::Ci::JobArtifact.security_reports).first ||
pipeline_scope.with_legacy_security_reports.first
end
def latest_pipeline_with_reports(reports)
......
# frozen_string_literal: true
module EE
module Terraform
module State
extend ActiveSupport::Concern
prepended do
include ::Gitlab::Geo::ReplicableModel
with_replicator Geo::TerraformStateReplicator
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
end
class_methods do
def replicables_for_geo_node(node = ::Gitlab::Geo.current_node)
selective_sync_scope(node).merge(object_storage_scope(node))
end
private
def object_storage_scope(node)
return all if node.sync_object_storage?
with_files_stored_locally
end
def selective_sync_scope(node)
return all unless node.selective_sync?
project_id_in(node.projects)
end
end
def log_geo_deleted_event
# Keep empty for now. Should be addressed in future
# by https://gitlab.com/gitlab-org/gitlab/-/issues/232917
end
end
end
end
# frozen_string_literal: true
class Geo::TerraformStateRegistry < Geo::BaseRegistry
include Geo::ReplicableRegistry
MODEL_CLASS = ::Terraform::State
MODEL_FOREIGN_KEY = :terraform_state_id
belongs_to :terraform_state, class_name: 'Terraform::State'
end
......@@ -141,9 +141,18 @@ class Vulnerability < ApplicationRecord
def resolved_on_default_branch
return false unless findings.any?
latest_successful_pipeline_for_default_branch = project.latest_successful_pipeline_for_default_branch
latest_pipeline_with_vulnerability = finding.pipelines.order(created_at: :desc).first
latest_pipeline_with_vulnerability != latest_successful_pipeline_for_default_branch
# We can't just use project.latest_successful_pipeline_for_default_branch
# because there's no guarantee that it actually ran the security jobs
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218012
latest_successful_pipeline = project
.latest_pipeline_with_security_reports(only_successful: true)
# Technically this shouldn't ever happen.
# If an vulnerability was discovered, then we must have ran a scan of the
# appropriate type at least once.
return false unless latest_successful_pipeline
finding.pipelines.exclude?(latest_successful_pipeline)
end
def user_notes_count
......
# frozen_string_literal: true
class DastSiteProfilePolicy < BasePolicy
delegate { @subject.project }
end
......@@ -239,7 +239,10 @@ module EE
enable :read_vulnerability_scanner
end
rule { on_demand_scans_enabled & can?(:developer_access) }.enable :read_on_demand_scans
rule { on_demand_scans_enabled & can?(:developer_access) }.policy do
enable :read_on_demand_scans
enable :create_on_demand_dast_scan
end
rule { can?(:read_merge_request) & can?(:read_pipeline) }.enable :read_merge_train
......
# frozen_string_literal: true
module Geo
class TerraformStateReplicator < Gitlab::Geo::Replicator
include ::Geo::BlobReplicatorStrategy
def carrierwave_uploader
model_record.file
end
def self.model
::Terraform::State
end
def self.replication_enabled_by_default?
false
end
end
end
- page_title _('Security')
- header_title _('Security'), instance_statistics_root_path
- nav 'security'
- @left_sidebar = true
= render template: 'layouts/application'
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to security_root_path, title: _('Security Dashboard'), id: 'logo' do
.avatar-container.s40.settings-avatar.rect-avatar
= brand_header_logo
.sidebar-context-title
= _('Security')
%ul.sidebar-top-level-items
= nav_link(path: %w[dashboard#show]) do
= link_to security_root_path, class: 'shortcuts-project rspec-project-link' do
.nav-icon-container
= sprite_icon('dashboard')
%span.nav-item-name
= _('Security Dashboard')
= render 'shared/sidebar_toggle_button'
......@@ -22,6 +22,7 @@ module Geo
Geo::LfsObjectRegistry,
Geo::PackageFileRegistry,
Geo::ProjectRegistry,
Geo::TerraformStateRegistry,
Geo::UploadRegistry
].freeze
......
---
title: Fetch latest successful pipeline with security jobs to check if vulnerability
was resolved
merge_request: 38452
author:
type: changed
---
title: 'Geo: Remove FDW warnings from health checks'
merge_request: 38620
author:
type: removed
---
title: 'Geo: Remove FDW warnings from configuration checks'
merge_request: 38629
author:
type: removed
---
title: Add a left-hand navigation to the security page
merge_request: 38529
author:
type: changed
---
title: Show blank empty text for approved by in MR widget
merge_request: 38436
author:
type: changed
---
title: Add Geo replication columns and tables for terraform states
merge_request: 36594
author:
type: added
# frozen_string_literal: true
class CreateTerraformStateRegistry < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:terraform_state_registry)
ActiveRecord::Base.transaction do
create_table :terraform_state_registry, id: :bigserial, force: :cascade do |t|
t.datetime_with_timezone :retry_at
t.datetime_with_timezone :last_synced_at
t.datetime_with_timezone :created_at, null: false
t.bigint :terraform_state_id, null: false
t.integer :state, default: 0, null: false, limit: 2
t.integer :retry_count, default: 0, limit: 2
t.text :last_sync_failure
t.index :terraform_state_id
t.index :retry_at
t.index :state
end
end
end
add_text_limit :terraform_state_registry, :last_sync_failure, 255
end
def down
drop_table :terraform_state_registry
end
end
......@@ -168,6 +168,19 @@ ActiveRecord::Schema.define(version: 2020_07_10_194046) do
t.index ["wiki_verification_checksum_sha"], name: "idx_project_registry_on_wiki_checksum_sha_partial", where: "(wiki_verification_checksum_sha IS NULL)"
end
create_table "terraform_state_registry", force: :cascade do |t|
t.datetime_with_timezone "retry_at"
t.datetime_with_timezone "last_synced_at"
t.datetime_with_timezone "created_at", null: false
t.bigint "terraform_state_id", null: false
t.integer "state", limit: 2, default: 0, null: false
t.integer "retry_count", limit: 2, default: 0
t.text "last_sync_failure"
t.index ["retry_at"], name: "index_terraform_state_registry_on_retry_at"
t.index ["state"], name: "index_terraform_state_registry_on_state"
t.index ["terraform_state_id"], name: "index_terraform_state_registry_on_terraform_state_id"
end
create_table "vulnerability_export_registry", force: :cascade do |t|
t.datetime_with_timezone "retry_at"
t.datetime_with_timezone "last_synced_at"
......
......@@ -55,7 +55,7 @@ module Gitlab
scanner = create_scanner(report, data['scanner'] || mutate_scanner_tool(data['tool']))
identifiers = create_identifiers(report, data['identifiers'])
report.add_finding(
::Gitlab::Ci::Reports::Security::Occurrence.new(
::Gitlab::Ci::Reports::Security::Finding.new(
uuid: SecureRandom.uuid,
report_type: report.type,
name: data['message'],
......
......@@ -4,7 +4,7 @@ module Gitlab
module Ci
module Reports
module Security
class Occurrence
class Finding
attr_reader :compare_key
attr_reader :confidence
attr_reader :identifiers
......
......@@ -162,7 +162,10 @@ module Gitlab
# solutions can be found at
# https://gitlab.com/gitlab-org/gitlab/-/issues/227693
def self.replicator_classes
classes = [::Geo::PackageFileReplicator]
classes = [
::Geo::PackageFileReplicator,
::Geo::TerraformStateReplicator
]
classes.select(&:enabled?)
end
......
......@@ -56,10 +56,6 @@ module Gitlab
end
end
def gitlab_schema_tables_count
ActiveRecord::Schema.tables.reject { |table| table.start_with?('pg_') }.count
end
def expire_cache!
Gitlab::Geo.expire_cache_keys!(CACHE_KEYS)
end
......
......@@ -12,13 +12,6 @@ module Gitlab
return 'Geo node has a database that is writable which is an indication it is not configured for replication with the primary node.' unless Gitlab::Database.db_read_only?
return 'Geo node does not appear to be replicating the database from the primary node.' if replication_enabled? && !replication_working?
return "Geo database version (#{database_version}) does not match latest migration (#{migration_version}).\nYou may have to run `gitlab-rake geo:db:migrate` as root on the secondary." unless database_migration_version_match?
return 'Geo database is not configured to use Foreign Data Wrapper.' unless Gitlab::Geo::Fdw.enabled?
unless Gitlab::Geo::Fdw.foreign_tables_up_to_date?
output = "Geo database has an outdated FDW remote schema."
output = "#{output} It contains #{foreign_schema_tables_count} of #{gitlab_schema_tables_count} expected tables." unless schema_tables_match?
return output
end
''
rescue => e
......@@ -109,18 +102,6 @@ module Gitlab
database_version.to_i == migration_version.to_i
end
def gitlab_schema_tables_count
@gitlab_schema_tables_count ||= Gitlab::Geo::Fdw.gitlab_schema_tables_count
end
def foreign_schema_tables_count
@foreign_schema_tables_count ||= Gitlab::Geo::Fdw.foreign_schema_tables_count
end
def schema_tables_match?
gitlab_schema_tables_count == foreign_schema_tables_count
end
def archive_recovery_replication_enabled?
!streaming_replication_enabled? && some_replication_active?
end
......
# frozen_string_literal: true
module SystemCheck
module Geo
class FdwEnabledCheck < SystemCheck::BaseCheck
set_name 'GitLab Geo tracking database is configured to use Foreign Data Wrapper?'
set_skip_reason 'not a secondary node'
def skip?
!Gitlab::Geo.secondary?
end
def check?
Gitlab::Geo::Fdw.enabled?
end
def show_error
try_fixing_it(
'Follow Geo setup instructions to configure secondary nodes with FDW support',
'If you upgraded recently check for any new step required to enable FDW'
)
for_more_information('doc/gitlab-geo/database.md')
end
end
end
end
# frozen_string_literal: true
module SystemCheck
module Geo
class FdwSchemaUpToDateCheck < SystemCheck::BaseCheck
set_name 'GitLab Geo tracking database Foreign Data Wrapper schema is up-to-date?'
NOT_SECONDARY_NODE = 'not a secondary node'.freeze
FDW_NOT_CONFIGURED = 'foreign data wrapper is not configured'.freeze
def skip?
unless Gitlab::Geo.secondary?
self.skip_reason = NOT_SECONDARY_NODE
return true
end
unless Gitlab::Geo::Fdw.enabled?
self.skip_reason = FDW_NOT_CONFIGURED
return true
end
false
end
def check?
Gitlab::Geo::Fdw.foreign_tables_up_to_date?
end
def show_error
try_fixing_it(
'Run the following command to refresh the FDW schema:',
'gitlab-rake geo:db:refresh_foreign_tables'
)
for_more_information('doc/administration/geo/replication/troubleshooting.md#geo-database-has-an-outdated-fdw-remote-schema-error')
end
end
end
end
......@@ -36,8 +36,6 @@ module SystemCheck
SystemCheck::Geo::GeoDatabaseConfiguredCheck,
SystemCheck::Geo::DatabaseReplicationEnabledCheck,
SystemCheck::Geo::DatabaseReplicationWorkingCheck,
SystemCheck::Geo::FdwEnabledCheck,
SystemCheck::Geo::FdwSchemaUpToDateCheck,
SystemCheck::Geo::HttpConnectionCheck
] + common_checks
end
......
......@@ -3,11 +3,34 @@
require 'spec_helper'
RSpec.describe Security::DashboardController do
let_it_be(:user) { create(:user) }
describe 'GET #show' do
subject { get :show }
it_behaves_like Security::ApplicationController do
let(:security_application_controller_child_action) do
get :show
end
end
context 'when security dashboard feature' do
before do
sign_in(user)
end
context 'is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it { is_expected.to render_template(:instance_security) }
end
context 'is disabled' do
it { is_expected.to have_gitlab_http_status(:not_found) }
it { is_expected.to render_template('errors/not_found') }
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_finding, class: '::Gitlab::Ci::Reports::Security::Occurrence' do
factory :ci_reports_security_finding, class: '::Gitlab::Ci::Reports::Security::Finding' do
compare_key { "#{identifiers.first.external_type}:#{identifiers.first.external_id}:#{location.fingerprint}" }
confidence { :medium }
identifiers { Array.new(1) { FactoryBot.build(:ci_reports_security_identifier) } }
......@@ -39,7 +39,7 @@ FactoryBot.define do
end
initialize_with do
::Gitlab::Ci::Reports::Security::Occurrence.new(attributes)
::Gitlab::Ci::Reports::Security::Finding.new(attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :geo_terraform_state_registry, class: 'Geo::TerraformStateRegistry' do
association :terraform_state, factory: :terraform_state
state { Geo::TerraformStateRegistry.state_value(:pending) }
trait :synced do
state { Geo::TerraformStateRegistry.state_value(:synced) }
last_synced_at { 5.days.ago }
end
trait :failed do
state { Geo::TerraformStateRegistry.state_value(:failed) }
last_synced_at { 1.day.ago }
retry_count { 2 }
last_sync_failure { 'Random error' }
end
trait :started do
state { Geo::TerraformStateRegistry.state_value(:started) }
last_synced_at { 1.day.ago }
retry_count { 0 }
end
end
end
import { mount } from '@vue/test-utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import IssueWeight from 'ee_component/boards/components/issue_card_weight.vue';
import {
defaultAssignees,
defaultMilestone,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import { TEST_HOST } from 'jest/helpers/test_constants';
describe('RelatedIssuableItem', () => {
let wrapper;
function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) {
wrapper = mountMethod(RelatedIssuableItem, {
propsData: props,
slots,
stubs,
});
}
const props = {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
path: `${TEST_HOST}/path`,
title: 'title',
confidential: true,
dueDate: '1990-12-31',
weight: 10,
createdAt: '2018-12-01T00:00:00.00Z',
milestone: defaultMilestone,
assignees: defaultAssignees,
eventNamespace: 'relatedIssue',
};
const slots = {
dueDate: '<div class="js-due-date-slot"></div>',
weight: '<div class="js-weight-slot"></div>',
};
beforeEach(() => {
mountComponent({ props, slots });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders weight component with correct weight', () => {
expect(wrapper.find(IssueWeight).props('weight')).toBe(props.weight);
});
});
......@@ -160,6 +160,7 @@ describe('EE MRWidget approvals list', () => {
expect(approvers.props()).toEqual(
expect.objectContaining({
items: rule.approved_by,
emptyText: '',
}),
);
});
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['DastSiteProfile'] do
let_it_be(:dast_site_profile) { create(:dast_site_profile) }
let_it_be(:project) { dast_site_profile.project }
let_it_be(:user) { create(:user) }
let_it_be(:fields) { %i[id profileName targetUrl validationStatus userPermissions] }
subject do
GitlabSchema.execute(
query,
context: {
current_user: user
},
variables: {
fullPath: project.full_path
}
).as_json
end
before do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class.graphql_name).to eq('DastSiteProfile') }
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::DastSiteProfile) }
it { expect(described_class).to have_graphql_fields(fields) }
describe 'dast_site_profiles' do
before do
project.add_developer(user)
end
let(:query) do
%(
query project($fullPath: ID!) {
project(fullPath: $fullPath) {
dastSiteProfiles(first: 1) {
nodes {
id
profileName
targetUrl
validationStatus
}
}
}
}
)
end
let(:first_dast_site_profile) do
subject.dig('data', 'project', 'dastSiteProfiles', 'nodes', 0)
end
describe 'id field' do
it 'is a global id' do
expect(first_dast_site_profile['id']).to eq(dast_site_profile.to_global_id.to_s)
end
end
describe 'profile_name field' do
it 'is the name' do
expect(first_dast_site_profile['profileName']).to eq(dast_site_profile.name)
end
end
describe 'target_url field' do
it 'is the url of the associated dast_site' do
expect(first_dast_site_profile['targetUrl']).to eq(dast_site_profile.dast_site.url)
end
end
describe 'validation_status field' do
it 'is a placeholder validation status' do
expect(first_dast_site_profile['validationStatus']).to eq('PENDING_VALIDATION')
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Occurrence do
RSpec.describe Gitlab::Ci::Reports::Security::Finding do
describe '#initialize' do
subject { described_class.new(**params) }
......
......@@ -138,20 +138,6 @@ RSpec.describe Gitlab::Geo::Fdw, :geo do
end
end
describe '.gitlab_schema_tables_count' do
it 'returns the same number of tables as defined in the database' do
expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count)
end
it 'excludes tables that start with `pg_`' do
ActiveRecord::Base.connection.create_table(:pg_gitlab_test)
expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count - 1)
ActiveRecord::Base.connection.drop_table(:pg_gitlab_test)
end
end
describe '.expire_cache!' do
it 'calls Gitlab::Geo.expire_cache_keys!' do
expect(Gitlab::Geo).to receive(:expire_cache_keys!).with(Gitlab::Geo::Fdw::CACHE_KEYS)
......
......@@ -13,6 +13,8 @@ RSpec.describe Gitlab::Geo::GeoNodeStatusCheck do
describe '#replication_verification_complete?' do
before do
allow(Gitlab.config.geo.registry_replication).to receive(:enabled).and_return(true)
stub_feature_flags(geo_terraform_state_replication: false)
end
it 'prints messages for all verification checks' do
......
......@@ -102,8 +102,6 @@ RSpec.describe Gitlab::Geo::HealthCheck, :geo do
context 'that is working' do
before do
allow(subject).to receive(:replication_working?).and_return(true)
allow(Gitlab::Geo::Fdw).to receive(:enabled?) { true }
allow(Gitlab::Geo::Fdw).to receive(:foreign_tables_up_to_date?) { true }
end
it 'returns an error if database is not fully migrated' do
......@@ -116,32 +114,6 @@ RSpec.describe Gitlab::Geo::HealthCheck, :geo do
expect(message).to include('gitlab-rake geo:db:migrate')
end
it 'returns an error when FDW is disabled' do
allow(Gitlab::Geo::Fdw).to receive(:enabled?) { false }
expect(subject.perform_checks).to match(/Geo database is not configured to use Foreign Data Wrapper/)
end
context 'when foreign tables are not up-to-date' do
before do
allow(Gitlab::Geo::Fdw).to receive(:foreign_tables_up_to_date?) { false }
end
it 'returns an error when FDW remote table is not in sync but has same amount of tables' do
allow(Gitlab::Geo::Fdw).to receive(:foreign_schema_tables_count) { 1 }
allow(Gitlab::Geo::Fdw).to receive(:gitlab_schema_tables_count) { 1 }
expect(subject.perform_checks).to match(/Geo database has an outdated FDW remote schema\./)
end
it 'returns an error when FDW remote table is not in sync and has same different amount of tables' do
allow(Gitlab::Geo::Fdw).to receive(:foreign_schema_tables_count) { 1 }
allow(Gitlab::Geo::Fdw).to receive(:gitlab_schema_tables_count) { 2 }
expect(subject.perform_checks).to match(/Geo database has an outdated FDW remote schema\. It contains [0-9]+ of [0-9]+ expected tables/)
end
end
it 'finally returns an empty string when everything is healthy' do
expect(subject.perform_checks).to be_blank
end
......
# frozen_string_literal: true
require 'spec_helper'
require 'rake_helper'
RSpec.describe SystemCheck::Geo::FdwEnabledCheck, :geo do
describe '#skip?' do
subject { described_class.new.skip? }
it 'skips when Geo is disabled' do
allow(Gitlab::Geo).to receive(:enabled?) { false }
is_expected.to be_truthy
end
it 'skips when Geo is enabled but its a primary node' do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { false }
is_expected.to be_truthy
end
it 'does not skip when Geo is enabled and its a secondary node' do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { true }
is_expected.to be_falsey
end
end
describe '#check?' do
context 'with functional FDW environment', :geo_fdw do
it 'returns true' do
expect(subject.check?).to be_truthy
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'rake_helper'
RSpec.describe SystemCheck::Geo::FdwSchemaUpToDateCheck, :geo do
describe '#skip?' do
it 'skips when Geo is disabled' do
allow(Gitlab::Geo).to receive(:enabled?) { false }
expect(subject.skip?).to be_truthy
expect(subject.skip_reason).to eq('not a secondary node')
end
it 'skips when Geo is enabled but its a primary node' do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { false }
expect(subject.skip?).to be_truthy
expect(subject.skip_reason).to eq('not a secondary node')
end
it 'skips when FDW is disabled' do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Geo::Fdw).to receive(:enabled?) { false }
expect(subject.skip?).to be_truthy
expect(subject.skip_reason).to eq('foreign data wrapper is not configured')
end
it 'does not skip when Geo is enabled, its a secondary node and FDW is enabled' do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Geo::Fdw).to receive(:enabled?) { true }
expect(subject.skip?).to be_falsey
end
end
context 'with functional FDW environment', :geo_fdw do
it 'returns true' do
expect(subject.check?).to be_truthy
end
end
end
......@@ -25,8 +25,6 @@ RSpec.describe SystemCheck::RakeTask::GeoTask do
SystemCheck::Geo::GeoDatabaseConfiguredCheck,
SystemCheck::Geo::DatabaseReplicationEnabledCheck,
SystemCheck::Geo::DatabaseReplicationWorkingCheck,
SystemCheck::Geo::FdwEnabledCheck,
SystemCheck::Geo::FdwSchemaUpToDateCheck,
SystemCheck::Geo::HttpConnectionCheck
] + common_checks
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Terraform::State do
using RSpec::Parameterized::TableSyntax
include EE::GeoHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
subject { create(:terraform_state, :with_file) }
describe '.with_files_stored_locally' do
it 'includes states with local storage' do
create_list(:terraform_state, 5, :with_file)
expect(described_class.with_files_stored_locally).to have_attributes(count: 5)
end
it 'excludes states with local storage' do
stub_terraform_state_object_storage(Terraform::StateUploader)
create_list(:terraform_state, 5, :with_file)
expect(described_class.with_files_stored_locally).to have_attributes(count: 0)
end
end
describe '.replicables_for_geo_node' do
where(:selective_sync_enabled, :object_storage_sync_enabled, :terraform_object_storage_enabled, :synced_states) do
true | true | true | 5
true | true | false | 5
true | false | true | 0
true | false | false | 5
false | false | false | 10
false | false | true | 0
false | true | true | 10
false | true | false | 10
true | true | false | 5
end
with_them do
let(:secondary) do
node = build(:geo_node, sync_object_storage: object_storage_sync_enabled)
if selective_sync_enabled
node.selective_sync_type = 'namespaces'
node.namespaces = [group]
end
node.save!
node
end
before do
stub_current_geo_node(secondary)
stub_terraform_state_object_storage(Terraform::StateUploader) if terraform_object_storage_enabled
create_list(:terraform_state, 5, project: project)
create_list(:terraform_state, 5, project: create(:project))
end
it 'returns the proper number of terraform states' do
expect(Terraform::State.replicables_for_geo_node.count).to eq(synced_states)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment