Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-12 12:09:36 +00:00
parent 91e8c3a6ef
commit 57a3a42c88
42 changed files with 435 additions and 219 deletions

View File

@ -172,6 +172,8 @@ review-qa-smoke:
- .review-qa-base - .review-qa-base
- .review:rules:review-qa-smoke - .review:rules:review-qa-smoke
retry: 1 # This is confusing but this means "2 runs at max". retry: 1 # This is confusing but this means "2 runs at max".
variables:
QA_RUN_TYPE: review-qa-smoke
script: script:
- bin/test Test::Instance::Smoke "${CI_ENVIRONMENT_URL}" - bin/test Test::Instance::Smoke "${CI_ENVIRONMENT_URL}"
@ -180,6 +182,8 @@ review-qa-all:
- .review-qa-base - .review-qa-base
- .review:rules:review-qa-all - .review:rules:review-qa-all
parallel: 5 parallel: 5
variables:
QA_RUN_TYPE: review-qa-all
script: script:
- export KNAPSACK_REPORT_PATH=knapsack/master_report.json - export KNAPSACK_REPORT_PATH=knapsack/master_report.json
- export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb

View File

@ -8,7 +8,9 @@ import CiLint from './components/ci_lint.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers), defaultClient: createDefaultClient(resolvers, {
assumeImmutableResults: true,
}),
}); });
export default (containerId = '#js-ci-lint') => { export default (containerId = '#js-ci-lint') => {

View File

@ -90,17 +90,18 @@ export default {
showMore() { showMore() {
this.$emit('showMore'); this.$emit('showMore');
}, },
generateRowNumber(id) { generateRowNumber(path, id, index) {
const key = `${path}-${id}-${index}`;
if (!this.glFeatures.lazyLoadCommits) { if (!this.glFeatures.lazyLoadCommits) {
return 0; return 0;
} }
if (!this.rowNumbers[id] && this.rowNumbers[id] !== 0) { if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
this.$options.totalRowsLoaded += 1; this.$options.totalRowsLoaded += 1;
this.rowNumbers[id] = this.$options.totalRowsLoaded; this.rowNumbers[key] = this.$options.totalRowsLoaded;
} }
return this.rowNumbers[id]; return this.rowNumbers[key];
}, },
getCommit(fileName, type) { getCommit(fileName, type) {
if (!this.glFeatures.lazyLoadCommits) { if (!this.glFeatures.lazyLoadCommits) {
@ -150,7 +151,7 @@ export default {
:lfs-oid="entry.lfsOid" :lfs-oid="entry.lfsOid"
:loading-path="loadingPath" :loading-path="loadingPath"
:total-entries="totalEntries" :total-entries="totalEntries"
:row-number="generateRowNumber(entry.id)" :row-number="generateRowNumber(entry.flatPath, entry.id, index)"
:commit-info="getCommit(entry.name, entry.type)" :commit-info="getCommit(entry.name, entry.type)"
v-on="$listeners" v-on="$listeners"
/> />

View File

@ -35,13 +35,17 @@ export default {
} }
if (!this.rulesLeft.length) { if (!this.rulesLeft.length) {
return n__('Requires approval.', 'Requires %d more approvals.', this.approvalsLeft); return n__(
'Requires %d approval from eligible users.',
'Requires %d approvals from eligible users.',
this.approvalsLeft,
);
} }
return sprintf( return sprintf(
n__( n__(
'Requires approval from %{names}.', 'Requires %{count} approval from %{names}.',
'Requires %{count} more approvals from %{names}.', 'Requires %{count} approvals from %{names}.',
this.approvalsLeft, this.approvalsLeft,
), ),
{ {

View File

@ -71,7 +71,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="d-flex mr-source-target gl-mb-3"> <div class="gl-display-flex mr-source-target">
<mr-widget-icon name="git-merge" /> <mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex"> <div class="git-merge-container d-flex">
<div class="normal"> <div class="normal">

View File

@ -25,19 +25,15 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@members = GroupMembersFinder
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
if can?(current_user, :admin_group_member, @group) if can?(current_user, :admin_group_member, @group)
@skip_groups = @group.related_group_ids @skip_groups = @group.related_group_ids
@invited_members = @members.invite @invited_members = invited_members
@invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present? @invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
@invited_members = present_invited_members(@invited_members) @invited_members = present_invited_members(@invited_members)
end end
@members = present_group_members(@members.non_invite) @members = present_group_members(non_invited_members)
@requesters = present_members( @requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user) AccessRequestsFinder.new(@group).execute(current_user)
@ -51,6 +47,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
private private
def group_members
@group_members ||= GroupMembersFinder
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
end
def invited_members
group_members.invite
end
def non_invited_members
group_members.non_invite
end
def present_invited_members(invited_members) def present_invited_members(invited_members)
present_members(invited_members present_members(invited_members
.page(params[:invited_members_page]) .page(params[:invited_members_page])

View File

@ -19,16 +19,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links @group_links = @project.project_group_links
@group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present? @group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
project_members = MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
if can?(current_user, :admin_project_member, @project) if can?(current_user, :admin_project_member, @project)
@invited_members = present_members(project_members.invite) @invited_members = present_members(invited_members)
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
end end
@project_members = present_members(project_members.non_invite.page(params[:page])) @project_members = present_members(non_invited_members.page(params[:page]))
@project_member = @project.project_members.new @project_member = @project.project_members.new
end end
@ -55,6 +51,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController
private private
def members
@members ||= MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
end
def invited_members
members.invite
end
def non_invited_members
members.non_invite
end
def filter_params def filter_params
params.permit(:search).merge(sort: @sort) params.permit(:search).merge(sort: @sort)
end end

View File

@ -45,6 +45,11 @@ class RegistrationsController < Devise::RegistrationsController
end end
def destroy def destroy
if current_user.required_terms_not_accepted?
redirect_to profile_account_path, status: :see_other, alert: s_('Profiles|You must accept the Terms of Service in order to perform this action.')
return
end
if destroy_confirmation_valid? if destroy_confirmation_valid?
current_user.delete_async(deleted_by: current_user) current_user.delete_async(deleted_by: current_user)
session.try(:destroy) session.try(:destroy)

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Resolvers
class BoardListResolver < BaseResolver.single
include Gitlab::Graphql::Authorize::AuthorizeResource
include BoardItemFilterable
type Types::BoardListType, null: true
description 'Find an issue board list.'
authorize :read_issue_board_list
argument :id, Types::GlobalIDType[List],
required: true,
description: 'Global ID of the list.'
argument :issue_filters, Types::Boards::BoardIssueInputType,
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
def resolve(id: nil, issue_filters: {})
context.scoped_set!(:issue_filters, item_filters(issue_filters))
Gitlab::Graphql::Lazy.with_value(find_list(id: id)) do |list|
list if authorized_resource?(list)
end
end
private
def find_list(id:)
GitlabSchema.object_from_id(id, expected_type: ::List)
end
end
end

View File

@ -47,6 +47,19 @@ module Types
.metadata .metadata
end end
end end
# board lists have a data dependency on label - so we batch load them here
def title
if object.association(:label).loaded? && object.label_id.present?
object.title
else
loader = Gitlab::Graphql::Loaders::BatchModelLoader.new(Label, object.label_id)
Gitlab::Graphql::Lazy.with_value(loader.find) do |label|
object.label = label
object.title
end
end
end
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
end end

View File

@ -136,6 +136,10 @@ module Types
complexity: 5, complexity: 5,
resolver: ::Resolvers::TimelogResolver resolver: ::Resolvers::TimelogResolver
field :board_list, ::Types::BoardListType,
null: true,
resolver: Resolvers::BoardListResolver
def design_management def design_management
DesignManagementObject.new(nil) DesignManagementObject.new(nil)
end end

View File

@ -28,8 +28,8 @@ class Environment < ApplicationRecord
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment' has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) } has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) } has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
@ -198,14 +198,14 @@ class Environment < ApplicationRecord
# Overriding association # Overriding association
def last_visible_deployable def last_visible_deployable
return super if association_cached?(:last_visible_deployable) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) return super if association_cached?(:last_visible_deployable)
last_visible_deployment&.deployable last_visible_deployment&.deployable
end end
# Overriding association # Overriding association
def last_visible_pipeline def last_visible_pipeline
return super if association_cached?(:last_visible_pipeline) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) return super if association_cached?(:last_visible_pipeline)
last_visible_deployable&.pipeline last_visible_deployable&.pipeline
end end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class ListPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
delegate { @subject.board.resource_parent }
end

View File

@ -3,10 +3,15 @@
module Ci module Ci
class ArchiveTraceService class ArchiveTraceService
def execute(job, worker_name:) def execute(job, worker_name:)
unless job.trace.archival_attempts_available?
Sidekiq.logger.warn(class: worker_name, message: 'The job is out of archival attempts.', job_id: job.id)
job.trace.attempt_archive_cleanup!
return
end
unless job.trace.can_attempt_archival_now? unless job.trace.can_attempt_archival_now?
Sidekiq.logger.warn(class: worker_name, Sidekiq.logger.warn(class: worker_name, message: 'The job can not be archived right now.', job_id: job.id)
message: job.trace.archival_attempts_message,
job_id: job.id)
return return
end end

View File

@ -6,7 +6,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank') } = _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank', rel: 'noopener noreferrer') }
.settings-content .settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-mailgun-settings'), html: { class: 'fieldset-form', id: 'mailgun-settings' } do |f| = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-mailgun-settings'), html: { class: 'fieldset-form', id: 'mailgun-settings' } do |f|
= form_errors(@application_setting) if expanded = form_errors(@application_setting) if expanded

View File

@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('development/snowplow/index') } - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('development/snowplow/index') }
= html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank').html_safe, link_start: link_start, link_end: '</a>'.html_safe } = html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank', rel: 'noopener noreferrer').html_safe, link_start: link_start, link_end: '</a>'.html_safe }
.settings-content .settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f| = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f|
= form_errors(@application_setting) if expanded = form_errors(@application_setting) if expanded

View File

@ -1,8 +0,0 @@
---
name: environment_last_visible_pipeline_disable_joins
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68870
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340283
milestone: '14.3'
type: development
group: group::release
default_enabled: true

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CleanupDeleteOrphanedDeploymentsBackgroundMigration < Gitlab::Database::Migration[1.0]
MIGRATION = 'DeleteOrphanedDeployments'
disable_ddl_transaction!
def up
finalize_background_migration(MIGRATION)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
5701681a1006584149c88da520f780b186ca32ba1facb8b952252c6d426b6c0d

View File

@ -34,7 +34,7 @@ GET /projects/:id/dependencies?package_manager=yarn,bundler
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). | | `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`, `maven`, `npm`, `pip` or `yarn`. | | `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`. |
```shell ```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies"

View File

@ -36,6 +36,19 @@ in [Removed Items](../removed_items.md).
The `Query` type contains the API's top-level entry points for all executable queries. The `Query` type contains the API's top-level entry points for all executable queries.
### `Query.boardList`
Find an issue board list.
Returns [`BoardList`](#boardlist).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryboardlistid"></a>`id` | [`ListID!`](#listid) | Global ID of the list. |
| <a id="queryboardlistissuefilters"></a>`issueFilters` | [`BoardIssueInput`](#boardissueinput) | Filters applied when getting issue metadata in the board list. |
### `Query.ciApplicationSettings` ### `Query.ciApplicationSettings`
CI related settings that apply to the entire instance. CI related settings that apply to the entire instance.

View File

@ -275,7 +275,8 @@ You can verify if the MR was deployed to GitLab.com by executing
`/chatops run auto_deploy status <merge_sha>`. To verify existence of `/chatops run auto_deploy status <merge_sha>`. To verify existence of
the index, you can: the index, you can:
- Use a meta-command in #database-lab, such as: `\di <index_name>` - Use a meta-command in #database-lab, such as: `\d <index_name>`
- Ensure that the index is not [`invalid`](https://www.postgresql.org/docs/12/sql-createindex.html#:~:text=The%20psql%20%5Cd%20command%20will%20report%20such%20an%20index%20as%20INVALID)
- Ask someone in #database to check if the index exists - Ask someone in #database to check if the index exists
- With proper access, you can also verify directly on production or in a - With proper access, you can also verify directly on production or in a
production clone production clone

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto type: howto
--- ---
# Activating GitLab EE # Activating GitLab EE **(PREMIUM SELF)**
To enable features of GitLab Enterprise Edition (EE), you need to activate your instance. Ensure you are running an enterprise edition. To verify, sign in to GitLab and browse to `/help`. The GitLab edition and version are listed at the top of the **Help** page. To enable features of GitLab Enterprise Edition (EE), you need to activate your instance. Ensure you are running an enterprise edition. To verify, sign in to GitLab and browse to `/help`. The GitLab edition and version are listed at the top of the **Help** page.
@ -15,7 +15,7 @@ As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an
uploaded license only has the Free features active. A trial license activates all Ultimate features, but after [the trial expires](#what-happens-when-your-license-expires), some functionality uploaded license only has the Free features active. A trial license activates all Ultimate features, but after [the trial expires](#what-happens-when-your-license-expires), some functionality
is locked. is locked.
## Activate GitLab EE with an Activation Code **(PREMIUM SELF)** ## Activate GitLab EE with an Activation Code
As of GitLab Enterprise Edition 14.1, you need an activation code to activate your instance. You can obtain an activation code by [purchasing a license](https://about.gitlab.com/pricing/) or by signing up for a [free trial](https://about.gitlab.com/free-trial/). This activation code is a 24-character alphanumeric string you receive in a confirmation email. You can also sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) to copy the activation code to your clipboard. As of GitLab Enterprise Edition 14.1, you need an activation code to activate your instance. You can obtain an activation code by [purchasing a license](https://about.gitlab.com/pricing/) or by signing up for a [free trial](https://about.gitlab.com/free-trial/). This activation code is a 24-character alphanumeric string you receive in a confirmation email. You can also sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) to copy the activation code to your clipboard.
@ -28,7 +28,7 @@ To begin the activation process with your activation code:
1. Read and accept the terms of service. 1. Read and accept the terms of service.
1. Select **Activate**. 1. Select **Activate**.
## Activate GitLab EE with a License File **(PREMIUM SELF)** ## Activate GitLab EE with a License File
If you receive a license file from GitLab (for example a new trial), you can upload it by signing into your GitLab instance as an administrator or adding it during installation. The license is a base64-encoded ASCII text file with a `.gitlab-license` extension. If you receive a license file from GitLab (for example a new trial), you can upload it by signing into your GitLab instance as an administrator or adding it during installation. The license is a base64-encoded ASCII text file with a `.gitlab-license` extension.

View File

@ -25,7 +25,7 @@ module Gitlab
delegate :old_trace, to: :job delegate :old_trace, to: :job
delegate :can_attempt_archival_now?, :increment_archival_attempts!, delegate :can_attempt_archival_now?, :increment_archival_attempts!,
:archival_attempts_message, to: :trace_metadata :archival_attempts_message, :archival_attempts_available?, to: :trace_metadata
def initialize(job) def initialize(job)
@job = job @job = job
@ -122,6 +122,10 @@ module Gitlab
end end
end end
def attempt_archive_cleanup!
destroy_any_orphan_trace_data!
end
def update_interval def update_interval
if being_watched? if being_watched?
UPDATE_FREQUENCY_WHEN_BEING_WATCHED UPDATE_FREQUENCY_WHEN_BEING_WATCHED
@ -191,7 +195,10 @@ module Gitlab
def unsafe_archive! def unsafe_archive!
raise ArchiveError, 'Job is not finished yet' unless job.complete? raise ArchiveError, 'Job is not finished yet' unless job.complete?
unsafe_trace_conditionally_cleanup_before_retry! already_archived?.tap do |archived|
destroy_any_orphan_trace_data!
raise AlreadyArchivedError, 'Could not archive again' if archived
end
if job.trace_chunks.any? if job.trace_chunks.any?
Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream| Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
@ -214,16 +221,15 @@ module Gitlab
def already_archived? def already_archived?
# TODO check checksum to ensure archive completed successfully # TODO check checksum to ensure archive completed successfully
# See https://gitlab.com/gitlab-org/gitlab/-/issues/259619 # See https://gitlab.com/gitlab-org/gitlab/-/issues/259619
trace_artifact.archived_trace_exists? trace_artifact&.archived_trace_exists?
end end
def unsafe_trace_conditionally_cleanup_before_retry! def destroy_any_orphan_trace_data!
return unless trace_artifact return unless trace_artifact
if already_archived? if already_archived?
# An archive already exists, so make sure to remove the trace chunks # An archive already exists, so make sure to remove the trace chunks
erase_trace_chunks! erase_trace_chunks!
raise AlreadyArchivedError, 'Could not archive again'
else else
# An archive already exists, but its associated file does not, so remove it # An archive already exists, but its associated file does not, so remove it
trace_artifact.destroy! trace_artifact.destroy!

View File

@ -11,6 +11,12 @@ module Gitlab
# balancing is enabled, but no replicas have been configured (= the # balancing is enabled, but no replicas have been configured (= the
# default case). # default case).
class PrimaryHost class PrimaryHost
WAL_ERROR_MESSAGE = <<~MSG.strip
Obtaining WAL information when not using any replicas results in
redundant queries, and may break installations that don't support
streaming replication (e.g. AWS' Aurora database).
MSG
def initialize(load_balancer) def initialize(load_balancer)
@load_balancer = load_balancer @load_balancer = load_balancer
end end
@ -51,30 +57,16 @@ module Gitlab
end end
def primary_write_location def primary_write_location
@load_balancer.primary_write_location raise NotImplementedError, WAL_ERROR_MESSAGE
end end
def database_replica_location def database_replica_location
row = query_and_release(<<-SQL.squish) raise NotImplementedError, WAL_ERROR_MESSAGE
SELECT pg_last_wal_replay_lsn()::text AS location
SQL
row['location'] if row.any?
rescue *Host::CONNECTION_ERRORS
nil
end end
def caught_up?(_location) def caught_up?(_location)
true true
end end
def query_and_release(sql)
connection.select_all(sql).first || {}
rescue StandardError
{}
ensure
release_connection
end
end end
end end
end end

View File

@ -42,6 +42,9 @@ module Gitlab
end end
def wal_location_for(load_balancer) def wal_location_for(load_balancer)
# When only using the primary there's no need for any WAL queries.
return if load_balancer.primary_only?
if ::Gitlab::Database::LoadBalancing::Session.current.use_primary? if ::Gitlab::Database::LoadBalancing::Session.current.use_primary?
load_balancer.primary_write_location load_balancer.primary_write_location
else else

View File

@ -104,6 +104,10 @@ module Gitlab
end end
def with_primary_write_location def with_primary_write_location
# When only using the primary, there's no point in getting write
# locations, as the primary is always in sync with itself.
return if @load_balancer.primary_only?
location = @load_balancer.primary_write_location location = @load_balancer.primary_write_location
return if location.blank? return if location.blank?

View File

@ -26312,6 +26312,9 @@ msgstr ""
msgid "Profiles|You don't have access to delete this user." msgid "Profiles|You don't have access to delete this user."
msgstr "" msgstr ""
msgid "Profiles|You must accept the Terms of Service in order to perform this action."
msgstr ""
msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account" msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account"
msgstr "" msgstr ""
@ -28973,13 +28976,13 @@ msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture." msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
msgstr "" msgstr ""
msgid "Requires approval from %{names}." msgid "Requires %d approval from eligible users."
msgid_plural "Requires %{count} more approvals from %{names}." msgid_plural "Requires %d approvals from eligible users."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Requires approval." msgid "Requires %{count} approval from %{names}."
msgid_plural "Requires %d more approvals." msgid_plural "Requires %{count} approvals from %{names}."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
@ -40209,6 +40212,9 @@ msgstr ""
msgid "element is not a hierarchy" msgid "element is not a hierarchy"
msgstr "" msgstr ""
msgid "eligible users"
msgstr ""
msgid "email '%{email}' is not a verified email." msgid "email '%{email}' is not a verified email."
msgstr "" msgstr ""

View File

@ -55,9 +55,9 @@
"@babel/preset-env": "^7.10.1", "@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.7", "@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0", "@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.215.0", "@gitlab/svgs": "1.218.0",
"@gitlab/tributejs": "1.0.0", "@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "32.18.0", "@gitlab/ui": "32.19.1",
"@gitlab/visual-review-tools": "1.6.1", "@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.4-1", "@rails/actioncable": "6.1.4-1",
"@rails/ujs": "6.1.4-1", "@rails/ujs": "6.1.4-1",

View File

@ -20,11 +20,6 @@ module Gitlab
def additional_limits def additional_limits
additional_minutes_usage[%r{([^/ ]+)$}] additional_minutes_usage[%r{([^/ ]+)$}]
end end
# TODO: Refactor/Remove this method once https://gitlab.com/gitlab-org/quality/chemlab/-/merge_requests/28 is merged
def additional_minutes_exist?
has_element?(:strong, :additional_minutes, text: 'Additional minutes')
end
end end
end end
end end

View File

@ -602,6 +602,22 @@ RSpec.describe RegistrationsController do
end end
end end
context 'when user did not accept app terms' do
let(:user) { create(:user, accepted_term: nil) }
before do
stub_application_setting(password_authentication_enabled_for_web: false)
stub_application_setting(password_authentication_enabled_for_git: false)
stub_application_setting(enforce_terms: true)
end
it 'fails with message' do
post :destroy, params: { username: user.username }
expect_failure(s_('Profiles|You must accept the Terms of Service in order to perform this action.'))
end
end
it 'sets the username and caller_id in the context' do it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args| expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
m.call(*args) m.call(*args)

View File

@ -61,9 +61,7 @@ describe('MRWidget approvals summary', () => {
it('render message', () => { it('render message', () => {
const names = toNounSeriesText(testRulesLeft()); const names = toNounSeriesText(testRulesLeft());
expect(wrapper.text()).toContain( expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} approvals from ${names}.`);
`Requires ${TEST_APPROVALS_LEFT} more approvals from ${names}.`,
);
}); });
}); });
@ -75,7 +73,9 @@ describe('MRWidget approvals summary', () => {
}); });
it('renders message', () => { it('renders message', () => {
expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} more approvals.`); expect(wrapper.text()).toContain(
`Requires ${TEST_APPROVALS_LEFT} approvals from eligible users`,
);
}); });
}); });

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::BoardListResolver do
include GraphqlHelpers
include Gitlab::Graphql::Laziness
let_it_be(:guest) { create(:user) }
let_it_be(:unauth_user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') }
let_it_be(:board) { create(:board, resource_parent: group) }
let_it_be(:label_list) { create(:list, board: board, label: group_label) }
describe '#resolve' do
subject { resolve_board_list(args: { id: global_id_of(label_list) }, current_user: current_user) }
context 'with unauthorized user' do
let(:current_user) { unauth_user }
it { is_expected.to be_nil }
end
context 'when authorized' do
let(:current_user) { guest }
before do
group.add_guest(guest)
end
it { is_expected.to eq label_list }
end
end
def resolve_board_list(args: {}, current_user: user)
force(resolve(described_class, obj: nil, args: args, ctx: { current_user: current_user }))
end
end

View File

@ -27,6 +27,7 @@ RSpec.describe GitlabSchema.types['Query'] do
runner runner
runners runners
timelogs timelogs
board_list
] ]
expect(described_class).to have_graphql_fields(*expected_fields).at_least expect(described_class).to have_graphql_fields(*expected_fields).at_least
@ -136,4 +137,14 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_resolver(Resolvers::TimelogResolver) is_expected.to have_graphql_resolver(Resolvers::TimelogResolver)
end end
end end
describe 'boardList field' do
subject { described_class.fields['boardList'] }
it 'finds a board list by its gid' do
is_expected.to have_graphql_arguments(:id, :issue_filters)
is_expected.to have_graphql_type(Types::BoardListType)
is_expected.to have_graphql_resolver(Resolvers::BoardListResolver)
end
end
end end

View File

@ -63,9 +63,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end end
describe '#primary_write_location' do describe '#primary_write_location' do
it 'returns the write location of the primary' do it 'raises NotImplementedError' do
expect(host.primary_write_location).to be_an_instance_of(String) expect { host.primary_write_location }.to raise_error(NotImplementedError)
expect(host.primary_write_location).not_to be_empty
end end
end end
@ -76,51 +75,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end end
describe '#database_replica_location' do describe '#database_replica_location' do
let(:connection) { double(:connection) } it 'raises NotImplementedError' do
expect { host.database_replica_location }.to raise_error(NotImplementedError)
it 'returns the write ahead location of the replica', :aggregate_failures do
expect(host)
.to receive(:query_and_release)
.and_return({ 'location' => '0/D525E3A8' })
expect(host.database_replica_location).to be_an_instance_of(String)
end
it 'returns nil when the database query returned no rows' do
expect(host).to receive(:query_and_release).and_return({})
expect(host.database_replica_location).to be_nil
end
it 'returns nil when the database connection fails' do
allow(host).to receive(:connection).and_raise(PG::Error)
expect(host.database_replica_location).to be_nil
end
end
describe '#query_and_release' do
it 'executes a SQL query' do
results = host.query_and_release('SELECT 10 AS number')
expect(results).to be_an_instance_of(Hash)
expect(results['number'].to_i).to eq(10)
end
it 'releases the connection after running the query' do
expect(host)
.to receive(:release_connection)
.once
host.query_and_release('SELECT 10 AS number')
end
it 'returns an empty Hash in the event of an error' do
expect(host.connection)
.to receive(:select_all)
.and_raise(RuntimeError, 'kittens')
expect(host.query_and_release('SELECT 10 AS number')).to eq({})
end end
end end
end end

View File

@ -21,7 +21,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
middleware.call(worker_class, job, nil, nil) {} middleware.call(worker_class, job, nil, nil) {}
end end
describe '#call' do describe '#call', :database_replica do
shared_context 'data consistency worker class' do |data_consistency, feature_flag| shared_context 'data consistency worker class' do |data_consistency, feature_flag|
let(:expected_consistency) { data_consistency } let(:expected_consistency) { data_consistency }
let(:worker_class) do let(:worker_class) do

View File

@ -188,6 +188,10 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end end
it 'sticks an entity to the primary', :aggregate_failures do it 'sticks an entity to the primary', :aggregate_failures do
allow(ActiveRecord::Base.connection.load_balancer)
.to receive(:primary_only?)
.and_return(false)
ids.each do |id| ids.each do |id|
expect(sticking) expect(sticking)
.to receive(:set_write_location_for) .to receive(:set_write_location_for)
@ -199,6 +203,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
subject subject
end end
it 'does not update the write location when no replicas are used' do
expect(sticking).not_to receive(:set_write_location_for)
subject
end
end end
describe '#stick' do describe '#stick' do
@ -221,12 +231,22 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
.to receive(:primary_write_location) .to receive(:primary_write_location)
.and_return('foo') .and_return('foo')
allow(ActiveRecord::Base.connection.load_balancer)
.to receive(:primary_only?)
.and_return(false)
expect(sticking) expect(sticking)
.to receive(:set_write_location_for) .to receive(:set_write_location_for)
.with(:user, 42, 'foo') .with(:user, 42, 'foo')
sticking.mark_primary_write_location(:user, 42) sticking.mark_primary_write_location(:user, 42)
end end
it 'does nothing when no replicas are used' do
expect(sticking).not_to receive(:set_write_location_for)
sticking.mark_primary_write_location(:user, 42)
end
end end
describe '#unstick' do describe '#unstick' do

View File

@ -2791,17 +2791,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
extra_update_queries = 4 # transition ... => :canceled, queue pop extra_update_queries = 4 # transition ... => :canceled, queue pop
extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types
# The number of extra load balancing queries depends on whether or not expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries)
# we use a load balancer for CI. That in turn depends on the contents of
# database.yml, so here we support both cases.
extra_load_balancer_queries =
if Gitlab::Database.has_config?(:ci)
6
else
3
end
expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries + extra_load_balancer_queries)
end end
end end

View File

@ -801,38 +801,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(query_count).to eq(0) expect(query_count).to eq(0)
end end
end end
context 'when the feature for disable_join is disabled' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, project: project, pipeline: pipeline) }
before do
stub_feature_flags(environment_last_visible_pipeline_disable_joins: false)
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build)
end
context 'for preload' do
it 'executes the original association instead of override' do
environment.reload
ActiveRecord::Associations::Preloader.new.preload(environment, [last_visible_deployable: []])
expect_any_instance_of(Deployment).not_to receive(:deployable)
query_count = ActiveRecord::QueryRecorder.new do
expect(subject.id).to eq(ci_build.id)
end.count
expect(query_count).to eq(0)
end
end
context 'for direct call' do
it 'executes the original association instead of override' do
expect_any_instance_of(Deployment).not_to receive(:deployable)
expect(subject.id).to eq(ci_build.id)
end
end
end
end end
describe '#last_visible_pipeline' do describe '#last_visible_pipeline' do
@ -963,40 +931,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(query_count).to eq(0) expect(query_count).to eq(0)
end end
end end
context 'when the feature for disable_join is disabled' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, project: project, pipeline: pipeline) }
before do
stub_feature_flags(environment_last_visible_pipeline_disable_joins: false)
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build)
end
subject { environment.last_visible_pipeline }
context 'for preload' do
it 'executes the original association instead of override' do
environment.reload
ActiveRecord::Associations::Preloader.new.preload(environment, [last_visible_pipeline: []])
expect_any_instance_of(Ci::Build).not_to receive(:pipeline)
query_count = ActiveRecord::QueryRecorder.new do
expect(subject.id).to eq(pipeline.id)
end.count
expect(query_count).to eq(0)
end
end
context 'for direct call' do
it 'executes the original association instead of override' do
expect_any_instance_of(Ci::Build).not_to receive(:pipeline)
expect(subject.id).to eq(pipeline.id)
end
end
end
end end
describe '#upcoming_deployment' do describe '#upcoming_deployment' do

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Querying a Board list' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:board) { create(:board, resource_parent: project) }
let_it_be(:label) { create(:label, project: project, name: 'foo') }
let_it_be(:list) { create(:list, board: board, label: label) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label]) }
let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) }
let(:filters) { {} }
let(:query) do
graphql_query_for(
:board_list,
{ id: list.to_global_id.to_s, issueFilters: filters },
%w[title issuesCount]
)
end
subject { graphql_data['boardList'] }
before do
post_graphql(query, current_user: current_user)
end
context 'when the user has access to the list' do
before_all do
project.add_guest(current_user)
end
it_behaves_like 'a working graphql query'
it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) }
context 'with matching issue filters' do
let(:filters) { { assigneeUsername: current_user.username } }
it 'filters issues metadata' do
is_expected.to include({ 'issuesCount' => 1, 'title' => list.title })
end
end
context 'with unmatching issue filters' do
let(:filters) { { assigneeUsername: 'foo' } }
it 'filters issues metadata' do
is_expected.to include({ 'issuesCount' => 0, 'title' => list.title })
end
end
end
context 'when the user does not have access to the list' do
it { is_expected.to be_nil }
end
context 'when ID argument is missing' do
let(:query) do
graphql_query_for('boardList', {}, 'title')
end
it 'raises an exception' do
expect(graphql_errors).to include(a_hash_including('message' => "Field 'boardList' is missing required arguments: id"))
end
end
context 'when list ID is not found' do
let(:query) do
graphql_query_for('boardList', { id: "gid://gitlab/List/#{non_existing_record_id}" }, 'title')
end
it { is_expected.to be_nil }
end
it 'does not have an N+1 performance issue' do
a, b = create_list(:list, 2, board: board)
ctx = { current_user: current_user }
project.add_guest(current_user)
baseline = graphql_query_for(:board_list, { id: global_id_of(a) }, 'title')
query = <<~GQL
query {
a: #{query_graphql_field(:board_list, { id: global_id_of(a) }, 'title')}
b: #{query_graphql_field(:board_list, { id: global_id_of(b) }, 'title')}
}
GQL
control = ActiveRecord::QueryRecorder.new do
run_with_clean_state(baseline, context: ctx)
end
expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control)
end
end

