Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-11 15:10:57 +00:00
parent 1c7411c597
commit e40f19ef83
59 changed files with 785 additions and 171 deletions

View File

@ -1,8 +1,16 @@
<script>
import { GlAlert, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import {
GlAlert,
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
@ -13,15 +21,17 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency
export default {
components: {
GlFormGroup,
GlAlert,
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
GlSprintf,
ClipboardButton,
TitleArea,
GlSkeletonLoader,
ManifestsList,
},
inject: ['groupPath', 'dependencyProxyAvailable'],
inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'],
i18n: {
proxyNotAvailableText: s__(
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
@ -33,6 +43,7 @@ export default {
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
pageTitle: s__('DependencyProxy|Dependency Proxy'),
noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
},
data() {
return {
@ -46,7 +57,7 @@ export default {
return !this.dependencyProxyAvailable;
},
variables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
return this.queryVariables;
},
},
},
@ -62,6 +73,38 @@ export default {
dependencyProxyEnabled() {
return this.group?.dependencyProxySetting?.enabled;
},
queryVariables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
},
pageInfo() {
return this.group.dependencyProxyManifests.pageInfo;
},
manifests() {
return this.group.dependencyProxyManifests.nodes;
},
},
methods: {
fetchNextPage() {
this.fetchMore({
first: GRAPHQL_PAGE_SIZE,
after: this.pageInfo?.endCursor,
});
},
fetchPreviousPage() {
this.fetchMore({
first: null,
last: GRAPHQL_PAGE_SIZE,
before: this.pageInfo?.startCursor,
});
},
fetchMore(variables) {
this.$apollo.queries.group.fetchMore({
variables: { ...this.queryVariables, ...variables },
updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
});
},
},
};
</script>
@ -103,6 +146,20 @@ export default {
</span>
</template>
</gl-form-group>
<manifests-list
v-if="manifests && manifests.length"
:manifests="manifests"
:pagination="pageInfo"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
/>
<gl-empty-state
v-else
:svg-path="noManifestsIllustration"
:title="$options.i18n.noManifestTitle"
/>
</div>
<gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
{{ $options.i18n.proxyDisabledText }}

View File

@ -21,13 +21,18 @@ export default {
},
},
i18n: {
listTitle: s__('DependencyProxy|Manifest list'),
listTitle: s__('DependencyProxy|Image list'),
},
computed: {
showPagination() {
return this.pagination.hasNextPage || this.pagination.hasPreviousPage;
},
},
};
</script>
<template>
<div class="gl-mt-5">
<div class="gl-mt-6">
<h3 class="gl-font-base">{{ $options.i18n.listTitle }}</h3>
<div
class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column"
@ -36,6 +41,7 @@ export default {
</div>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
v-bind="pagination"
class="gl-mt-3"
@prev="$emit('prev-page')"

View File

@ -43,6 +43,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr
};
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
const specialRepo = document.querySelector('.js-user-readme-repo');
// eslint-disable-next-line @gitlab/no-global-event-off
$projectNameInput.off('keyup change').on('keyup change', () => {
onProjectNameChange($projectNameInput, $projectPathInput);
@ -54,6 +56,11 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
$projectPathInput.off('keyup change').on('keyup change', () => {
onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName);
hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0;
specialRepo.classList.toggle(
'gl-display-none',
$projectPathInput.val() !== $projectPathInput.data('username'),
);
});
};

View File

@ -37,10 +37,13 @@ export const SAST_IAC_SHORT_NAME = s__('ciReport|IaC Scanning');
export const SAST_IAC_DESCRIPTION = __(
'Analyze your infrastructure as code configuration files for known vulnerabilities.',
);
export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/sast/index');
export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', {
anchor: 'configuration',
});
export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index');
export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
'user/application_security/iac_scanning/index',
{
anchor: 'configuration',
},
);
export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
export const DAST_SHORT_NAME = s__('ciReport|DAST');

View File

@ -98,7 +98,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def create_self_monitoring_project
job_id = SelfMonitoringProjectCreateWorker.perform_async # rubocop:disable CodeReuse/Worker
job_id = SelfMonitoringProjectCreateWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,
@ -137,7 +137,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def delete_self_monitoring_project
job_id = SelfMonitoringProjectDeleteWorker.perform_async # rubocop:disable CodeReuse/Worker
job_id = SelfMonitoringProjectDeleteWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,

View File

@ -33,7 +33,6 @@ module TabHelper
# :item_active - Overrides the default state focing the "active" css classes (optional).
#
def gl_tab_link_to(name = nil, options = {}, html_options = {}, &block)
tab_class = 'nav-item'
link_classes = %w[nav-link gl-tab-nav-item]
active_link_classes = %w[active gl-tab-nav-item-active gl-tab-nav-item-active-indigo]
@ -52,6 +51,8 @@ module TabHelper
end
html_options = html_options.except(:item_active)
extra_tab_classes = html_options.delete(:tab_class)
tab_class = %w[nav-item].push(*extra_tab_classes)
content_tag(:li, class: tab_class, role: 'presentation') do
if block_given?
@ -215,6 +216,7 @@ def gl_tab_counter_badge(count, html_options = {})
badge_classes = %w[badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge]
content_tag(:span,
count,
class: [*html_options[:class], badge_classes].join(' ')
class: [*html_options[:class], badge_classes].join(' '),
data: html_options[:data]
)
end

View File

@ -2,7 +2,8 @@
# == Strip Attribute module
#
# Contains functionality to clean attributes before validation
# Contains functionality to remove leading and trailing
# whitespace from the attribute before validation
#
# Usage:
#

View File

