Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-01 18:10:04 +00:00
parent 41aebff8ec
commit c3afdb42dd
57 changed files with 1159 additions and 413 deletions

View File

@ -22,7 +22,7 @@ import EditorTab from './ui/editor_tab.vue';
export default {
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabEdit: s__('Pipelines|Edit'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
tabMergedYaml: s__('Pipelines|View merged YAML'),

View File

@ -151,7 +151,7 @@ export default {
},
},
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabEdit: s__('Pipelines|Edit'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},

View File

@ -1,12 +1,12 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { GlModal, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { __, s__ } from '~/locale';
export default {
components: {
GlModal,
GlSprintf,
},
props: {
actionUrl: {
@ -32,33 +32,8 @@ export default {
csrfToken() {
return csrf.token;
},
inputLabel() {
let confirmationValue;
if (this.confirmWithPassword) {
confirmationValue = __('password');
} else {
confirmationValue = __('username');
}
confirmationValue = `<code>${confirmationValue}</code>`;
return sprintf(
s__('Profiles|Type your %{confirmationValue} to confirm:'),
{ confirmationValue },
false,
);
},
text() {
return sprintf(
s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`,
},
false,
);
confirmationValue() {
return this.confirmWithPassword ? __('password') : __('username');
},
primaryProps() {
return {
@ -90,6 +65,12 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
this.$refs.form.submit();
},
},
i18n: {
text: s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
inputLabel: s__('Profiles|Type your %{confirmationValue} to confirm:'),
},
};
</script>
@ -102,13 +83,29 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
:ok-disabled="!canSubmit"
@primary="onSubmit"
>
<p v-html="text"></p>
<p>
<gl-sprintf :message="$options.i18n.text">
<template #yourAccount>
<strong>{{ s__('Profiles|your account') }}</strong>
</template>
<template #deleteAccount>
<strong>{{ s__('Profiles|Delete account') }}</strong>
</template>
</gl-sprintf>
</p>
<form ref="form" :action="actionUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<p id="input-label" v-html="inputLabel"></p>
<p id="input-label">
<gl-sprintf :message="$options.i18n.inputLabel">
<template #confirmationValue>
<code>{{ confirmationValue }}</code>
</template>
</gl-sprintf>
</p>
<input
v-if="confirmWithPassword"

View File

@ -19,8 +19,6 @@ class Group < Namespace
include HasTimelogsReport
include BulkMemberAccessLoad
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@ -634,7 +632,7 @@ class Group < Namespace
end
def access_request_approvers_to_be_notified
members.owners.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def supports_events?

View File

@ -14,6 +14,7 @@ class Member < ApplicationRecord
include UpdateHighestRole
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
attr_accessor :raw_invite_token

View File

@ -63,8 +63,6 @@ class Project < ApplicationRecord
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
SORTING_PREFERENCE_FIELD = :projects_sort
MAX_BUILD_TIMEOUT = 1.month
@ -2439,7 +2437,7 @@ class Project < ApplicationRecord
end
def access_request_approvers_to_be_notified
members.maintainers.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
members.maintainers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def pages_lookup_path(trim_prefix: nil, domain: nil)

View File

@ -13,17 +13,20 @@ class UserProjectAccessChangedService
def execute(blocking: true, priority: HIGH_PRIORITY)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
if priority == HIGH_PRIORITY
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
result =
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in( # rubocop:disable Scalability/BulkPerformWithContext
DELAY, bulk_args, batch_size: 100, batch_delay: 30.seconds)
if priority == HIGH_PRIORITY
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
else
AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in( # rubocop:disable Scalability/BulkPerformWithContext
DELAY, bulk_args, batch_size: 100, batch_delay: 30.seconds)
end
end
end
::Gitlab::Database::LoadBalancing::Sticking.bulk_stick(:user, @user_ids)
result
end
end
UserProjectAccessChangedService.prepend_mod_with('UserProjectAccessChangedService')

View File

@ -64,6 +64,7 @@ cloud providers:
- Amazon RDS - [Creating a Read Replica](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Create)
- Azure Database for PostgreSQL - [Create and manage read replicas in Azure Database for PostgreSQL](https://docs.microsoft.com/en-us/azure/postgresql/howto-read-replicas-portal)
- Google Cloud SQL - [Creating read replicas](https://cloud.google.com/sql/docs/postgres/replication/create-replica)
Once your read-only replica is set up, you can skip to [configure you secondary application node](#configure-secondary-application-nodes-to-use-the-external-read-replica).
@ -182,6 +183,10 @@ to grant additional roles to your tracking database user (by default, this is
- Amazon RDS requires the [`rds_superuser`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.html#Appendix.PostgreSQL.CommonDBATasks.Roles) role.
- Azure Database for PostgreSQL requires the [`azure_pg_admin`](https://docs.microsoft.com/en-us/azure/postgresql/howto-create-users#how-to-create-additional-admin-users-in-azure-database-for-postgresql) role.
- Google Cloud SQL requires the [`cloudsqlsuperuser`](https://cloud.google.com/sql/docs/postgres/users#default-users) role.
This is for the installation of extensions during installation and upgrades. As an alternative,
[ensure the extensions are installed manually, and read about the problems that may arise during future GitLab upgrades](../../../install/postgresql_extensions.md).
If you have an external database ready to be used as the tracking database,
follow the instructions below to use it:

View File

@ -82,7 +82,7 @@ This writes the downloaded file to `MyNuGetPkg.1.3.0.17.nupkg` in the current di
> Introduced in GitLab 12.8.
Download a NuGet package file:
Upload a NuGet package file:
```plaintext
PUT projects/:id/packages/nuget

View File

@ -22,7 +22,7 @@ The access levels are defined in the `ProtectedRefAccess.allowed_access_levels`
## List protected branches
Gets a list of protected branches from a project.
Gets a list of protected branches from a project as they are defined [in the UI](../user/project/protected_branches.md#configure-a-protected-branch). If a wildcard is set, it is returned instead of the exact name of the branches that match that wildcard.
```plaintext
GET /projects/:id/protected_branches
@ -59,6 +59,24 @@ Example response:
"allow_force_push":false,
"code_owner_approval_required": false
},
{
"id": 1,
"name": "release/*",
"push_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
},
...
]
```

View File

@ -45,25 +45,32 @@ We have many definitions of Snowplow's schema. We have an active issue to [stand
- [Iglu schema](https://gitlab.com/gitlab-org/iglu/)
- [Snowplow authored events](https://github.com/snowplow/snowplow/wiki/Snowplow-authored-events)
## Enabling Snowplow
## Enable Snowplow tracking
Tracking can be enabled at:
- The instance level, which enables tracking on both the frontend and backend layers.
- User level, though user tracking can be disabled on a per-user basis. GitLab tracking respects the [Do Not Track](https://www.eff.org/issues/do-not-track) standard, so any user who has enabled the Do Not Track option in their browser is not tracked at a user level.
- The user level, though user tracking can be disabled on a per-user basis.
GitLab respects the [Do Not Track](https://www.eff.org/issues/do-not-track) standard, so any user who has enabled the Do Not Track option in their browser is not tracked at a user level.
We use Snowplow for the majority of our tracking strategy and it is enabled on GitLab.com. On a self-managed instance, Snowplow can be enabled by navigating to:
Snowplow tracking is enabled on GitLab.com, and we use it for most of our tracking strategy.
- **Admin Area > Settings > General** in the UI.
- `admin/application_settings/integrations` in your browser.
To enable Snowplow tracking on a self-managed instance:
Example configuration:
1. Go to the Admin Area (**{admin}**) and select **Settings > General**.
Alternatively, go to `admin/application_settings/general` in your browser.
| Name | Value |
|---------------|-------------------------------|
| Collector | `your-snowplow-collector.net` |
| Site ID | `gitlab` |
| Cookie domain | `.your-gitlab-instance.com` |
1. Expand **Snowplow**.
1. Select **Enable snowplow tracking** and enter your Snowplow configuration information. For example:
| Name | Value |
|--------------------|-------------------------------|
| Collector hostname | `your-snowplow-collector.net` |
| App ID | `gitlab` |
| Cookie domain | `.your-gitlab-instance.com` |
1. Select **Save changes**.
## Snowplow request flow
@ -459,7 +466,7 @@ There are several tools for developing and testing Snowplow Event
To test frontend events in development:
- [Enable Snowplow in the admin area](#enabling-snowplow).
- [Enable Snowplow tracking in the Admin Area](#enable-snowplow-tracking).
- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment.
- Turn off "Do Not Track" (DNT) in your browser.

View File

@ -177,12 +177,12 @@ You can [configure](#customizing-the-container-scanning-settings) both analyzers
| `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. | All |
| `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. | All |
| `CS_ANALYZER_IMAGE` | `$SECURE_ANALYZERS_PREFIX/$CS_PROJECT:$CS_MAJOR_VERSION` | Docker image of the analyzer. | All |
| `CS_DOCKER_INSECURE` | `"false"` | Allow access to secure Docker registries using HTTPS without validating the certificates. | All |
| `CS_REGISTRY_INSECURE` | `"false"` | Allow access to insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | All |
| `DOCKER_IMAGE` | `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG` | The Docker image to be scanned. If set, this variable overrides the `$CI_APPLICATION_REPOSITORY` and `$CI_APPLICATION_TAG` variables. | All |
| `DOCKER_INSECURE` | `"false"` | Allow access to secure Docker registries using HTTPS without validating the certificates. | All |
| `DOCKER_PASSWORD` | `$CI_REGISTRY_PASSWORD` | Password for accessing a Docker registry requiring authentication. | All |
| `DOCKER_USER` | `$CI_REGISTRY_USER` | Username for accessing a Docker registry requiring authentication. | All |
| `DOCKERFILE_PATH` | `Dockerfile` | The path to the `Dockerfile` to use for generating remediations. By default, the scanner looks for a file named `Dockerfile` in the root directory of the project. You should configure this variable only if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | All |
| `REGISTRY_INSECURE` | `"false"` | Allow access to insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | All |
| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. | All |
### Overriding the container scanning template

View File

@ -233,6 +233,14 @@ username = gitlab-ci-token
password = ${env.CI_JOB_TOKEN}
```
### Authenticate to access packages within a group
Follow the instructions above for the token type, but use the group URL in place of the project URL:
```shell
https://gitlab.example.com/api/v4/groups/<group_id>/-/packages/pypi
```
## Publish a PyPI package
Prerequisites:
@ -357,7 +365,7 @@ Successfully installed mypypipackage-0.0.1
To install the latest version of a package from a group, use the following command:
```shell
pip install --index-url https://<personal_access_token_name>:<personal_access_token>@gitlab.example.com/api/v4/groups/<group_id>/packages/pypi/simple --no-deps <package_name>
pip install --index-url https://<personal_access_token_name>:<personal_access_token>@gitlab.example.com/api/v4/groups/<group_id>/-/packages/pypi/simple --no-deps <package_name>
```
In this command:
@ -376,7 +384,7 @@ provided URL as an additional registry which the client checks if the package is
If you're following the guide and want to install the `MyPyPiPackage` package, you can run:
```shell
pip install mypypipackage --no-deps --index-url https://<personal_access_token_name>:<personal_access_token>@gitlab.example.com/api/v4/groups/<your_group_id>/packages/pypi/simple
pip install mypypipackage --no-deps --index-url https://<personal_access_token_name>:<personal_access_token>@gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/pypi/simple
```
### Package names

View File

@ -14,6 +14,14 @@ module Gitlab
SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze
TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze
DB_LOAD_BALANCING_COUNTERS = %i{
db_replica_count db_replica_cached_count db_replica_wal_count
db_primary_count db_primary_cached_count db_primary_wal_count
}.freeze
DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze
SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze
# This event is published from ActiveRecordBaseTransactionMetrics and
# used to record a database transaction duration when calling
# ActiveRecord::Base.transaction {} block.
@ -39,20 +47,57 @@ module Gitlab
observe(:gitlab_sql_duration_seconds, event) do
buckets SQL_DURATION_BUCKET
end
if ::Gitlab::Database::LoadBalancing.enable?
db_role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(payload[:connection])
return if db_role.blank?
increment_db_role_counters(db_role, payload)
observe_db_role_duration(db_role, event)
end
end
def self.db_counter_payload
return {} unless Gitlab::SafeRequestStore.active?
payload = {}
DB_COUNTERS.each do |counter|
payload[counter] = Gitlab::SafeRequestStore[counter].to_i
{}.tap do |payload|
DB_COUNTERS.each do |counter|
payload[counter] = Gitlab::SafeRequestStore[counter].to_i
end
if ::Gitlab::SafeRequestStore.active? && ::Gitlab::Database::LoadBalancing.enable?
DB_LOAD_BALANCING_COUNTERS.each do |counter|
payload[counter] = ::Gitlab::SafeRequestStore[counter].to_i
end
DB_LOAD_BALANCING_DURATIONS.each do |duration|
payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3)
end
end
end
payload
end
private
def wal_command?(payload)
payload[:sql].match(SQL_WAL_LOCATION_REGEX)
end
def increment_db_role_counters(db_role, payload)
increment("db_#{db_role}_count".to_sym)
increment("db_#{db_role}_cached_count".to_sym) if cached_query?(payload)
increment("db_#{db_role}_wal_count".to_sym) if !cached_query?(payload) && wal_command?(payload)
end
def observe_db_role_duration(db_role, event)
observe("gitlab_sql_#{db_role}_duration_seconds".to_sym, event) do
buckets ::Gitlab::Metrics::Subscribers::ActiveRecord::SQL_DURATION_BUCKET
end
duration = event.duration / 1000.0
duration_key = "db_#{db_role}_duration_s".to_sym
::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
end
def ignored_query?(payload)
payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
end
@ -82,5 +127,3 @@ module Gitlab
end
end
end
Gitlab::Metrics::Subscribers::ActiveRecord.prepend_mod_with('Gitlab::Metrics::Subscribers::ActiveRecord')

View File

@ -1,35 +1,13 @@
# frozen_string_literal: true
# please require all dependencies below:
require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab
module Redis
class Cache < ::Gitlab::Redis::Wrapper
CACHE_NAMESPACE = 'cache:gitlab'
DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'
REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'
class << self
def default_url
DEFAULT_REDIS_CACHE_URL
end
def config_file_name
# if ENV set for this class, use it even if it points to a file does not exist
file_name = ENV[REDIS_CACHE_CONFIG_ENV_VAR_NAME]
return file_name unless file_name.nil?
# otherwise, if config files exists for this class, use it
file_name = config_file_path('redis.cache.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent
super
end
def instrumentation_class
::Gitlab::Instrumentation::Redis::Cache
'redis://localhost:6380'
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
# please require all dependencies below:
# We need this require for MailRoom
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
module Gitlab
@ -8,29 +8,10 @@ module Gitlab
class Queues < ::Gitlab::Redis::Wrapper
SIDEKIQ_NAMESPACE = 'resque:gitlab'
MAILROOM_NAMESPACE = 'mail_room:gitlab'
DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'
REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'
class << self
def default_url
DEFAULT_REDIS_QUEUES_URL
end
def config_file_name
# if ENV set for this class, use it even if it points to a file does not exist
file_name = ENV[REDIS_QUEUES_CONFIG_ENV_VAR_NAME]
return file_name if file_name
# otherwise, if config files exists for this class, use it
file_name = config_file_path('redis.queues.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent
super
end
def instrumentation_class
::Gitlab::Instrumentation::Redis::Queues
'redis://localhost:6381'
end
end
end

View File

@ -1,8 +1,5 @@
# frozen_string_literal: true
# please require all dependencies below:
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
@ -10,29 +7,10 @@ module Gitlab
USER_SESSIONS_NAMESPACE = 'session:user:gitlab'
USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'
class << self
def default_url
DEFAULT_REDIS_SHARED_STATE_URL
end
def config_file_name
# if ENV set for this class, use it even if it points to a file does not exist
file_name = ENV[REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME]
return file_name if file_name
# otherwise, if config files exists for this class, use it
file_name = config_file_path('redis.shared_state.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_SHARED_STATE_URL when config file is absent
super
end
def instrumentation_class
::Gitlab::Instrumentation::Redis::SharedState
'redis://localhost:6382'
end
end
end

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true
# This file should only be used by sub-classes, not directly by any clients of the sub-classes
# please require all dependencies below:
# Explicitly load parts of ActiveSupport because MailRoom does not load
# Rails.
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/string/inflections'
module Gitlab
module Redis
class Wrapper
DEFAULT_REDIS_URL = 'redis://localhost:6379'
REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'
class << self
delegate :params, :url, to: :new
@ -52,32 +52,35 @@ module Gitlab
end
def default_url
DEFAULT_REDIS_URL
raise NotImplementedError
end
# Return the absolute path to a Rails configuration file
#
# We use this instead of `Rails.root` because for certain tasks
# utilizing these classes, `Rails` might not be available.
def config_file_path(filename)
File.expand_path("../../../config/#{filename}", __dir__)
path = File.join(rails_root, 'config', filename)
return path if File.file?(path)
end
# We need this local implementation of Rails.root because MailRoom
# doesn't load Rails.
def rails_root
File.expand_path('../../..', __dir__)
end
def config_file_name
# if ENV set for wrapper class, use it even if it points to a file does not exist
file_name = ENV[REDIS_CONFIG_ENV_VAR_NAME]
return file_name unless file_name.nil?
[
ENV["GITLAB_REDIS_#{store_name.underscore.upcase}_CONFIG_FILE"],
config_file_path("redis.#{store_name.underscore}.yml"),
ENV['GITLAB_REDIS_CONFIG_FILE'],
config_file_path('resque.yml')
].compact.first
end
# otherwise, if config files exists for wrapper class, use it
file_name = config_file_path('resque.yml')
return file_name if File.file?(file_name)
# nil will force use of DEFAULT_REDIS_URL when config file is absent
nil
def store_name
name.demodulize
end
def instrumentation_class
raise NotImplementedError
"::Gitlab::Instrumentation::Redis::#{store_name}".constantize
end
end

View File

@ -24395,9 +24395,6 @@ msgstr ""
msgid "Pipelines|Visualize"
msgstr ""
msgid "Pipelines|Write pipeline configuration"
msgstr ""
msgid "Pipelines|invalid"
msgstr ""

View File

@ -33,6 +33,8 @@
"lint:stylelint:fix": "yarn run lint:stylelint --fix",
"lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
"generate:startup_css": "scripts/frontend/startup_css/setup.sh && node scripts/frontend/startup_css/main.js",
"generate:startup_css:full": "scripts/frontend/startup_css/setup.sh force && node scripts/frontend/startup_css/main.js",
"markdownlint": "markdownlint --config .markdownlint.yml",
"markdownlint:no-trailing-spaces": "markdownlint --config doc/.markdownlint/markdownlint-no-trailing-spaces.yml",
"markdownlint:no-trailing-spaces:fix": "yarn run markdownlint:no-trailing-spaces --fix",
@ -200,6 +202,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-istanbul": "^6.0.0",
"chalk": "^2.4.1",
"cheerio": "^1.0.0-rc.9",
"commander": "^2.18.0",
"custom-jquery-matchers": "^2.1.0",
"docdash": "^1.0.2",
@ -241,6 +244,7 @@
"nodemon": "^2.0.4",
"postcss": "^7.0.14",
"prettier": "2.2.1",
"purgecss": "^4.0.3",
"readdir-enhanced": "^2.2.4",
"sass": "^1.32.12",
"timezone-mock": "^1.0.8",

View File

@ -0,0 +1,83 @@
const { memoize, isString, isRegExp } = require('lodash');
const { parse } = require('postcss');
const { CSS_TO_REMOVE } = require('./constants');
const getSelectorRemoveTesters = memoize(() =>
CSS_TO_REMOVE.map((x) => {
if (isString(x)) {
return (selector) => x === selector;
}
if (isRegExp(x)) {
return (selector) => x.test(selector);
}
throw new Error(`Unexpected type in CSS_TO_REMOVE content "${x}". Expected String or RegExp.`);
}),
);
const getRemoveTesters = memoize(() => {
const selectorTesters = getSelectorRemoveTesters();
// These are mostly carried over from the previous project
// https://gitlab.com/gitlab-org/frontend/gitlab-css-statistics/-/blob/2aa00af25dba08fc71081c77206f45efe817ea4b/lib/gl_startup_extract.js
return [
(node) => node.type === 'comment',
(node) =>
node.type === 'atrule' &&
(node.params === 'print' ||
node.params === 'prefers-reduced-motion: reduce' ||
node.name === 'keyframe' ||
node.name === 'charset'),
(node) => node.selector && node.selectors && !node.selectors.length,
(node) => node.selector && selectorTesters.some((fn) => fn(node.selector)),
(node) =>
node.type === 'decl' &&
(node.prop === 'transition' ||
node.prop.indexOf('-webkit-') > -1 ||
node.prop.indexOf('-ms-') > -1),
];
});
const getNodesToRemove = (nodes) => {
const removeTesters = getRemoveTesters();
const remNodes = [];
nodes.forEach((node) => {
if (removeTesters.some((fn) => fn(node))) {
remNodes.push(node);
} else if (node.nodes?.length) {
remNodes.push(...getNodesToRemove(node.nodes));
}
});
return remNodes;
};
const getEmptyNodesToRemove = (nodes) =>
nodes
.filter((node) => node.nodes)
.reduce((acc, node) => {
if (node.nodes.length) {
acc.push(...getEmptyNodesToRemove(node.nodes));
} else {
acc.push(node);
}
return acc;
}, []);
const cleanCSS = (css) => {
const cssRoot = parse(css);
getNodesToRemove(cssRoot.nodes).forEach((node) => {
node.remove();
});
getEmptyNodesToRemove(cssRoot.nodes).forEach((node) => {
node.remove();
});
return cssRoot.toResult().css;
};
module.exports = { cleanCSS };

View File

@ -0,0 +1,99 @@
const path = require('path');
const IS_EE = require('../../../config/helpers/is_ee_env');
// controls --------------------------------------------------------------------
const HTML_TO_REMOVE = [
'style',
'script',
'link[rel="stylesheet"]',
'.content-wrapper',
'#js-peek',
'.modal',
'.feature-highlight',
// We don't want to capture all the children of a dropdown-menu
'.dropdown-menu',
];
const CSS_TO_REMOVE = [
'.tooltip',
'.tooltip.show',
'.fa',
'.gl-accessibility:focus',
'.toasted-container',
'body .toasted-container.bottom-left',
'.popover',
'.with-performance-bar .navbar-gitlab',
'.text-secondary',
/\.feature-highlight-popover-content/,
/\.commit/,
/\.md/,
/\.with-performance-bar/,
/\.identicon/,
];
const APPLICATION_CSS_PREFIX = 'application';
const APPLICATION_DARK_CSS_PREFIX = 'application_dark';
const UTILITIES_CSS_PREFIX = 'application_utilities';
const UTILITIES_DARK_CSS_PREFIX = 'application_utilities_dark';
// paths -----------------------------------------------------------------------
const ROOT = path.resolve(__dirname, '../../..');
const FIXTURES_FOLDER_NAME = IS_EE ? 'fixtures-ee' : 'fixtures';
const FIXTURES_ROOT = path.join(ROOT, 'tmp/tests/frontend', FIXTURES_FOLDER_NAME);
const PATH_SIGNIN_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in.html');
const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets');
const PATH_STARTUP_SCSS = path.join(ROOT, 'app/assets/stylesheets/startup');
const OUTPUTS = [
{
outFile: 'startup-general',
htmlPaths: [
path.join(FIXTURES_ROOT, 'startup_css/project-general.html'),
path.join(FIXTURES_ROOT, 'startup_css/project-general-legacy-menu.html'),
path.join(FIXTURES_ROOT, 'startup_css/project-general-signed-out.html'),
],
cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
// We want to include the root dropdown-menu style since it should be hidden by default
purgeOptions: {
safelist: {
standard: ['dropdown-menu'],
},
},
},
{
outFile: 'startup-dark',
htmlPaths: [
path.join(FIXTURES_ROOT, 'startup_css/project-dark.html'),
path.join(FIXTURES_ROOT, 'startup_css/project-dark-legacy-menu.html'),
path.join(FIXTURES_ROOT, 'startup_css/project-dark-signed-out.html'),
],
cssKeys: [APPLICATION_DARK_CSS_PREFIX, UTILITIES_DARK_CSS_PREFIX],
// We want to include the root dropdown-menu styles since it should be hidden by default
purgeOptions: {
safelist: {
standard: ['dropdown-menu'],
},
},
},
{
outFile: 'startup-signin',
htmlPaths: [PATH_SIGNIN_HTML],
cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
purgeOptions: {
safelist: {
standard: ['fieldset'],
deep: [/login-page$/],
},
},
},
];
module.exports = {
HTML_TO_REMOVE,
CSS_TO_REMOVE,
APPLICATION_CSS_PREFIX,
APPLICATION_DARK_CSS_PREFIX,
UTILITIES_CSS_PREFIX,
UTILITIES_DARK_CSS_PREFIX,
ROOT,
PATH_ASSETS,
PATH_STARTUP_SCSS,
OUTPUTS,
};

View File

@ -0,0 +1,22 @@
const fs = require('fs');
const path = require('path');
const { memoize } = require('lodash');
const { PATH_ASSETS } = require('./constants');
const { die } = require('./utils');
const listAssetsDir = memoize(() => fs.readdirSync(PATH_ASSETS));
const getCSSPath = (prefix) => {
const matcher = new RegExp(`^${prefix}-[^-]+\\.css$`);
const cssPath = listAssetsDir().find((x) => matcher.test(x));
if (!cssPath) {
die(
`Could not find the CSS asset matching "${prefix}". Have you run "scripts/frontend/startup_css/setup.sh"?`,
);
}
return path.join(PATH_ASSETS, cssPath);
};
module.exports = { getCSSPath };

View File

@ -0,0 +1,62 @@
const fs = require('fs');
const cheerio = require('cheerio');
const { mergeWith, isArray } = require('lodash');
const { PurgeCSS } = require('purgecss');
const { cleanCSS } = require('./clean_css');
const { HTML_TO_REMOVE } = require('./constants');
const { die } = require('./utils');
const cleanHtml = (html) => {
const $ = cheerio.load(html);
HTML_TO_REMOVE.forEach((selector) => {
$(selector).remove();
});
return $.html();
};
const mergePurgeCSSOptions = (...options) =>
mergeWith(...options, (objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue);
}
return undefined;
});
const getStartupCSS = async ({ htmlPaths, cssPaths, purgeOptions }) => {
const content = htmlPaths.map((htmlPath) => {
if (!fs.existsSync(htmlPath)) {
die(`Could not find fixture "${htmlPath}". Have you run the fixtures?`);
}
const rawHtml = fs.readFileSync(htmlPath);
const html = cleanHtml(rawHtml);
return { raw: html, extension: 'html' };
});
const purgeCSSResult = await new PurgeCSS().purge({
content,
css: cssPaths,
...mergePurgeCSSOptions(
{
fontFace: true,
variables: true,
keyframes: true,
blocklist: [/:hover/, /:focus/, /-webkit-/, /-moz-focusring-/, /-ms-expand/],
safelist: {
standard: ['brand-header-logo'],
},
// By default, PurgeCSS ignores special characters, but our utilities use "!"
defaultExtractor: (x) => x.match(/[\w-!]+/g),
},
purgeOptions,
),
});
return purgeCSSResult.map(({ css }) => cleanCSS(css)).join('\n');
};
module.exports = { getStartupCSS };

View File

@ -0,0 +1,60 @@
const { memoize } = require('lodash');
const { OUTPUTS } = require('./constants');
const { getCSSPath } = require('./get_css_path');
const { getStartupCSS } = require('./get_startup_css');
const { log, die } = require('./utils');
const { writeStartupSCSS } = require('./write_startup_scss');
const memoizedCSSPath = memoize(getCSSPath);
const runTask = async ({ outFile, htmlPaths, cssKeys, purgeOptions = {} }) => {
try {
log(`Generating startup CSS for HTML files: ${htmlPaths}`);
const generalCSS = await getStartupCSS({
htmlPaths,
cssPaths: cssKeys.map(memoizedCSSPath),
purgeOptions,
});
log(`Writing to startup CSS...`);
const startupCSSPath = writeStartupSCSS(outFile, generalCSS);
log(`Finished writing to ${startupCSSPath}`);
return {
success: true,
outFile,
};
} catch (e) {
log(`ERROR! Unexpected error occurred while generating startup CSS for: ${outFile}`);
log(e);
return {
success: false,
outFile,
};
}
};
const main = async () => {
const result = await Promise.all(OUTPUTS.map(runTask));
const fullSuccess = result.every((x) => x.success);
log('RESULTS:');
log('--------');
result.forEach(({ success, outFile }) => {
const status = success ? '✓' : '';
log(`${status}: ${outFile}`);
});
log('--------');
if (fullSuccess) {
log('Done!');
} else {
die('Some tasks have failed');
}
};
main();

View File

@ -0,0 +1,76 @@
path_public_dir="public"
path_tmp="tmp"
path_dest="$path_tmp/startup_css_assets"
glob_css_dest="$path_dest/application*.css"
glob_css_src="$path_public_dir/assets/application*.css"
should_clean=false
should_force() {
$1=="force"
}
has_dest_already() {
find $glob_css_dest -quit
}
has_src_already() {
find $glob_css_src -quit
}
compile_assets() {
# We need to build the same test bundle that is built in CI
RAILS_ENV=test bundle exec rake rake:assets:precompile
}
clean_assets() {
bundle exec rake rake:assets:clobber
}
copy_assets() {
rm -rf $path_dest
mkdir $path_dest
cp $glob_css_src $path_dest
}
echo "-----------------------------------------------------------"
echo "If you are run into any issues with Startup CSS generation,"
echo "please check out the feedback issue:"
echo ""
echo "https://gitlab.com/gitlab-org/gitlab/-/issues/331812"
echo "-----------------------------------------------------------"
if [ ! -e $path_public_dir ]; then
echo "Could not find '$path_public_dir/'. This script must be run in the root directory of the gitlab project."
exit 1
fi
if [ ! -e $path_tmp ]; then
echo "Could not find '$path_tmp/'. This script must be run in the root directory of the gitlab project."
exit 1
fi
if [ "$1" != "force" ] && has_dest_already; then
echo "Already found assets for '$glob_css_dest'. Did you want to run this script with 'force' argument?"
exit 0
fi
# If we are in CI, don't recompile things...
if [ -n "$CI" ]; then
if ! has_src_already; then
echo "Could not find '$glob_css_src'. Expected these artifacts to be generated by CI pipeline."
exit 1
fi
elif has_src_already; then
echo "Found '$glob_css_src'. Skipping compile assets..."
else
echo "Starting compile assets process..."
compile_assets
should_clean=true
fi
copy_assets
if $should_clean; then
echo "Starting cleanup..."
clean_assets
fi

View File

@ -0,0 +1,8 @@
const die = (message) => {
console.log(message);
process.exit(1);
};
const log = (message) => console.error(`[gitlab.startup_css] ${message}`);
module.exports = { die, log };

View File

@ -0,0 +1,28 @@
const { writeFileSync } = require('fs');
const path = require('path');
const prettier = require('prettier');
const { PATH_STARTUP_SCSS } = require('./constants');
const buildFinalContent = (raw) => {
const content = `// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
// Please see the feedback issue for more details and help:
// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
@charset "UTF-8";
${raw}
@import 'cloaking';
@include cloak-startup-scss(none);
`;
// We run prettier so that there is more determinism with the generated file.
return prettier.format(content, { parser: 'scss' });
};
const writeStartupSCSS = (name, raw) => {
const fullPath = path.join(PATH_STARTUP_SCSS, `${name}.scss`);
writeFileSync(fullPath, buildFinalContent(raw));
return fullPath;
};
module.exports = { writeStartupSCSS };

View File

@ -43,7 +43,7 @@ RSpec.describe 'mail_room.yml' do
context 'when both incoming email and service desk email are enabled' do
let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do
@ -72,7 +72,7 @@ RSpec.describe 'mail_room.yml' do
context 'when both incoming email and service desk email are enabled for Microsoft Graph' do
let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled_ms_graph.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do

View File

@ -1,2 +0,0 @@
test:
url: <%= ENV['TEST_GITLAB_REDIS_CACHE_URL'] %>

View File

@ -1,29 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development:
url: redis://:mynewpassword@localhost:6380/10
sentinels:
-
host: localhost
port: 26380 # point to sentinel, not to redis port
-
host: replica2
port: 26380 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6380/10
sentinels:
-
host: localhost
port: 26380 # point to sentinel, not to redis port
-
host: replica2
port: 26380 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6380/10
sentinels:
-
host: replica1
port: 26380 # point to sentinel, not to redis port
-
host: replica2
port: 26380 # point to sentinel, not to redis port

View File

@ -1,6 +0,0 @@
development:
url: unix:/path/to/redis.cache.sock
test:
url: unix:/path/to/redis.cache.sock
production:
url: unix:/path/to/redis.cache.sock

View File

@ -1,5 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development: redis://:mypassword@localhost:6380/10
test: redis://:mypassword@localhost:6380/10
production: redis://:mypassword@localhost:6380/10

View File

@ -1,3 +0,0 @@
development: unix:/path/to/old/redis.cache.sock
test: unix:/path/to/old/redis.cache.sock
production: unix:/path/to/old/redis.cache.sock

View File

@ -1,29 +1,29 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development:
url: redis://:mynewpassword@localhost:6379/99
url: redis://:mynewpassword@development-host:6379/99
sentinels:
-
host: localhost
host: development-replica1
port: 26379 # point to sentinel, not to redis port
-
host: replica2
host: development-replica2
port: 26379 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6379/99
url: redis://:mynewpassword@test-host:6379/99
sentinels:
-
host: localhost
host: test-replica1
port: 26379 # point to sentinel, not to redis port
-
host: replica2
host: test-replica2
port: 26379 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6379/99
url: redis://:mynewpassword@production-host:6379/99
sentinels:
-
host: replica1
host: production-replica1
port: 26379 # point to sentinel, not to redis port
-
host: replica2
host: production-replica2
port: 26379 # point to sentinel, not to redis port

View File

@ -1,2 +0,0 @@
test:
url: <%= ENV['TEST_GITLAB_REDIS_QUEUES_URL'] %>

View File

@ -1,29 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development:
url: redis://:mynewpassword@localhost:6381/11
sentinels:
-
host: localhost
port: 26381 # point to sentinel, not to redis port
-
host: replica2
port: 26381 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6381/11
sentinels:
-
host: localhost
port: 26381 # point to sentinel, not to redis port
-
host: replica2
port: 26381 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6381/11
sentinels:
-
host: replica1
port: 26381 # point to sentinel, not to redis port
-
host: replica2
port: 26381 # point to sentinel, not to redis port

View File

@ -1,6 +0,0 @@
development:
url: unix:/path/to/redis.queues.sock
test:
url: unix:/path/to/redis.queues.sock
production:
url: unix:/path/to/redis.queues.sock

View File

@ -1,5 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development: redis://:mypassword@localhost:6381/11
test: redis://:mypassword@localhost:6381/11
production: redis://:mypassword@localhost:6381/11

View File

@ -1,3 +0,0 @@
development: unix:/path/to/old/redis.queues.sock
test: unix:/path/to/old/redis.queues.sock
production: unix:/path/to/old/redis.queues.sock

View File

@ -1,2 +0,0 @@
test:
url: <%= ENV['TEST_GITLAB_REDIS_SHARED_STATE_URL'] %>

View File

@ -1,29 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development:
url: redis://:mynewpassword@localhost:6382/12
sentinels:
-
host: localhost
port: 26382 # point to sentinel, not to redis port
-
host: replica2
port: 26382 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6382/12
sentinels:
-
host: localhost
port: 26382 # point to sentinel, not to redis port
-
host: replica2
port: 26382 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6382/12
sentinels:
-
host: replica1
port: 26382 # point to sentinel, not to redis port
-
host: replica2
port: 26382 # point to sentinel, not to redis port

View File

@ -1,6 +0,0 @@
development:
url: unix:/path/to/redis.shared_state.sock
test:
url: unix:/path/to/redis.shared_state.sock
production:
url: unix:/path/to/redis.shared_state.sock

View File

@ -1,5 +0,0 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development: redis://:mypassword@localhost:6382/12
test: redis://:mypassword@localhost:6382/12
production: redis://:mypassword@localhost:6382/12

View File

@ -1,3 +0,0 @@
development: unix:/path/to/old/redis.shared_state.sock
test: unix:/path/to/old/redis.shared_state.sock
production: unix:/path/to/old/redis.shared_state.sock

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Startup CSS fixtures', type: :controller do
include JavaScriptFixturesHelpers
let(:use_full_html) { true }
render_views
before(:all) do
stub_feature_flags(combined_menu: true)
clean_frontend_fixtures('startup_css/')
end
shared_examples 'startup css project fixtures' do |type|
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :public, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png'), creator: user) }
before do
sign_in(user)
end
it "startup_css/project-#{type}-legacy-menu.html" do
stub_feature_flags(combined_menu: false)
get :show, params: {
namespace_id: project.namespace.to_param,
id: project
}
expect(response).to be_successful
end
it "startup_css/project-#{type}.html" do
get :show, params: {
namespace_id: project.namespace.to_param,
id: project
}
expect(response).to be_successful
end
it "startup_css/project-#{type}-signed-out.html" do
sign_out(user)
get :show, params: {
namespace_id: project.namespace.to_param,
id: project
}
expect(response).to be_successful
end
end
describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
it_behaves_like 'startup css project fixtures', 'general'
end
describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
before do
user.update!(theme_id: 11)
end
it_behaves_like 'startup css project fixtures', 'dark'
end
describe RegistrationsController, '(Startup CSS fixtures)', type: :controller do
it 'startup_css/sign-in.html' do
get :new
expect(response).to be_successful
end
end
end

View File

@ -150,4 +150,140 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
it_behaves_like 'track generic sql events'
end
end
context 'Database Load Balancing enabled' do
let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10', connection: connection } }
let(:event) do
double(
:event,
name: 'sql.active_record',
duration: 2,
payload: payload
)
end
# Emulate Marginalia pre-pending comments
def sql(query, comments: true)
if comments && !%w[BEGIN COMMIT].include?(query)
"/*application:web,controller:badges,action:pipeline,correlation_id:01EYN39K9VMJC56Z7808N7RSRH*/ #{query}"
else
query
end
end
shared_examples 'track sql events for each role' do
where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query, :record_wal_query) do
'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false | false
'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false | false
'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false | false
'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false | false
'SQL' | 'DELETE FROM users where id = 10' | true | true | false | false
'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false | false
'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false | false
'SQL' | 'SELECT pg_current_wal_insert_lsn()::text AS location' | true | false | false | true
'SQL' | 'SELECT pg_last_wal_replay_lsn()::text AS location' | true | false | false | true
'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true | false
'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false | false
nil | 'BEGIN' | false | false | false | false
nil | 'COMMIT' | false | false | false | false
end
with_them do
let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } }
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
end
context 'query using a connection to a replica' do
before do
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(:replica)
end
it 'queries connection db role' do
subscriber.sql(event)
if record_query
expect(Gitlab::Database::LoadBalancing).to have_received(:db_role_for_connection).with(connection)
end
end
it_behaves_like 'record ActiveRecord metrics', :replica
it_behaves_like 'store ActiveRecord info in RequestStore', :replica
end
context 'query using a connection to a primary' do
before do
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(:primary)
end
it 'queries connection db role' do
subscriber.sql(event)
if record_query
expect(Gitlab::Database::LoadBalancing).to have_received(:db_role_for_connection).with(connection)
end
end
it_behaves_like 'record ActiveRecord metrics', :primary
it_behaves_like 'store ActiveRecord info in RequestStore', :primary
end
context 'query using a connection to an unknown source' do
let(:transaction) { double('Gitlab::Metrics::WebTransaction') }
before do
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(nil)
allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction)
allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(nil)
allow(transaction).to receive(:increment)
allow(transaction).to receive(:observe)
end
it 'does not record DB role metrics' do
expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_primary_count_total".to_sym, any_args)
expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_replica_count_total".to_sym, any_args)
expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_primary_cached_count_total".to_sym, any_args)
expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_replica_cached_count_total".to_sym, any_args)
expect(transaction).not_to receive(:observe).with("gitlab_sql_primary_duration_seconds".to_sym, any_args)
expect(transaction).not_to receive(:observe).with("gitlab_sql_replica_duration_seconds".to_sym, any_args)
subscriber.sql(event)
end
it 'does not store DB roles into into RequestStore' do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
expect(described_class.db_counter_payload).to include(
db_primary_cached_count: 0,
db_primary_count: 0,
db_primary_duration_s: 0,
db_replica_cached_count: 0,
db_replica_count: 0,
db_replica_duration_s: 0
)
end
end
end
end
end
context 'without Marginalia comments' do
let(:comments) { false }
it_behaves_like 'track sql events for each role'
end
context 'with Marginalia comments' do
let(:comments) { true }
it_behaves_like 'track sql events for each role'
end
end
end

View File

@ -3,20 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Cache do
let(:config_file_name) { "config/redis.cache.yml" }
let(:instance_specific_config_file) { "config/redis.cache.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_CACHE_CONFIG_FILE" }
let(:config_old_format_socket) { "spec/fixtures/config/redis_cache_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_cache_new_format_socket.yml" }
let(:old_socket_path) {"/path/to/old/redis.cache.sock" }
let(:new_socket_path) {"/path/to/redis.cache.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_cache_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_cache_new_format_host.yml" }
let(:redis_port) { 6380 }
let(:redis_database) { 10 }
let(:sentinel_port) { redis_port + 20000 }
let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_cache_config_with_env.yml"}
let(:config_env_variable_url) {"TEST_GITLAB_REDIS_CACHE_URL"}
let(:class_redis_url) { Gitlab::Redis::Cache::DEFAULT_REDIS_CACHE_URL }
let(:class_redis_url) { 'redis://localhost:6380' }
include_examples "redis_shared_examples"
end

View File

@ -3,20 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Queues do
let(:config_file_name) { "config/redis.queues.yml" }
let(:instance_specific_config_file) { "config/redis.queues.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_QUEUES_CONFIG_FILE" }
let(:config_old_format_socket) { "spec/fixtures/config/redis_queues_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_queues_new_format_socket.yml" }
let(:old_socket_path) {"/path/to/old/redis.queues.sock" }
let(:new_socket_path) {"/path/to/redis.queues.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_queues_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_queues_new_format_host.yml" }
let(:redis_port) { 6381 }
let(:redis_database) { 11 }
let(:sentinel_port) { redis_port + 20000 }
let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_queues_config_with_env.yml"}
let(:config_env_variable_url) {"TEST_GITLAB_REDIS_QUEUES_URL"}
let(:class_redis_url) { Gitlab::Redis::Queues::DEFAULT_REDIS_QUEUES_URL }
let(:class_redis_url) { 'redis://localhost:6381' }
include_examples "redis_shared_examples"
end

View File

@ -3,20 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::SharedState do
let(:config_file_name) { "config/redis.shared_state.yml" }
let(:instance_specific_config_file) { "config/redis.shared_state.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_SHARED_STATE_CONFIG_FILE" }
let(:config_old_format_socket) { "spec/fixtures/config/redis_shared_state_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_shared_state_new_format_socket.yml" }
let(:old_socket_path) {"/path/to/old/redis.shared_state.sock" }
let(:new_socket_path) {"/path/to/redis.shared_state.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_shared_state_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_shared_state_new_format_host.yml" }
let(:redis_port) { 6382 }
let(:redis_database) { 12 }
let(:sentinel_port) { redis_port + 20000 }
let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_shared_state_config_with_env.yml"}
let(:config_env_variable_url) {"TEST_GITLAB_REDIS_SHARED_STATE_URL"}
let(:class_redis_url) { Gitlab::Redis::SharedState::DEFAULT_REDIS_SHARED_STATE_URL }
let(:class_redis_url) { 'redis://localhost:6382' }
include_examples "redis_shared_examples"
end

View File

@ -3,47 +3,15 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Wrapper do
let(:config_file_name) { "config/resque.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_CONFIG_FILE" }
let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
let(:old_socket_path) {"/path/to/old/redis.sock" }
let(:new_socket_path) {"/path/to/redis.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
let(:redis_port) { 6379 }
let(:redis_database) { 99 }
let(:sentinel_port) { redis_port + 20000 }
let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
let(:class_redis_url) { Gitlab::Redis::Wrapper::DEFAULT_REDIS_URL }
include_examples "redis_shared_examples" do
before do
allow(described_class).to receive(:instrumentation_class) do
::Gitlab::Instrumentation::Redis::Cache
end
end
end
describe '.version' do
it 'returns a version' do
expect(described_class.version).to be_present
end
end
describe '.instrumentation_class' do
it 'raises a NotImplementedError' do
expect(described_class).to receive(:instrumentation_class).and_call_original
expect { described_class.instrumentation_class }.to raise_error(NotImplementedError)
it 'raises a NameError' do
expect { described_class.instrumentation_class }.to raise_error(NameError)
end
end
describe '.config_file_path' do
it 'returns the absolute path to the configuration file' do
expect(described_class.config_file_path('foo.yml'))
.to eq Rails.root.join('config', 'foo.yml').to_s
describe '.default_url' do
it 'is not implemented' do
expect { described_class.default_url }.to raise_error(NotImplementedError)
end
end
end

View File

@ -2240,14 +2240,16 @@ RSpec.describe Group do
let_it_be(:group) { create(:group, :public) }
it 'returns a maximum of ten owners of the group in recent_sign_in descending order' do
users = create_list(:user, 12, :with_sign_ins)
limit = 2
stub_const("Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT", limit)
users = create_list(:user, limit + 1, :with_sign_ins)
active_owners = users.map do |user|
create(:group_member, :owner, group: group, user: user)
end
active_owners_in_recent_sign_in_desc_order = group.members_and_requesters
.id_in(active_owners)
.order_recent_sign_in.limit(10)
.order_recent_sign_in.limit(limit)
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end

View File

@ -6229,14 +6229,16 @@ RSpec.describe Project, factory_default: :keep do
let_it_be(:project) { create(:project, group: create(:group, :public)) }
it 'returns a maximum of ten maintainers of the project in recent_sign_in descending order' do
users = create_list(:user, 12, :with_sign_ins)
limit = 2
stub_const("Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT", limit)
users = create_list(:user, limit + 1, :with_sign_ins)
active_maintainers = users.map do |user|
create(:project_member, :maintainer, user: user, project: project)
end
active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters
.id_in(active_maintainers)
.order_recent_sign_in.limit(10)
.order_recent_sign_in.limit(limit)
expect(project.access_request_approvers_to_be_notified).to eq(active_maintainers_in_recent_sign_in_desc_order)
end

View File

@ -31,4 +31,37 @@ RSpec.describe UserProjectAccessChangedService do
priority: described_class::LOW_PRIORITY)
end
end
context 'with load balancing enabled' do
let(:service) { UserProjectAccessChangedService.new([1, 2]) }
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
.with([[1], [2]])
.and_return(10)
end
it 'sticks all the updated users and returns the original result', :aggregate_failures do
expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:bulk_stick).with(:user, [1, 2])
expect(service.execute).to eq(10)
end
it 'avoids N+1 cached queries', :use_sql_query_cache, :request_store do
# Run this once to establish a baseline
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
service.execute
end
service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5])
allow(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
.with([[1], [2], [3], [4], [5]])
.and_return(10)
expect { service.execute }.not_to exceed_all_query_limit(control_count.count)
end
end
end

View File

@ -66,6 +66,14 @@ module JavaScriptFixturesHelpers
File.write(full_fixture_path, fixture)
end
def parse_html(fixture)
if respond_to?(:use_full_html) && public_send(:use_full_html)
Nokogiri::HTML::Document.parse(fixture)
else
Nokogiri::HTML::DocumentFragment.parse(fixture)
end
end
# Private: Prepare a response object for use as a frontend fixture
#
# response - response object to prepare
@ -76,7 +84,7 @@ module JavaScriptFixturesHelpers
response_mime_type = Mime::Type.lookup(response.media_type)
if response_mime_type.html?
doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
doc = parse_html(fixture)
link_tags = doc.css('link')
link_tags.remove

View File

@ -4,9 +4,21 @@ RSpec.shared_examples "redis_shared_examples" do
include StubENV
let(:test_redis_url) { "redis://redishost:#{redis_port}"}
let(:config_file_name) { instance_specific_config_file }
let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
let(:old_socket_path) {"/path/to/old/redis.sock" }
let(:new_socket_path) {"/path/to/redis.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
let(:redis_port) { 6379 }
let(:redis_database) { 99 }
let(:sentinel_port) { 26379 }
let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
before do
stub_env(environment_config_file_name, Rails.root.join(config_file_name))
allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
clear_raw_config
end
@ -14,8 +26,72 @@ RSpec.shared_examples "redis_shared_examples" do
clear_raw_config
end
describe '.config_file_name' do
subject { described_class.config_file_name }
let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
before do
# Undo top-level stub of config_file_name because we are testing that method now.
allow(described_class).to receive(:config_file_name).and_call_original
allow(described_class).to receive(:rails_root).and_return(rails_root)
FileUtils.mkdir_p(File.join(rails_root, 'config'))
end
after do
FileUtils.rm_rf(rails_root)
end
context 'when there is no config file anywhere' do
it { expect(subject).to be_nil }
context 'but resque.yml exists' do
before do
FileUtils.touch(File.join(rails_root, 'config', 'resque.yml'))
end
it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
it 'returns a path that exists' do
expect(File.file?(subject)).to eq(true)
end
context 'and there is a global env override' do
before do
stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
end
it { expect(subject).to eq('global override') }
context 'and there is an instance specific config file' do
before do
FileUtils.touch(File.join(rails_root, instance_specific_config_file))
end
it { expect(subject).to eq("#{rails_root}/#{instance_specific_config_file}") }
it 'returns a path that exists' do
expect(File.file?(subject)).to eq(true)
end
context 'and there is a specific env override' do
before do
stub_env(environment_config_file_name, 'instance specific override')
end
it { expect(subject).to eq('instance specific override') }
end
end
end
end
end
end
describe '.params' do
subject { described_class.params }
subject { described_class.new(rails_env).params }
let(:rails_env) { 'development' }
it 'withstands mutation' do
params1 = described_class.params
@ -58,9 +134,19 @@ RSpec.shared_examples "redis_shared_examples" do
context 'with new format' do
let(:config_file_name) { config_new_format_host }
it 'returns hash with host, port, db, and password' do
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: redis_port, db: redis_database)
is_expected.not_to have_key(:url)
where(:rails_env, :host) do
[
%w[development development-host],
%w[test test-host],
%w[production production-host]
]
end
with_them do
it 'returns hash with host, port, db, and password' do
is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database)
is_expected.not_to have_key(:url)
end
end
end
end
@ -88,6 +174,12 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
describe '.version' do
it 'returns a version' do
expect(described_class.version).to be_present
end
end
describe '._raw_config' do
subject { described_class._raw_config }
@ -143,14 +235,26 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '#sentinels' do
subject { described_class.new(Rails.env).sentinels }
subject { described_class.new(rails_env).sentinels }
let(:rails_env) { 'development' }
context 'when sentinels are defined' do
let(:config_file_name) { config_new_format_host }
it 'returns an array of hashes with host and port keys' do
is_expected.to include(host: 'localhost', port: sentinel_port)
is_expected.to include(host: 'replica2', port: sentinel_port)
where(:rails_env, :hosts) do
[
['development', %w[development-replica1 development-replica2]],
['test', %w[test-replica1 test-replica2]],
['production', %w[production-replica1 production-replica2]]
]
end
with_them do
it 'returns an array of hashes with host and port keys' do
is_expected.to include(host: hosts[0], port: sentinel_port)
is_expected.to include(host: hosts[1], port: sentinel_port)
end
end
end

154
yarn.lock
View File

@ -2741,6 +2741,11 @@ bonjour@^3.5.0:
multicast-dns "^6.0.1"
multicast-dns-service-types "^1.1.0"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bootstrap-vue@2.17.3:
version "2.17.3"
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.17.3.tgz#3d78b7b4ff992a8ad69d2ed1c7413fcfdcefaec7"
@ -3140,6 +3145,30 @@ charenc@~0.0.1:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
cheerio-select@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9"
integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew==
dependencies:
css-select "^4.1.2"
css-what "^5.0.0"
domelementtype "^2.2.0"
domhandler "^4.2.0"
domutils "^2.6.0"
cheerio@^1.0.0-rc.9:
version "1.0.0-rc.9"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.9.tgz#a3ae6b7ce7af80675302ff836f628e7cb786a67f"
integrity sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng==
dependencies:
cheerio-select "^1.4.0"
dom-serializer "^1.3.1"
domhandler "^4.2.0"
htmlparser2 "^6.1.0"
parse5 "^6.0.1"
parse5-htmlparser2-tree-adapter "^6.0.1"
tslib "^2.2.0"
"chokidar@>=3.0.0 <4.0.0", chokidar@^2.1.8, chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.4.0, chokidar@^3.4.1:
version "3.4.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8"
@ -3342,10 +3371,10 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colorette@^1.2.1, colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colors@^1.1.0:
version "1.3.3"
@ -3748,6 +3777,17 @@ css-loader@^2.1.1:
postcss-value-parser "^3.3.0"
schema-utils "^1.0.0"
css-select@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
integrity sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw==
dependencies:
boolbase "^1.0.0"
css-what "^5.0.0"
domhandler "^4.2.0"
domutils "^2.6.0"
nth-check "^2.0.0"
css-selector-parser@^1.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
@ -3767,6 +3807,11 @@ css-values@^0.1.0:
ends-with "^0.2.0"
postcss-value-parser "^3.3.0"
css-what@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47"
integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA==
css@^2.1.0:
version "2.2.4"
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
@ -4428,6 +4473,15 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-serializer@^1.0.1, dom-serializer@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be"
integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
entities "^2.0.0"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@ -4443,10 +4497,10 @@ domelementtype@1, domelementtype@^1.3.1:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domelementtype@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domexception@^2.0.1:
version "2.0.1"
@ -4462,6 +4516,13 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
domhandler@^4.0.0, domhandler@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
dependencies:
domelementtype "^2.2.0"
dompurify@^2.2.8:
version "2.2.8"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.8.tgz#ce88e395f6d00b6dc53f80d6b2a6fdf5446873c6"
@ -4475,6 +4536,15 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
domutils@^2.5.2, domutils@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7"
integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
dot-prop@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
@ -5674,10 +5744,10 @@ glob-to-regexp@^0.4.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
"glob@5 - 7", glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
"glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@ -6076,6 +6146,16 @@ htmlparser2@^3.10.0:
inherits "^2.0.1"
readable-stream "^3.1.1"
htmlparser2@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
domutils "^2.5.2"
entities "^2.0.0"
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
@ -8559,6 +8639,11 @@ multicast-dns@^6.0.1:
dns-packet "^1.0.1"
thunky "^0.1.0"
nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -8766,6 +8851,13 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
nth-check@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
dependencies:
boolbase "^1.0.0"
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@ -9147,7 +9239,19 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
"parse5@5 - 6", parse5@5.1.1:
parse5-htmlparser2-tree-adapter@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
dependencies:
parse5 "^6.0.1"
"parse5@5 - 6", parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
@ -9458,6 +9562,15 @@ postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^8.2.1:
version "8.2.15"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65"
integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.23"
source-map "^0.6.1"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -9764,6 +9877,16 @@ pupa@^2.0.1:
dependencies:
escape-goat "^2.0.0"
purgecss@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.0.3.tgz#8147b429f9c09db719e05d64908ea8b672913742"
integrity sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==
dependencies:
commander "^6.0.0"
glob "^7.0.0"
postcss "^8.2.1"
postcss-selector-parser "^6.0.2"
qjobs@^1.1.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
@ -11580,6 +11703,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"