View File

@ -88,6 +88,32 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
subject subject
end end
context 'job has archive and chunks' do
let(:job) { create(:ci_build, :success, :trace_artifact) }
before do
create(:ci_build_trace_chunk, build: job, chunk_index: 0)
end
context 'archive is not completed' do
before do
job.job_artifacts_trace.file.remove!
end
it 'cleanups any stale archive data' do
expect(job.job_artifacts_trace).to be_present
subject
expect(job.reload.job_artifacts_trace).to be_nil
end
end
it 'removes trace chunks' do
expect { subject }.to change { job.trace_chunks.count }.to(0)
end
end
end end
context 'when the archival process is backed off' do context 'when the archival process is backed off' do

View File

@ -904,23 +904,23 @@
stylelint-declaration-strict-value "1.7.7" stylelint-declaration-strict-value "1.7.7"
stylelint-scss "3.18.0" stylelint-scss "3.18.0"
"@gitlab/svgs@1.215.0": "@gitlab/svgs@1.218.0":
version "1.215.0" version "1.218.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.215.0.tgz#f2760bbb0a38b26346e1b755e63fb63eba005edd" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.218.0.tgz#0715e2ef50b5cb83813e1a5e29d5919a96685734"
integrity sha512-/bc0+EOYPQlPCMbfyOkMLxDKBn+ewEBlmTRmFwf7mXvfIRszdJPY8XCx/fJIEQwDr8+k4E28ktFnLZGnaFhCnw== integrity sha512-eckixyumeWogykEUZfP4pGjoRdhdWQIFwSTM0ks5tQqza+BikcL2xvxzicJs69T1IiCKwYtEpR1c3T/hSx39Mg==
"@gitlab/tributejs@1.0.0": "@gitlab/tributejs@1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@32.18.0": "@gitlab/ui@32.19.1":
version "32.18.0" version "32.19.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.18.0.tgz#cd340f050fe0183218f6233328aca2369bd6e449" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.19.1.tgz#ab54408272cb5ee695dc0a328892e047da3d41ac"
integrity sha512-bDMmsNB9VMBX2JbezyJWfk02t0aFfAT9Ez4ALTDUJLb5/Q9GKByfE5sLycms6L1aZxzP6r1jypnu5DD0eT92eg== integrity sha512-ooc0TwCvREuWJfvn8EbOkEz1Mh4UKEu7x0MKhD+TBjG+JJwLKDClmD1cPPE05BXtWAvW5W9JUBkaeMCVQG2l3g==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
bootstrap-vue "2.19.0" bootstrap-vue "2.20.1"
copy-to-clipboard "^3.0.8" copy-to-clipboard "^3.0.8"
dompurify "^2.3.3" dompurify "^2.3.3"
echarts "^4.9.0" echarts "^4.9.0"
@ -2758,10 +2758,10 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bootstrap-vue@2.19.0: bootstrap-vue@2.20.1:
version "2.19.0" version "2.20.1"
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.19.0.tgz#5019df48251e552a5c34da57fc97dabebd53b02f" resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.20.1.tgz#1b6cd4368632c1a6dd4a5ed161242baa131c3cd5"
integrity sha512-IjAXUSrRU5Qu9x3uwUcoj6LtysKbCVeWoJOsODyI/WokStUr95M+tTIajXUjIrB/Nsk0fS+RNvZnm2sWeNFrhg== integrity sha512-s+w83q0T2mo/RbFwTM8gExbLJMEOYpdTUqmyFaHv2Ir+TFprMvTWpeAzeNuawJ130W1gePZ3LW3cNp1t/tZbOw==
dependencies: dependencies:
"@nuxt/opencollective" "^0.3.2" "@nuxt/opencollective" "^0.3.2"
bootstrap ">=4.5.3 <5.0.0" bootstrap ">=4.5.3 <5.0.0"