@ -662,7 +662,7 @@ class MergeRequest < ApplicationRecord
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
jid = MergeWorker.perform_async(id, user_id, params.to_h)
jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
# merge_ongoing? depends on merge_jid
@ -681,7 +681,7 @@ class MergeRequest < ApplicationRecord
# attribute is set *and* that the sidekiq job is still running. So a JID
# for a completed RebaseWorker is equivalent to a nil JID.
jid = Sidekiq::Worker.skipping_transaction_check do
RebaseWorker.perform_async(id, user_id, skip_ci)
RebaseWorker.with_status.perform_async(id, user_id, skip_ci)
end
update_column(:rebase_jid, jid)

View File

@ -1992,6 +1992,18 @@ class User < ApplicationRecord
saved
end
def user_project
strong_memoize(:user_project) do
personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
end
def user_readme
strong_memoize(:user_readme) do
user_project&.repository&.readme
end
end
protected
# override, from Devise::Validatable

View File

@ -3,12 +3,6 @@
class UsersStatistics < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) }
class << self
def latest
order_created_at_desc.first
end
end
def active
[
without_groups_and_projects,
@ -26,30 +20,26 @@ class UsersStatistics < ApplicationRecord
end
class << self
def create_current_stats!
stats_by_role = highest_role_stats
def latest
order_created_at_desc.first
end
create!(
without_groups_and_projects: without_groups_and_projects_stats,
with_highest_role_guest: stats_by_role[:guest],
with_highest_role_reporter: stats_by_role[:reporter],
with_highest_role_developer: stats_by_role[:developer],
with_highest_role_maintainer: stats_by_role[:maintainer],
with_highest_role_owner: stats_by_role[:owner],
bots: bot_stats,
blocked: blocked_stats
)
def create_current_stats!
create!(highest_role_stats)
end
private
def highest_role_stats
{
owner: batch_count_for_access_level(Gitlab::Access::OWNER),
maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
guest: batch_count_for_access_level(Gitlab::Access::GUEST)
without_groups_and_projects: without_groups_and_projects_stats,
with_highest_role_guest: batch_count_for_access_level(Gitlab::Access::GUEST),
with_highest_role_reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
with_highest_role_developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
with_highest_role_maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
with_highest_role_owner: batch_count_for_access_level(Gitlab::Access::OWNER),
bots: bot_stats,
blocked: blocked_stats
}
end

View File

@ -14,7 +14,7 @@ module Groups
def async_execute
group_import_state = GroupImportState.safe_find_or_create_by!(group: group, user: current_user)
jid = GroupImportWorker.perform_async(current_user.id, group.id)
jid = GroupImportWorker.with_status.perform_async(current_user.id, group.id)
if jid.present?
group_import_state.update!(jid: jid)

View File

@ -1,74 +1,75 @@
- page_title s_('AdminArea|Users statistics')
%h3.my-4
%h3.gl-my-6
= s_('AdminArea|Users statistics')
%table.table.gl-text-gray-500
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users without a Group and Project')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
%td.p-3.text-right
= @users_statistics&.without_groups_and_projects.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.without_groups_and_projects
= render_if_exists 'admin/dashboard/minimal_access_stats_row', users_statistics: @users_statistics
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Guest')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
%td.p-3.text-right
= @users_statistics&.with_highest_role_guest.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.with_highest_role_guest
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Reporter')
%td.p-3.text-right
= @users_statistics&.with_highest_role_reporter.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.with_highest_role_reporter
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Developer')
%td.p-3.text-right
= @users_statistics&.with_highest_role_developer.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.with_highest_role_developer
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Maintainer')
%td.p-3.text-right
= @users_statistics&.with_highest_role_maintainer.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.with_highest_role_maintainer
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Owner')
%td.p-3.text-right
= @users_statistics&.with_highest_role_owner.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.with_highest_role_owner
%tr
%td.p-3
%td.gl-p-5!
= s_('AdminArea|Bots')
%td.p-3.text-right
= @users_statistics&.bots.to_i
%td.gl-text-right{ class: 'gl-p-5!' }
= @users_statistics&.bots
= render_if_exists 'admin/dashboard/billable_users_row'
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%td.gl-p-5!
%strong
= s_('AdminArea|Active users')
%td.p-3.text-right
%td.gl-text-right{ class: 'gl-p-5!' }
%strong
= @users_statistics&.active.to_i
= @users_statistics&.active
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%td.gl-p-5!
%strong
= s_('AdminArea|Blocked users')
%td.p-3.text-right
%td.gl-text-right{ class: 'gl-p-5!' }
%strong
= @users_statistics&.blocked.to_i
= @users_statistics&.blocked
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%td.gl-p-5!
%strong
= s_('AdminArea|Total users')
%td.p-3.text-right
%td.gl-text-right{ class: 'gl-p-5!' }
%strong
= @users_statistics&.total.to_i
= @users_statistics&.total

View File

@ -3,4 +3,5 @@
- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
#js-dependency-proxy{ data: { group_path: @group.full_path,
dependency_proxy_available: dependency_proxy_available.to_s } }
dependency_proxy_available: dependency_proxy_available.to_s,
no_manifests_illustration: image_path('illustrations/docker-empty-state.svg') } }

View File

@ -40,12 +40,17 @@
.form-group.project-path.col-sm-6
= f.label :path, class: 'label-bold' do
%span= _("Project slug")
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }, data: { username: current_user.username }
- if current_user.can_create_group?
.form-text.text-muted
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
- project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
= project_tip.html_safe
.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo
= sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'user-profile-readme') }
= html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
.form-group
= f.label :description, class: 'label-bold' do

View File

