Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-12 06:08:53 +00:00
parent db4ee69eb3
commit a1e664d4cc
15 changed files with 539 additions and 45 deletions

View File

@ -102,33 +102,16 @@ module RelativePositioning
delta = at_end ? gap : -gap
indexed = (at_end ? objects : objects.reverse).each_with_index
# Some classes are polymorphic, and not all siblings are in the same table.
by_model = indexed.group_by { |pair| pair.first.class }
lower_bound, upper_bound = at_end ? [position, MAX_POSITION] : [MIN_POSITION, position]
by_model.each do |model, pairs|
model.transaction do
pairs.each_slice(100) do |batch|
# These are known to be integers, one from the DB, and the other
# calculated by us, and thus safe to interpolate
values = batch.map do |obj, i|
desired_pos = position + delta * (i + 1)
pos = desired_pos.clamp(lower_bound, upper_bound)
obj.relative_position = pos
"(#{obj.id}, #{pos})"
end.join(', ')
model.connection.exec_query(<<~SQL, "UPDATE #{model.table_name} positions")
WITH cte(cte_id, new_pos) AS (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
)
UPDATE #{model.table_name}
SET relative_position = cte.new_pos
FROM cte
WHERE cte_id = id
SQL
representative.model_class.transaction do
indexed.each_slice(100) do |batch|
mapping = batch.to_h.transform_values! do |i|
desired_pos = position + delta * (i + 1)
{ relative_position: desired_pos.clamp(lower_bound, upper_bound) }
end
::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping, &:model_class)
end
end
@ -206,4 +189,10 @@ module RelativePositioning
def reset_relative_position
reset.relative_position
end
# Override if the model class needs a more complicated computation (e.g. the
# object is a member of a union).
def model_class
self.class
end
end

View File