@ -9,6 +9,22 @@
%a.js-retry-load{ href: '#' }
= s_('UserProfile|Retry')
.user-calendar-activities
- if @user.user_readme
.row
.col-12.gl-my-6
.gl-border-gray-100.gl-border-1.gl-border-solid.gl-rounded-small.gl-py-4.gl-px-6
.gl-display-flex
%ol.breadcrumb.gl-breadcrumb-list.gl-mb-4
%li.breadcrumb-item.gl-breadcrumb-item
= link_to @user.username, project_path(@user.user_project)
%span.gl-breadcrumb-separator
= sprite_icon("chevron-right", size: 16)
%li.breadcrumb-item.gl-breadcrumb-item
= link_to @user.user_readme.path, @user.user_project.readme_url
- if current_user == @user
.gl-ml-auto
= link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
= render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
.row
%div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project)

View File

@ -55,6 +55,12 @@ module ApplicationWorker
subclass.after_set_class_attribute { subclass.set_queue }
end
def with_status
status_from_class = self.sidekiq_options_hash['status_expiration']
set(status_expiration: status_from_class || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
end
def generated_queue_name
Gitlab::SidekiqConfig::WorkerRouter.queue_name_from_worker_name(self)
end

View File

@ -47,7 +47,7 @@ module LimitedCapacity
# would be occupied by a job that will be performed in the distant future.
# We let the cron worker enqueue new jobs, this could be seen as our retry and
# back off mechanism because the job might fail again if executed immediately.
sidekiq_options retry: 0
sidekiq_options retry: 0, status_expiration: Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
deduplicate :none
end

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343966
milestone: '14.5'
type: development
group: group::static analysis
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddWithHighestRoleMinimalAccessToUsersStatistics < Gitlab::Database::Migration[1.0]
def change
add_column :users_statistics, :with_highest_role_minimal_access, :integer, null: false, default: 0
end
end

View File

@ -0,0 +1 @@
a22322122144f28306b3b38dbe50b3465ad623c389f8bfe6fa97a0f71b1c7c21

View File

@ -20289,7 +20289,8 @@ CREATE TABLE users_statistics (
with_highest_role_maintainer integer DEFAULT 0 NOT NULL,
with_highest_role_owner integer DEFAULT 0 NOT NULL,
bots integer DEFAULT 0 NOT NULL,
blocked integer DEFAULT 0 NOT NULL
blocked integer DEFAULT 0 NOT NULL,
with_highest_role_minimal_access integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE users_statistics_id_seq

View File

@ -34,7 +34,7 @@ GET /projects/:id/dependencies?package_manager=yarn,bundler
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `maven`, `npm`, `nuget`, `pip`, `yarn`, or `sbt`. |
| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `gradle`, `maven`, `npm`, `nuget`, `pip`, `pipenv`, `yarn`, `sbt`, or `setuptools`. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies"

View File

@ -269,10 +269,11 @@ see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) GitLab 14.3.
> - [Feature flag `ci_include_rules` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) in GitLab 14.4.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) in GitLab 14.4.
> - [Support for `exists` keyword added](https://gitlab.com/gitlab-org/gitlab/-/issues/341511) in GitLab 14.5.
You can use [`rules`](index.md#rules) with `include` to conditionally include other configuration files.
You can only use [`if` rules](index.md#rulesif) in `include`, and only with [certain variables](#use-variables-with-include).
`rules` keywords such as `changes` and `exists` are not supported.
You can only use [`if` rules](index.md#rulesif) and [`exists` rules](index.md#rulesexists) in `include`, and only with
[certain variables](#use-variables-with-include). `rules` keyword `changes` is not supported.
```yaml
include:

View File

@ -0,0 +1,98 @@
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Infrastructure as Code (IaC) Scanning
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6655) in GitLab 14.5.
Infrastructure as Code (IaC) Scanning scans your IaC configuration files for known vulnerabilities.
Currently, IaC scanning supports configuration files for Terraform, Ansible, AWS CloudFormation, and Kubernetes.
## Requirements
To run IaC scanning jobs, by default, you need GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
If you're using the shared runners on GitLab.com, this is enabled by default.
WARNING:
Our IaC scanning jobs require a Linux container type. Windows containers are not yet supported.
WARNING:
If you use your own runners, make sure the Docker version installed
is **not** `19.03.0`. See [troubleshooting information](../sast/index.md#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
## Supported languages and frameworks
GitLab IaC scanning supports a variety of IaC configuration files. Our IaC security scanners also feature automatic language detection which works even for mixed-language projects. If any supported configuration files are detected in project source code we automatically run the appropriate IaC analyzers.
| Configuration File Type | Scan tool | Introduced in GitLab Version |
|------------------------------------------|----------------------------------|-------------------------------|
| Ansible | [kics](https://kics.io/) | 14.5 |
| AWS CloudFormation | [kics](https://kics.io/) | 14.5 |
| Kubernetes | [kics](https://kics.io/) | 14.5 |
| Terraform | [kics](https://kics.io/) | 14.5 |
### Making IaC analyzers available to all GitLab tiers
All open source (OSS) analyzers are availibile with the GitLab Free tier. Future propietary analyzers may be restricted to higher tiers.
#### Summary of features per tier
Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
as shown in the following table:
| Capability | In Free | In Ultimate |
|:---------------------------------------------------------------------------------------|:--------------------|:-------------------|
| [Configure IaC Scanners](#configuration) v | **{check-circle}** | **{check-circle}** |
| View [JSON Report](#reports-json-format) | **{check-circle}** | **{check-circle}** |
| Presentation of JSON Report in Merge Request | **{dotted-circle}** | **{check-circle}** |
| [Address vulnerabilities](../../application_security/vulnerabilities/index.md) | **{dotted-circle}** | **{check-circle}** |
| [Access to Security Dashboard](../../application_security/security_dashboard/index.md) | **{dotted-circle}** | **{check-circle}** |
## Contribute your scanner
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab.
## Configuration
To configure IaC Scanning for a project you can:
- [Configure IaC Scanning manually](#configure-iac-scanning-manually)
- [Enable IaC Scanning via an automatic merge request](#enable-iac-scanning-via-an-automatic-merge-request)
### Configure IaC Scanning manually
To enable IaC Scanning you must [include](../../../ci/yaml/index.md#includetemplate) the
[`SAST-IaC.latest.gitlab-ci.yml template`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST-IaC.latest.gitlab-ci.yml) provided as part of your GitLab installation.
The included template creates IaC scanning jobs in your CI/CD pipeline and scans
your project's configuration files for possible vulnerabilities.
The results are saved as a
[SAST report artifact](../../../ci/yaml/index.md#artifactsreportssast)
that you can download and analyze.
### Enable IaC Scanning via an automatic merge request
To enable IaC Scanning in a project, you can create a merge request
from the Security Configuration page:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Security & Compliance > Configuration**.
1. In the **Infrastructure as Code (IaC) Scanning** row, select **Configure via Merge Request**.
This automatically creates a merge request with the changes necessary to enable IaC Scanning
that you can review and merge to complete the configuration.
## Reports JSON format
The IaC tool emits a JSON report file in the existing SAST report format. For more information, see the
[schema for this report](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json).
The JSON report file can be downloaded from the CI pipelines page, or the
pipelines tab on merge requests by [setting `artifacts: paths`](../../../ci/yaml/index.md#artifactspaths) to `gl-sast-report.json`. For more information see [Downloading artifacts](../../../ci/pipelines/job_artifacts.md).

View File

@ -31,19 +31,20 @@ For an overview of GitLab application security, see [Shifting Security Left](htt
GitLab uses the following tools to scan and report known vulnerabilities found in your project.
| Secure scanning tool | Description |
|:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------|
| [Container Scanning](container_scanning/index.md) | Scan Docker containers for known vulnerabilities. |
| [Dependency List](dependency_list/index.md) | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](dast/index.md) | Analyze running web applications for known vulnerabilities. |
| [DAST API](dast_api/index.md) | Analyze running web APIs for known vulnerabilities. |
| [API fuzzing](api_fuzzing/index.md) | Find unknown bugs and vulnerabilities in web APIs with fuzzing. |
| [Secret Detection](secret_detection/index.md) | Analyze Git history for leaked secrets. |
| [Security Dashboard](security_dashboard/index.md) | View vulnerabilities in all your projects and groups. |
| [Static Application Security Testing (SAST)](sast/index.md) | Analyze source code for known vulnerabilities. |
| [Coverage fuzzing](coverage_fuzzing/index.md) | Find unknown bugs and vulnerabilities with coverage-guided fuzzing. |
| [Cluster Image Scanning](cluster_image_scanning/index.md) | Scan Kubernetes clusters for known vulnerabilities. |
| Secure scanning tool | Description |
| :------------------------------------------------------------- | :------------------------------------------------------------------ |
| [Container Scanning](container_scanning/index.md) | Scan Docker containers for known vulnerabilities. |
| [Dependency List](dependency_list/index.md) | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](dast/index.md) | Analyze running web applications for known vulnerabilities. |
| [DAST API](dast_api/index.md) | Analyze running web APIs for known vulnerabilities. |
| [API fuzzing](api_fuzzing/index.md) | Find unknown bugs and vulnerabilities in web APIs with fuzzing. |
| [Secret Detection](secret_detection/index.md) | Analyze Git history for leaked secrets. |
| [Security Dashboard](security_dashboard/index.md) | View vulnerabilities in all your projects and groups. |
| [Static Application Security Testing (SAST)](sast/index.md) | Analyze source code for known vulnerabilities. |
| [Infrastructure as Code (IaC) Scanning](iac_scanning/index.md) | Analyze your IaC coniguration files for known vulnerabilities. |
| [Coverage fuzzing](coverage_fuzzing/index.md) | Find unknown bugs and vulnerabilities with coverage-guided fuzzing. |
| [Cluster Image Scanning](cluster_image_scanning/index.md) | Scan Kubernetes clusters for known vulnerabilities. |
## Security scanning with Auto DevOps

View File

@ -100,6 +100,18 @@ When visiting the public page of a user, you can only see the projects which you
If the [public level is restricted](../admin_area/settings/visibility_and_access_controls.md#restrict-visibility-levels),
user profiles are only visible to signed-in users.
## User profile README
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
You can add a README section to your profile that can include more information and formatting than
your profile's bio.
To add a README to your profile:
1. Create a new public project with the same name as your GitLab username.
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
## Add external accounts to your user profile page
You can add links to certain other external accounts you might have, like Skype and Twitter.

View File

@ -177,7 +177,9 @@ audit trail:
include: # Execute individual project's configuration (if project contains .gitlab-ci.yml)
project: '$CI_PROJECT_PATH'
file: '$CI_CONFIG_PATH'
ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch.
ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch
rules:
- exists: '$CI_CONFIG_PATH'
```
##### Ensure compliance jobs are always run

View File

@ -46,7 +46,8 @@ module API
def finder_params
{
package_type: :terraform_module,
package_name: "#{params[:module_name]}/#{params[:module_system]}"
package_name: "#{params[:module_name]}/#{params[:module_system]}",
exact_name: true
}.tap do |finder_params|
finder_params[:package_version] = params[:module_version] if params.has_key?(:module_version)
end

View File

@ -5,6 +5,8 @@ module Gitlab
module Build
module Context
class Base
include Gitlab::Utils::StrongMemoize
attr_reader :pipeline
def initialize(pipeline)
@ -15,6 +17,26 @@ module Gitlab
raise NotImplementedError
end
def project
pipeline.project
end
def sha
pipeline.sha
end
def top_level_worktree_paths
strong_memoize(:top_level_worktree_paths) do
project.repository.tree(sha).blobs.map(&:path)
end
end
def all_worktree_paths
strong_memoize(:all_worktree_paths) do
project.repository.ls_files(sha)
end
end
protected
def pipeline_attributes

View File

@ -15,19 +15,21 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end
def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline)
def satisfied_by?(_pipeline, context)
paths = worktree_paths(context)
exact_matches?(paths) || pattern_matches?(paths)
end
private
def worktree_paths(pipeline)
def worktree_paths(context)
return unless context.project
if @top_level_only
pipeline.top_level_worktree_paths
context.top_level_worktree_paths
else
pipeline.all_worktree_paths
context.all_worktree_paths
end
end

View File

@ -9,9 +9,9 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[if].freeze
ALLOWED_KEYS = %i[if exists].freeze
attributes :if
attributes :if, :exists
validations do
validates :config, presence: true

View File

@ -5,6 +5,8 @@ module Gitlab
class Config
module External
class Context
include Gitlab::Utils::StrongMemoize
TimeoutError = Class.new(StandardError)
attr_reader :project, :sha, :user, :parent_pipeline, :variables
@ -22,6 +24,18 @@ module Gitlab
yield self if block_given?
end
def top_level_worktree_paths
strong_memoize(:top_level_worktree_paths) do
project.repository.tree(sha).blobs.map(&:path)
end
end
def all_worktree_paths
strong_memoize(:all_worktree_paths) do
project.repository.ls_files(sha)
end
end
def mutate(attrs = {})
self.class.new(**attrs) do |ctx|
ctx.expandset = expandset

View File

@ -74,6 +74,9 @@ gemnasium-maven-dependency_scanning:
# override the analyzer image with a custom value. This may be subject to change or
# breakage across GitLab releases.
DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"
# Stop reporting Gradle as "maven".
# See https://gitlab.com/gitlab-org/gitlab/-/issues/338252
DS_REPORT_PACKAGE_MANAGER_MAVEN_WHEN_JAVA: "false"
rules:
- if: $DEPENDENCY_SCANNING_DISABLED
when: never
@ -97,6 +100,9 @@ gemnasium-python-dependency_scanning:
# override the analyzer image with a custom value. This may be subject to change or
# breakage across GitLab releases.
DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
# Stop reporting Pipenv and Setuptools as "pip".
# See https://gitlab.com/gitlab-org/gitlab/-/issues/338252
DS_REPORT_PACKAGE_MANAGER_PIP_WHEN_PYTHON: "false"
rules:
- if: $DEPENDENCY_SCANNING_DISABLED
when: never

View File

@ -7,12 +7,16 @@ module Gitlab
# To check if a job has been completed, simply pass the job ID to the
# `completed?` method:
#
# job_id = SomeWorker.perform_async(...)
# job_id = SomeWorker.with_status.perform_async(...)
#
# if Gitlab::SidekiqStatus.completed?(job_id)
# ...
# end
#
# If you do not use `with_status`, and the worker class does not declare
# `status_expiration` in its `sidekiq_options`, then this status will not be
# stored.
#
# For each job ID registered a separate key is stored in Redis, making lookups
# much faster than using Sidekiq's built-in job finding/status API. These keys
# expire after a certain period of time to prevent storing too many keys in

View File

@ -815,6 +815,9 @@ msgstr ""
msgid "%{primary} (%{secondary})"
msgstr ""
msgid "%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}"
msgstr ""
msgid "%{ref} cannot be added: %{error}"
msgstr ""
@ -2313,6 +2316,9 @@ msgstr ""
msgid "AdminArea|Maintainer"
msgstr ""
msgid "AdminArea|Minimal access"
msgstr ""
msgid "AdminArea|New group"
msgstr ""
@ -11337,7 +11343,10 @@ msgstr ""
msgid "DependencyProxy|Enable Proxy"
msgstr ""
msgid "DependencyProxy|Manifest list"
msgid "DependencyProxy|Image list"
msgstr ""
msgid "DependencyProxy|There are no images in the cache"
msgstr ""
msgid "Depends on %d merge request being merged"
@ -18547,12 +18556,18 @@ msgstr ""
msgid "Integrations|Default settings are inherited from the instance level."
msgstr ""
msgid "Integrations|Edit project alias"
msgstr ""
msgid "Integrations|Enable GitLab.com slash commands in a Slack workspace."
msgstr ""
msgid "Integrations|Enable comments"
msgstr ""
msgid "Integrations|Enter your alias"
msgstr ""
msgid "Integrations|Failed to link namespace. Please try again."
msgstr ""
@ -18670,6 +18685,9 @@ msgstr ""
msgid "Integrations|You can now close this window and return to the GitLab for Jira application."
msgstr ""
msgid "Integrations|You can use this alias in your Slack commands"
msgstr ""
msgid "Integrations|You haven't activated any integrations yet."
msgstr ""

View File

@ -10,7 +10,7 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_maintainer_to_push: false) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_collaboration: false) }
let(:merge_request_source_project) { project }
before do
@ -507,6 +507,7 @@ RSpec.describe Projects::MergeRequestsController do
end
it 'starts the merge immediately with permitted params' do
allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'sha' => merge_request.diff_head_sha })
merge_with_sha
@ -2078,6 +2079,10 @@ RSpec.describe Projects::MergeRequestsController do
post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request }
end
before do
allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
end
def expect_rebase_worker_for(user)
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, false)
end

View File

@ -5,10 +5,10 @@ FactoryBot.define do
highest_access_level { nil }
user
trait(:guest) { highest_access_level { GroupMember::GUEST } }
trait(:reporter) { highest_access_level { GroupMember::REPORTER } }
trait(:developer) { highest_access_level { GroupMember::DEVELOPER } }
trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } }
trait(:owner) { highest_access_level { GroupMember::OWNER } }
trait(:guest) { highest_access_level { GroupMember::GUEST } }
trait(:reporter) { highest_access_level { GroupMember::REPORTER } }
trait(:developer) { highest_access_level { GroupMember::DEVELOPER } }
trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } }
trait(:owner) { highest_access_level { GroupMember::OWNER } }
end
end

View File

@ -21,6 +21,14 @@ RSpec.describe 'User visits their profile' do
expect(page).to have_content "This information will appear on your profile"
end
it 'shows user readme' do
create(:project, :repository, :public, path: user.username, namespace: user.namespace)
visit(user_path(user))
expect(find('.file-content')).to have_content('testme')
end
context 'when user has groups' do
let(:group) do
create :group do |group|

View File

@ -39,7 +39,7 @@ export const member = {
Developer: 30,
Maintainer: 40,
Owner: 50,
'Minimal Access': 5,
'Minimal access': 5,
},
};