@ -2,15 +2,39 @@
# rubocop:disable Style/SignalException
PATTERNS = %w[
%a.btn.btn-
%button.btn.btn-
.alert
.alert-danger
.alert-dismissible
.alert-info
.alert-link
.alert-primary
.alert-success
.alert-warning
.nav-tabs
.toolbar-button-icon
.tooltip
.tooltip-inner
<button
<tabs
bs-callout
createFlash
deprecated-modal
gl-deprecated-button
loading-button
pagination-button
gl-deprecated-dropdown
gl-deprecated-dropdown-divider
gl-deprecated-dropdown-header
gl-deprecated-dropdown-item
graphql_pagination
has-tooltip
has_tooltip
initDeprecatedJQueryDropdown
loading-button
pagination-button
v-popover
v-tooltip
with_tooltip
].freeze
BLOCKING_PATTERNS = %w[

View File

@ -0,0 +1,61 @@
---
- title: Use HashiCorp Vault secrets in CI jobs
body: In GitLab 12.10, GitLab introduced functionality for GitLab Runner to fetch and inject secrets into CI jobs. GitLab is now expanding the JWT Vault Authentication method by building a new secrets syntax in the .gitlab-ci.yml file. This makes it easier for you to configure and use HashiCorp Vault with GitLab.
stage: Release
self-managed: true
gitlab-com: true
packages: [Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/secrets
image_url: https://about.gitlab.com/images/13_4/vault_ci.png
published_at: 2020-09-22
release: 13.4
- title: Introducing the GitLab Kubernetes Agent
body: "GitLab's Kubernetes integration has long enabled deployment to Kubernetes clusters without manual setup. Many users have enjoyed the ease-of-use, while others have run into some challenges. The current integration requires your cluster to be open to the Internet for GitLab to access it. For many organizations, this isn't possible, because they must lock down their cluster access for security, compliance, or regulatory purposes. To work around these restrictions, users needed to create custom tooling on top of GitLab, or they couldn't use the feature. Today, we're announcing the GitLab Kubernetes Agent: a new way to deploy to Kubernetes clusters. The Agent runs inside of your cluster, so you don't need to open it to the internet. The Agent orchestrates deployments by pulling new changes from GitLab, rather than GitLab pushing updates to the cluster. No matter what method of GitOps you use, GitLab has you covered."
stage: Configure
self-managed: true
gitlab-com: false
packages: [Premium, Ultimate]
url: https://docs.gitlab.com/ee/user/clusters/agent
image_url: https://about.gitlab.com/images/13_4/gitops-header.png
published_at: 2020-09-22
release: 13.4
- title: Grant users deployment permissions without code access
body: If your team needs to maintain separation of duties between team members who own development, and team members who own deployments, the permissions paradigm in GitLab has made this challenging. In GitLab 13.4, you can give non-code contributors permission to approve merge requests for deployment, and actually deploy code, without also granting them maintainer access.
stage: Release
self-managed: true
gitlab-com: true
packages: [Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/environments/protected_environments.html#environment-access-by-group-membership
image_url: https://about.gitlab.com/images/13_4/deployer-role.png
published_at: 2020-09-22
release: 13.4
- title: Security Center
body: We've made a foundational change to security visibility and management in GitLab. The Instance Security Dashboard has been transformed into a Security Center. The biggest change is introducing a new menu structure. Rather than a single page, you can now find a Security Dashboard, Vulnerability Report, and Settings area. While the functionality hasn't changed, breaking things apart enables future enhancements that would have been difficult otherwise. This also creates a top-level framework for including other security-related functionality in the future. The dedicated Vulnerability Report now has more room to display important details and inherits those currently found on the Project vulnerability list. Separating the vulnerability metrics widgets into their own area creates a true Security Dashboard.
stage: Secure
self-managed: true
gitlab-com: true
packages: [Ultimate]
url: https://docs.gitlab.com/ee/user/application_security/security_dashboard/#instance-security-center
image_url: https://about.gitlab.com/images/13_4/instance_vulnerability_report.png
published_at: 2020-09-22
release: 13.4
- title: Feature Flags made available in GitLab Starter
body: Earlier this year, GitLab committed to moving 18 features to our open source core product. With this release of GitLab we've finished moving Feature Flags to Starter, and we are continuing to migrate our Feature Flag service to Core in GitLab 13.5. We're excited about bringing these features to more users and seeing what use cases and workflows you use them for.
stage: Release
self-managed: true
gitlab-com: true
packages: [starter, premium, ultimate]
url: https://www.youtube.com/embed/1FBRaBQTQZk
image_url: http://i3.ytimg.com/vi/1FBRaBQTQZk/maxresdefault.jpg
published_at: 2020-09-22
release: 13.4
- title: Quick navigation using the Search bar
body: When navigating through GitLab, sometimes you just want to go to a specific project and not a search result page. Using the Global search bar, you can now quickly jump to recent issues, groups, projects, settings, and help topics. You can even use the `/` keyboard shortcut to move the cursor to the search bar, to navigate GitLab even more efficiently!
stage: Enablement
self-managed: true
gitlab-com: true
packages: [core, starter, premium, ultimate]
url: https://docs.gitlab.com/ee/user/search/#autocomplete-suggestions
image_url: https://about.gitlab.com/images/13_4/enablement_global_search.gif
published_at: 2020-09-22
release: 13.4

View File

@ -31,7 +31,7 @@ the `author` field. GitLab team members **should not**.
- Any change that introduces a database migration, whether it's regular, post,
or data migration, **must** have a changelog entry, even if it is behind a
disabled feature flag. Since the migration is executed on [GitLab FOSS](https://gitlab.com/gitlab-org/gitlab-foss/),
disabled feature flag. Since the migration is executed on [GitLab FOSS](https://gitlab.com/gitlab-org/gitlab-foss/),
the changelog for database schema changes should be written to the
`changelogs/unreleased/` directory, even when other elements of that change affect only GitLab EE.

View File

@ -15,7 +15,6 @@ Because of the way [Ruby on Rails manages database
connections](#connection-lifecycle), it is important that we have at
least as many connections as we have threads. While there is a 'pool'
setting in [`database.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/database.yml.postgresql), it is not very practical because you need to
maintain it in tandem with the number of application threads. Because
maintain it in tandem with the number of application threads. For this
reason, we override the number of allowed connections in the database
connection-pool based on the configured number of application threads.

View File

@ -57,6 +57,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
- [Query Count Limits](../query_count_limits.md)
- [Creating enums](../creating_enums.md)
- [Client-side connection-pool](client_side_connection_pool.md)
- [Updating multiple values](./setting_multiple_values.md)
## Case studies

View File

@ -0,0 +1,103 @@
# Setting Multiple Values
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32921) in GitLab 13.5.
Frequently, we will want to update multiple objects with new values for one
or more columns. The obvious way to do this is using `Relation#update_all`:
```ruby
user.issues.open.update_all(due_date: 7.days.from_now) # (1)
user.issues.update_all('relative_position = relative_position + 1') # (2)
```
But what do you do if you cannot express the update as either a static value (1)
or as a calculation (2)?
Thankfully we can use `UPDATE FROM` to express the need to update multiple rows
with distinct values in a single query. One can either use a temporary table, or
a Common Table Expression (CTE), and then use that as the source of the updates:
```sql
with updates(obj_id, new_title, new_weight) as (
values (1 :: integer, 'Very difficult issue' :: text, 8 :: integer),
(2, 'Very easy issue', 1)
)
update issues
set title = new_title, weight = new_weight
from updates
where id = obj_id
```
The bad news: There is no way to express this in ActiveRecord or even dropping
down to ARel - the `UpdateManager` just does not support `update from`, so this
is not expressible.
The good news: We supply an abstraction to help you generate these kinds of
updates, called `Gitlab::Database::BulkUpdate`. This constructs queries such as the
above, and uses binding parameters to avoid SQL injection.
## Usage
To use this, we need:
- the list of columns to update
- a mapping from object/ID to the new values to set for that object
- a way to determine the table for each object
So for example, we can express the query above as:
```ruby
issue_a = Issue.find(..)
issue_b = Issue.find(..)
# Issues a single query:
::Gitlab::Database::BulkUpdate.execute(%i[title weight], {
issue_a => { title: 'Very difficult issue', weight: 8 },
issue_b => { title: 'Very easy issue', weight: 1 }
})
```
Here the table can be determined automatically, from calling
`object.class.table_name`, so we don't need to provide anything.
We can even pass heterogeneous sets of objects, if the updates all make sense
for them:
```ruby
issue_a = Issue.find(..)
issue_b = Issue.find(..)
merge_request = MergeRequest.find(..)
# Issues two queries
::Gitlab::Database::BulkUpdate.execute(%i[title], {
issue_a => { title: 'A' },
issue_b => { title: 'B' },
merge_request => { title: 'B' }
})
```
If your objects do not return the correct model class (perhaps because they are
part of a union), then we need to specify this explicitly in a block:
```ruby
bazzes = params
objects = Foo.from_union([
Foo.select("id, 'foo' as object_type").where(quux: true),
Bar.select("id, 'bar' as object_type").where(wibble: true)
])
# At this point, all the objects are instances of Foo, even the ones from the
# Bar table
mapping = objects.to_h { |obj| [obj, bazzes[obj.id] }
# Issues at most 2 queries
::Gitlab::Database::BulkUpdate.execute(%i[baz], mapping) do |obj|
obj.object_type.constantize
end
```
## Caveats
Note that this is a **very low level** tool, and operates on the raw column
values. Enumerations and state fields must be translated into their underlying
representations, for example, and nested associations are not supported. No
validations or hooks will be called.

View File

@ -226,7 +226,7 @@ sum(JiraImportState.finished, :imported_issues_count)
### Grouping & Batch Operations
The `count`, `distinct_count`, and `sum` batch counters can accept an `ActiveRecord::Relation`
object, which groups by a specified column. With a grouped relation, the methods do batch counting,
object, which groups by a specified column. With a grouped relation, the methods do batch counting,
handle errors, and returns a hash table of key-value pairs.
Examples:
@ -912,7 +912,7 @@ The following is example content of the Usage Ping payload.
## Exporting Usage Ping SQL queries and definitions
Two Rake tasks exist to export Usage Ping definitions.
Two Rake tasks exist to export Usage Ping definitions.
- The Rake tasks export the raw SQL queries for `count`, `distinct_count`, `sum`.
- The Rake tasks export the Redis counter class or the line of the Redis block for `redis_usage_data`.

View File

@ -1,6 +1,6 @@
# Environment selection
Some tests are designed to be run against specific environments or [pipelines](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#scheduled-qa-test-pipelines).
Some tests are designed to be run against specific environments or [pipelines](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#scheduled-qa-test-pipelines).
We can specify what environments or pipelines to run tests against using the `only` metadata.
## Available switches

View File

@ -232,7 +232,7 @@ This allows you to create unique boards according to your team's need.
![Create scoped board](img/issue_board_creation.png)
You can define the scope of your board when creating it or by clicking the "Edit board" button.
Once a milestone, assignee or weight is assigned to an issue board, you can no longer
Once a milestone, assignee or weight is assigned to an issue board, you can no longer
filter through these in the search bar. In order to do that, you need to remove the desired scope
(for example, milestone, assignee, or weight) from the issue board.

View File

@ -0,0 +1,168 @@
# frozen_string_literal: true
module Gitlab
module Database
# Constructs queries of the form:
#
# with cte(a, b, c) as (
# select * from (values (:x, :y, :z), (:q, :r, :s)) as t
# )
# update table set b = cte.b, c = cte.c where a = cte.a
#
# Which is useful if you want to update a set of records in a single query
# but cannot express the update as a calculation (i.e. you have arbitrary
# updates to perform).
#
# The requirements are that the table must have an ID column used to
# identify the rows to be updated.
#
# Usage:
#
# mapping = {
# issue_a => { title: 'This title', relative_position: 100 },
# issue_b => { title: 'That title', relative_position: 173 }
# }
#
# ::Gitlab::Database::BulkUpdate.execute(%i[title relative_position], mapping)
#
# Note that this is a very low level tool, and operates on the raw column
# values. Enums/state fields must be translated into their underlying
# representations, for example, and no hooks will be called.
#
module BulkUpdate
LIST_SEPARATOR = ', '
class Setter
include Gitlab::Utils::StrongMemoize
def initialize(model, columns, mapping)
@table_name = model.table_name
@connection = model.connection
@columns = self.class.column_definitions(model, columns)
@mapping = self.class.value_mapping(mapping)
end
def update!
if without_prepared_statement?
# A workaround for https://github.com/rails/rails/issues/24893
# When prepared statements are prevented (such as when using the
# query counter or in omnibus by default), we cannot call
# `exec_update`, since that will discard the bindings.
connection.send(:exec_no_cache, sql, log_name, params) # rubocop: disable GitlabSecurity/PublicSend
else
connection.exec_update(sql, log_name, params)
end
end
def self.column_definitions(model, columns)
raise ArgumentError, 'invalid columns' if columns.blank? || columns.any? { |c| !c.is_a?(Symbol) }
raise ArgumentError, 'cannot set ID' if columns.include?(:id)
([:id] | columns).map { |name| column_definition(model, name) }
end
def self.column_definition(model, name)
definition = model.column_for_attribute(name)
raise ArgumentError, "Unknown column: #{name}" unless definition.type
definition
end
def self.value_mapping(mapping)
raise ArgumentError, 'invalid mapping' if mapping.blank?
raise ArgumentError, 'invalid mapping value' if mapping.any? { |_k, v| !v.is_a?(Hash) }
mapping
end
private
attr_reader :table_name, :connection, :columns, :mapping
def log_name
strong_memoize(:log_name) do
"BulkUpdate #{table_name} #{columns.drop(1).map(&:name)}:#{mapping.size}"
end
end
def params
mapping.flat_map do |k, v|
obj_id = k.try(:id) || k
v = v.merge(id: obj_id)
columns.map { |c| query_attribute(c, k, v.with_indifferent_access) }
end
end
# A workaround for https://github.com/rails/rails/issues/24893
# We need to detect if prepared statements have been disabled.
def without_prepared_statement?
strong_memoize(:without_prepared_statement) do
connection.send(:without_prepared_statement?, [1]) # rubocop: disable GitlabSecurity/PublicSend
end
end
def query_attribute(column, key, values)
value = values[column.name]
key[column.name] = value if key.try(:id) # optimistic update
ActiveRecord::Relation::QueryAttribute.from_user(nil, value, ActiveModel::Type.lookup(column.type))
end
def values
counter = 0
typed = false
mapping.map do |k, v|
binds = columns.map do |c|
bind = "$#{counter += 1}"
# PG is not great at inferring types - help it for the first row.
bind += "::#{c.sql_type}" unless typed
bind
end
typed = true
"(#{list_of(binds)})"
end
end
def list_of(list)
list.join(LIST_SEPARATOR)
end
def sql
<<~SQL
WITH cte(#{list_of(cte_columns)}) AS (VALUES #{list_of(values)})
UPDATE #{table_name} SET #{list_of(updates)} FROM cte WHERE cte_id = id
SQL
end
def column_names
strong_memoize(:column_names) { columns.map(&:name) }
end
def cte_columns
strong_memoize(:cte_columns) do
column_names.map do |c|
connection.quote_column_name("cte_#{c}")
end
end
end
def updates
column_names.zip(cte_columns).drop(1).map do |dest, src|
"#{connection.quote_column_name(dest)} = cte.#{src}"
end
end
end
def self.execute(columns, mapping, &to_class)
raise ArgumentError if mapping.blank?
entries_by_class = mapping.group_by { |k, v| block_given? ? to_class.call(k) : k.class }
entries_by_class.each do |model, entries|
Setter.new(model, columns, entries).update!
end
end
end
end
end

View File

@ -61,7 +61,7 @@ module Gitlab
end
def current_transaction
Transaction.current
::Gitlab::Metrics::Transaction.current
end
end
end

View File

@ -0,0 +1,139 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::BulkUpdate do
describe 'error states' do
let(:columns) { %i[title] }
let_it_be(:mapping) do
create_default(:user)
create_default(:project)
i_a, i_b = create_list(:issue, 2)
{
i_a => { title: 'Issue a' },
i_b => { title: 'Issue b' }
}
end
it 'does not raise errors on valid inputs' do
expect { described_class.execute(columns, mapping) }.not_to raise_error
end
it 'expects a non-empty list of column names' do
expect { described_class.execute([], mapping) }.to raise_error(ArgumentError)
end
it 'expects all columns to be symbols' do
expect { described_class.execute([1], mapping) }.to raise_error(ArgumentError)
end
it 'expects all columns to be valid columns on the tables' do
expect { described_class.execute([:foo], mapping) }.to raise_error(ArgumentError)
end
it 'refuses to set ID' do
expect { described_class.execute([:id], mapping) }.to raise_error(ArgumentError)
end
it 'expects a non-empty mapping' do
expect { described_class.execute(columns, []) }.to raise_error(ArgumentError)
end
it 'expects all map values to be Hash instances' do
bad_map = mapping.merge(build(:issue) => 2)
expect { described_class.execute(columns, bad_map) }.to raise_error(ArgumentError)
end
end
it 'is possible to update all objects in a single query' do
users = create_list(:user, 3)
mapping = users.zip(%w(foo bar baz)).to_h do |u, name|
[u, { username: name, admin: true }]
end
expect do
described_class.execute(%i[username admin], mapping)
end.not_to exceed_query_limit(1)
# We have optimistically updated the values
expect(users).to all(be_admin)
expect(users.map(&:username)).to eq(%w(foo bar baz))
users.each(&:reset)
# The values are correct on reset
expect(users).to all(be_admin)
expect(users.map(&:username)).to eq(%w(foo bar baz))
end
it 'is possible to update heterogeneous sets' do
create_default(:user)
create_default(:project)
mr_a = create(:merge_request)
i_a, i_b = create_list(:issue, 2)
mapping = {
mr_a => { title: 'MR a' },
i_a => { title: 'Issue a' },
i_b => { title: 'Issue b' }
}
expect do
described_class.execute(%i[title], mapping)
end.not_to exceed_query_limit(2)
expect([mr_a, i_a, i_b].map { |x| x.reset.title })
.to eq(['MR a', 'Issue a', 'Issue b'])
end
shared_examples 'basic functionality' do
it 'sets multiple values' do
create_default(:user)
create_default(:project)
i_a, i_b = create_list(:issue, 2)
mapping = {
i_a => { title: 'Issue a' },
i_b => { title: 'Issue b' }
}
described_class.execute(%i[title], mapping)
expect([i_a, i_b].map { |x| x.reset.title })
.to eq(['Issue a', 'Issue b'])
end
end
include_examples 'basic functionality'
context 'when prepared statements are configured differently to the normal test environment' do
# rubocop: disable RSpec/LeakyConstantDeclaration
# This cop is disabled because you cannot call establish_connection on
# an anonymous class.
class ActiveRecordBasePreparedStatementsInverted < ActiveRecord::Base
def self.abstract_class?
true # So it gets its own connection
end
end
# rubocop: enable RSpec/LeakyConstantDeclaration
before_all do
c = ActiveRecord::Base.connection.instance_variable_get(:@config)
inverted = c.merge(prepared_statements: !ActiveRecord::Base.connection.prepared_statements)
ActiveRecordBasePreparedStatementsInverted.establish_connection(inverted)
end
before do
allow(ActiveRecord::Base).to receive(:connection_specification_name)
.and_return(ActiveRecordBasePreparedStatementsInverted.connection_specification_name)
end
include_examples 'basic functionality'
end
end

View File

@ -37,18 +37,11 @@ RSpec.describe RelativePositioning::Mover do
end
def set_positions(positions)
vals = issues.zip(positions).map do |issue, pos|
issue.relative_position = pos
"(#{issue.id}, #{pos})"
end.join(', ')
mapping = issues.zip(positions).to_h do |issue, pos|
[issue, { relative_position: pos }]
end
Issue.connection.exec_query(<<~SQL, 'set-positions')
WITH cte(cte_id, new_pos) AS (
SELECT * FROM (VALUES #{vals}) as t (id, pos)
)
UPDATE issues SET relative_position = new_pos FROM cte WHERE id = cte_id
;
SQL
::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping)
end
def ids_in_position_order

View File

@ -152,9 +152,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min)
end
it 'manages to move nulls found in the relative scope' do
nils = create_items_with_positions([nil] * 4)
described_class.move_nulls_to_end(sibling_query.to_a)
positions = nils.map { |item| item.reset.relative_position }
expect(positions).to all(be_present)
expect(positions).to all(be_valid_position)
end
it 'can move many nulls' do
nils = create_items_with_positions([nil] * 101)
described_class.move_nulls_to_end(nils)
expect(nils.map(&:relative_position)).to all(be_valid_position)
end
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
a, b, c, d, e, f, *xs = create_items_with_positions([nil] * 10)
baseline = ActiveRecord::QueryRecorder.new do