View File

@ -1,32 +1,40 @@
import { GlFormInputGroup, GlFormGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import {
GlFormInputGroup,
GlFormGroup,
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
import { proxyDetailsQuery, proxyData } from './mock_data';
import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
const localVue = createLocalVue();
describe('DependencyProxyApp', () => {
let wrapper;
let apolloProvider;
let resolver;
const provideDefaults = {
groupPath: 'gitlab-org',
dependencyProxyAvailable: true,
noManifestsIllustration: 'noManifestsIllustration',
};
function createComponent({
provide = provideDefaults,
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()),
} = {}) {
function createComponent({ provide = provideDefaults } = {}) {
localVue.use(VueApollo);
const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
@ -53,6 +61,12 @@ describe('DependencyProxyApp', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findMainArea = () => wrapper.findByTestId('main-area');
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
});
afterEach(() => {
wrapper.destroy();
@ -78,8 +92,8 @@ describe('DependencyProxyApp', () => {
});
it('does not call the graphql endpoint', async () => {
const resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
createComponent({ ...createComponentArguments, resolver });
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
createComponent({ ...createComponentArguments });
await waitForPromises();
@ -145,14 +159,73 @@ describe('DependencyProxyApp', () => {
it('from group has a description with proxy count', () => {
expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
});
describe('manifest lists', () => {
describe('when there are no manifests', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(
proxyDetailsQuery({
extend: { dependencyProxyManifests: { nodes: [], pageInfo: pagination() } },
}),
);
createComponent();
return waitForPromises();
});
it('shows the empty state message', () => {
expect(findEmptyState().props()).toMatchObject({
svgPath: provideDefaults.noManifestsIllustration,
title: DependencyProxyApp.i18n.noManifestTitle,
});
});
it('hides the list', () => {
expect(findManifestList().exists()).toBe(false);
});
});
describe('when there are manifests', () => {
it('hides the empty state message', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
manifests: proxyManifests(),
pagination: stripTypenames(pagination()),
});
});
it('prev-page event on list fetches the previous page', () => {
findManifestList().vm.$emit('prev-page');
expect(resolver).toHaveBeenCalledWith({
before: pagination().startCursor,
first: null,
fullPath: provideDefaults.groupPath,
last: GRAPHQL_PAGE_SIZE,
});
});
it('next-page event on list fetches the next page', () => {
findManifestList().vm.$emit('next-page');
expect(resolver).toHaveBeenCalledWith({
after: pagination().endCursor,
first: GRAPHQL_PAGE_SIZE,
fullPath: provideDefaults.groupPath,
});
});
});
});
});
describe('when the dependency proxy is disabled', () => {
beforeEach(() => {
createComponent({
resolver: jest
.fn()
.mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })),
});
resolver = jest
.fn()
.mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } }));
createComponent();
return waitForPromises();
});

View File

@ -26,42 +26,56 @@ describe('Manifests List', () => {
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('has the correct title', () => {
createComponent();
expect(wrapper.text()).toContain(Component.i18n.listTitle);
});
it('shows a row for every manifest', () => {
createComponent();
expect(findRows().length).toBe(defaultProps.manifests.length);
});
it('binds a manifest to each row', () => {
createComponent();
expect(findRows().at(0).props()).toMatchObject({
manifest: defaultProps.manifests[0],
});
});
describe('pagination', () => {
it('is hidden when there is no next or prev pages', () => {
createComponent({ ...defaultProps, pagination: {} });
expect(findPagination().exists()).toBe(false);
});
it('has the correct props', () => {
createComponent();
expect(findPagination().props()).toMatchObject({
...defaultProps.pagination,
});
});
it('emits the next-page event', () => {
createComponent();
findPagination().vm.$emit('next');
expect(wrapper.emitted('next-page')).toEqual([[]]);
});
it('emits the prev-page event', () => {
createComponent();
findPagination().vm.$emit('prev');
expect(wrapper.emitted('prev-page')).toEqual([[]]);

View File

@ -21,7 +21,7 @@ export const pagination = (extend) => ({
...extend,
});
export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
export const proxyDetailsQuery = ({ extendSettings = {}, extend } = {}) => ({
data: {
group: {
...proxyData(),
@ -34,6 +34,7 @@ export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
nodes: proxyManifests(),
pageInfo: pagination(),
},
...extend,
},
},
});

View File

@ -36,7 +36,15 @@ RSpec.describe TabHelper do
expect(gl_tab_link_to('/url') { 'block content' }).to match(/block content/)
end
it 'creates a tab with custom classes' do
it 'creates a tab with custom classes for enclosing list item without content block provided' do
expect(gl_tab_link_to('Link', '/url', { tab_class: 'my-class' })).to match(/<li class=".*my-class.*"/)
end
it 'creates a tab with custom classes for enclosing list item with content block provided' do
expect(gl_tab_link_to('/url', { tab_class: 'my-class' }) { 'Link' }).to match(/<li class=".*my-class.*"/)
end
it 'creates a tab with custom classes for anchor element' do
expect(gl_tab_link_to('Link', '/url', { class: 'my-class' })).to match(/<a class=".*my-class.*"/)
end
@ -161,5 +169,11 @@ RSpec.describe TabHelper do
expect(gl_tab_counter_badge(1, { class: 'js-test' })).to eq('<span class="js-test badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge">1</span>')
end
end
context 'with data attributes' do
it 'creates a tab counter badge with the data attributes' do
expect(gl_tab_counter_badge(1, { data: { some_attribute: 'foo' } })).to eq('<span class="badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge" data-some-attribute="foo">1</span>')
end
end
end
end

View File

@ -3,10 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
describe '#satisfied_by?' do
let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
subject { described_class.new(globs).satisfied_by?(pipeline, nil) }
shared_examples 'an exists rule with a context' do
subject { described_class.new(globs).satisfied_by?(pipeline, context) }
it_behaves_like 'a glob matching rule' do
let(:project) { create(:project, :custom_repo, files: files) }
@ -24,4 +22,26 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
it { is_expected.to be_truthy }
end
end
describe '#satisfied_by?' do
let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
context 'when context is Build::Context::Build' do
it_behaves_like 'an exists rule with a context' do
let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') }
end
end
context 'when context is Build::Context::Global' do
it_behaves_like 'an exists rule with a context' do
let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) }
end
end
context 'when context is Config::External::Context' do
it_behaves_like 'an exists rule with a context' do
let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) }
end
end
end
end

View File

@ -5,7 +5,7 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
let(:factory) do
Gitlab::Config::Entry::Factory.new(described_class)
.value(config)
.value(config)
end
subject(:entry) { factory.create! }
@ -25,6 +25,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
it { is_expected.to be_valid }
end
context 'when specifying an exists: clause' do
let(:config) { { exists: './this.md' } }
it { is_expected.to be_valid }
end
context 'using a list of multiple expressions' do
let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } }
@ -86,5 +92,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
expect(subject).to eq(if: '$THIS || $THAT')
end
end
context 'when specifying an exists: clause' do
let(:config) { { exists: './test.md' } }
it 'returns the config' do
expect(subject).to eq(exists: './test.md')
end
end
end
end

View File

@ -406,7 +406,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
context 'when rules defined' do
context 'when a rule is invalid' do
let(:values) do
{ include: [{ local: 'builds.yml', rules: [{ exists: ['$MY_VAR'] }] }] }
{ include: [{ local: 'builds.yml', rules: [{ changes: ['$MY_VAR'] }] }] }
end
it 'raises IncludeError' do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::External::Rules do
let(:rule_hashes) {}
@ -32,6 +32,26 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do
end
end
context 'when there is a rule with exists' do
let(:project) { create(:project, :repository) }
let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['test.md']) }
let(:rule_hashes) { [{ exists: 'Dockerfile' }] }
context 'when the file does not exist' do
it { is_expected.to eq(false) }
end
context 'when the file exists' do
let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) }
before do
project.repository.create_file(project.owner, 'Dockerfile', "commit", message: 'test', branch_name: "master")
end
it { is_expected.to eq(true) }
end
end
context 'when there is a rule with if and when' do
let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] }
@ -41,12 +61,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do
end
end
context 'when there is a rule with exists' do
let(:rule_hashes) { [{ exists: ['$MY_VAR'] }] }
context 'when there is a rule with changes' do
let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] }
it 'raises an error' do
expect { result }.to raise_error(described_class::InvalidIncludeRulesError,
'invalid include rule: {:exists=>["$MY_VAR"]}')
'invalid include rule: {:changes=>["$MY_VAR"]}')
end
end
end

View File

@ -597,7 +597,7 @@ RSpec.describe Gitlab::Ci::Config do
job1: {
script: ["echo 'hello from main file'"],
variables: {
VARIABLE_DEFINED_IN_MAIN_FILE: 'some value'
VARIABLE_DEFINED_IN_MAIN_FILE: 'some value'
}
}
})
@ -727,30 +727,70 @@ RSpec.describe Gitlab::Ci::Config do
end
end
context "when an 'include' has rules with a project variable" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- local: #{local_location}
rules:
- if: $CI_PROJECT_ID == "#{project_id}"
image: ruby:2.7
HEREDOC
end
context "when an 'include' has rules" do
context "when the rule is an if" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- local: #{local_location}
rules:
- if: $CI_PROJECT_ID == "#{project_id}"
image: ruby:2.7
HEREDOC
end
context 'when the rules condition is satisfied' do
let(:project_id) { project.id }
context 'when the rules condition is satisfied' do
let(:project_id) { project.id }
it 'includes the file' do
expect(config.to_hash).to include(local_location_hash)
it 'includes the file' do
expect(config.to_hash).to include(local_location_hash)
end
end
context 'when the rules condition is satisfied' do
let(:project_id) { non_existing_record_id }
it 'does not include the file' do
expect(config.to_hash).not_to include(local_location_hash)
end
end
end
context 'when the rules condition is satisfied' do
let(:project_id) { non_existing_record_id }
context "when the rule is an exists" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- local: #{local_location}
rules:
- exists: "#{filename}"
image: ruby:2.7
HEREDOC
end
it 'does not include the file' do
expect(config.to_hash).not_to include(local_location_hash)
before do
project.repository.create_file(
project.creator,
'my_builds.yml',
local_file_content,
message: 'Add my_builds.yml',
branch_name: '12345'
)
end
context 'when the exists file does not exist' do
let(:filename) { 'not_a_real_file.md' }
it 'does not include the file' do
expect(config.to_hash).not_to include(local_location_hash)
end
end
context 'when the exists file does exist' do
let(:filename) { 'my_builds.yml' }
it 'does include the file' do
expect(config.to_hash).to include(local_location_hash)
end
end
end
end

View File

@ -2920,6 +2920,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
params = {}
merge_jid = 'hash-123'
allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
expect(merge_request).to receive(:expire_etag_cache)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
merge_jid
@ -2938,6 +2940,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject(:execute) { merge_request.rebase_async(user_id) }
before do
allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
end
it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
expect(RebaseWorker)
.to receive(:perform_async)

View File

@ -6225,4 +6225,31 @@ RSpec.describe User do
expect(described_class.get_ids_by_username([user_name])).to match_array([user_id])
end
end
describe 'user_project' do
it 'returns users project matched by username and public visibility' do
user = create(:user)
public_project = create(:project, :public, path: user.username, namespace: user.namespace)
create(:project, namespace: user.namespace)
expect(user.user_project).to eq(public_project)
end
end
describe 'user_readme' do
it 'returns readme from user project' do
user = create(:user)
create(:project, :repository, :public, path: user.username, namespace: user.namespace)
expect(user.user_readme.name).to eq('README.md')
expect(user.user_readme.data).to include('testme')
end
it 'returns nil if project is private' do
user = create(:user)
create(:project, :repository, :private, path: user.username, namespace: user.namespace)
expect(user.user_readme).to be(nil)
end
end
end

View File

@ -34,11 +34,11 @@ RSpec.describe UsersStatistics do
describe '.create_current_stats!' do
before do
create_list(:user_highest_role, 4)
create_list(:user_highest_role, 1)
create_list(:user_highest_role, 2, :guest)
create_list(:user_highest_role, 3, :reporter)
create_list(:user_highest_role, 4, :developer)
create_list(:user_highest_role, 3, :maintainer)
create_list(:user_highest_role, 2, :reporter)
create_list(:user_highest_role, 2, :developer)
create_list(:user_highest_role, 2, :maintainer)
create_list(:user_highest_role, 2, :owner)
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
@ -49,11 +49,11 @@ RSpec.describe UsersStatistics do
context 'when successful' do
it 'creates an entry with the current statistics values' do
expect(described_class.create_current_stats!).to have_attributes(
without_groups_and_projects: 4,
without_groups_and_projects: 1,
with_highest_role_guest: 2,
with_highest_role_reporter: 3,
with_highest_role_developer: 4,
with_highest_role_maintainer: 3,
with_highest_role_reporter: 2,
with_highest_role_developer: 2,
with_highest_role_maintainer: 2,
with_highest_role_owner: 2,
bots: 2,
blocked: 1

View File

@ -3278,6 +3278,8 @@ RSpec.describe API::MergeRequests do
context 'when skip_ci parameter is set' do
it 'enqueues a rebase of the merge request with skip_ci flag set' do
allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
Sidekiq::Testing.fake! do

View File

@ -28,10 +28,25 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") }
let(:headers) { {} }
let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
subject { get(url, headers: headers) }
context 'with a conflicting package name' do
let!(:conflicting_package) { create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0') }
before do
group.add_developer(user)
end
it 'returns only one version' do
subject
expect(json_response['modules'][0]['versions'].size).to eq(1)
expect(json_response['modules'][0]['versions'][0]['version']).to eq('1.0.0')
end
end
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success

View File

@ -60,6 +60,7 @@ RSpec.describe Import::GitlabGroupsController do
end
it 'imports the group data', :sidekiq_inline do
allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
allow(GroupImportWorker).to receive(:perform_async).and_call_original
import_request
@ -67,7 +68,6 @@ RSpec.describe Import::GitlabGroupsController do
group = Group.find_by(name: 'test-group-import')
expect(GroupImportWorker).to have_received(:perform_async).with(user.id, group.id)
expect(group.description).to eq 'A voluptate non sequi temporibus quam at.'
expect(group.visibility_level).to eq Gitlab::VisibilityLevel::PRIVATE
end

View File

@ -24,6 +24,10 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
project.add_maintainer(user)
end
before do
allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
end
describe "#available_for?" do
subject { service.available_for?(mr_merge_if_green_enabled) }

View File

@ -7,6 +7,10 @@ RSpec.describe Groups::ImportExport::ImportService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
before do
allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
end
context 'when the job can be successfully scheduled' do
subject(:import_service) { described_class.new(group: group, user: user) }
@ -20,6 +24,8 @@ RSpec.describe Groups::ImportExport::ImportService do
end
it 'enqueues an import job' do
allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
expect(GroupImportWorker).to receive(:perform_async).with(user.id, group.id)
import_service.async_execute

View File

@ -39,6 +39,10 @@ end
# let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
# subject { post create_self_monitoring_project_admin_application_settings_path }
RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do
before do
allow(worker_class).to receive(:with_status).and_return(worker_class)
end
it 'returns sidekiq job_id of expected length' do
subject

View File

@ -17,7 +17,7 @@ end
RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do
it 'returns true when job is enqueued' do
jid = described_class.perform_async
jid = described_class.with_status.perform_async
expect(described_class.in_progress?(jid)).to eq(true)
end

View File

@ -598,4 +598,48 @@ RSpec.describe ApplicationWorker do
end
end
end
describe '.with_status' do
around do |example|
Sidekiq::Testing.fake!(&example)
end
context 'when the worker does have status_expiration set' do
let(:status_expiration_worker) do
Class.new(worker) do
sidekiq_options status_expiration: 3
end
end
it 'uses status_expiration from the worker' do
status_expiration_worker.with_status.perform_async
expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3)
expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1)
end
it 'uses status_expiration from the worker without with_status' do
status_expiration_worker.perform_async
expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3)
expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1)
end
end
context 'when the worker does not have status_expiration set' do
it 'uses the default status_expiration' do
worker.with_status.perform_async
expect(Sidekiq::Queues[worker.queue].first).to include('status_expiration' => Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
expect(Sidekiq::Queues[worker.queue].length).to eq(1)
end
it 'does not set status_expiration without with_status' do
worker.perform_async
expect(Sidekiq::Queues[worker.queue].first).not_to include('status_expiration')
expect(Sidekiq::Queues[worker.queue].length).to eq(1)
end
end
end
end