diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue index 4e2f26af51d..103d08e7f14 100644 --- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue @@ -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'), diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index 79a2a51cebc..1ec4ee522d6 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -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'), }, diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 2336cb18cb5..17cbcabeedb 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,12 +1,12 @@ @@ -102,13 +83,29 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), :ok-disabled="!canSubmit" @primary="onSubmit" > -

+

+ + + + + +

-

+

+ + + +

{ 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? diff --git a/app/models/member.rb b/app/models/member.rb index 0c786cb0f94..b8203849ee9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 45f9b8ac344..fb6e6437964 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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) diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb index 80490bd4c9a..a3e7c4ce5bb 100644 --- a/app/services/user_project_access_changed_service.rb +++ b/app/services/user_project_access_changed_service.rb @@ -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') diff --git a/doc/administration/geo/setup/external_database.md b/doc/administration/geo/setup/external_database.md index 1b0082687e6..59156bdb1c2 100644 --- a/doc/administration/geo/setup/external_database.md +++ b/doc/administration/geo/setup/external_database.md @@ -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: diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index ed61704770b..bbcb2cb9bc4 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -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 diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index 044e6bf5394..8da6602c793 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -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 + }, ... ] ``` diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md index 504453b8e6f..31ede1f9ab3 100644 --- a/doc/development/snowplow/index.md +++ b/doc/development/snowplow/index.md @@ -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. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 308a027f8ce..1e4f4c4af2f 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -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 diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md index a4d17595ddd..2dd00fdc273 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -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//-/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://:@gitlab.example.com/api/v4/groups//packages/pypi/simple --no-deps +pip install --index-url https://:@gitlab.example.com/api/v4/groups//-/packages/pypi/simple --no-deps ``` 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://:@gitlab.example.com/api/v4/groups//packages/pypi/simple +pip install mypypipackage --no-deps --index-url https://:@gitlab.example.com/api/v4/groups//-/packages/pypi/simple ``` ### Package names diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 24006d75c4d..9f7884e1364 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -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') diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index a634f12345a..cf4dbe48643 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -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 diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb index 42d5167beb3..a0777510cd5 100644 --- a/lib/gitlab/redis/queues.rb +++ b/lib/gitlab/redis/queues.rb @@ -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 diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 2848c9f0b59..8bd831741f3 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -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 diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index 94ab67ef08a..ea0802ffbdc 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e8e9b56b456..0df958aa774 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24395,9 +24395,6 @@ msgstr "" msgid "Pipelines|Visualize" msgstr "" -msgid "Pipelines|Write pipeline configuration" -msgstr "" - msgid "Pipelines|invalid" msgstr "" diff --git a/package.json b/package.json index 640ed12762a..067a9745026 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/frontend/startup_css/clean_css.js b/scripts/frontend/startup_css/clean_css.js new file mode 100644 index 00000000000..67a0453e816 --- /dev/null +++ b/scripts/frontend/startup_css/clean_css.js @@ -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 }; diff --git a/scripts/frontend/startup_css/constants.js b/scripts/frontend/startup_css/constants.js new file mode 100644 index 00000000000..9b9dc522a8f --- /dev/null +++ b/scripts/frontend/startup_css/constants.js @@ -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, +}; diff --git a/scripts/frontend/startup_css/get_css_path.js b/scripts/frontend/startup_css/get_css_path.js new file mode 100644 index 00000000000..54078cf3149 --- /dev/null +++ b/scripts/frontend/startup_css/get_css_path.js @@ -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 }; diff --git a/scripts/frontend/startup_css/get_startup_css.js b/scripts/frontend/startup_css/get_startup_css.js new file mode 100644 index 00000000000..ad324dbb7d2 --- /dev/null +++ b/scripts/frontend/startup_css/get_startup_css.js @@ -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 }; diff --git a/scripts/frontend/startup_css/main.js b/scripts/frontend/startup_css/main.js new file mode 100644 index 00000000000..1e8dcbebae2 --- /dev/null +++ b/scripts/frontend/startup_css/main.js @@ -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(); diff --git a/scripts/frontend/startup_css/setup.sh b/scripts/frontend/startup_css/setup.sh new file mode 100755 index 00000000000..795799bd9fd --- /dev/null +++ b/scripts/frontend/startup_css/setup.sh @@ -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 diff --git a/scripts/frontend/startup_css/utils.js b/scripts/frontend/startup_css/utils.js new file mode 100644 index 00000000000..49ad201fb6b --- /dev/null +++ b/scripts/frontend/startup_css/utils.js @@ -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 }; diff --git a/scripts/frontend/startup_css/write_startup_scss.js b/scripts/frontend/startup_css/write_startup_scss.js new file mode 100644 index 00000000000..3754b95cefd --- /dev/null +++ b/scripts/frontend/startup_css/write_startup_scss.js @@ -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 }; diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index ce514bd8905..074549ff591 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -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 diff --git a/spec/fixtures/config/redis_cache_config_with_env.yml b/spec/fixtures/config/redis_cache_config_with_env.yml deleted file mode 100644 index 52fd5a06460..00000000000 --- a/spec/fixtures/config/redis_cache_config_with_env.yml +++ /dev/null @@ -1,2 +0,0 @@ -test: - url: <%= ENV['TEST_GITLAB_REDIS_CACHE_URL'] %> diff --git a/spec/fixtures/config/redis_cache_new_format_host.yml b/spec/fixtures/config/redis_cache_new_format_host.yml deleted file mode 100644 index 02b9e7384ac..00000000000 --- a/spec/fixtures/config/redis_cache_new_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_cache_new_format_socket.yml b/spec/fixtures/config/redis_cache_new_format_socket.yml deleted file mode 100644 index 3634c550163..00000000000 --- a/spec/fixtures/config/redis_cache_new_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_cache_old_format_host.yml b/spec/fixtures/config/redis_cache_old_format_host.yml deleted file mode 100644 index 3609dcd022e..00000000000 --- a/spec/fixtures/config/redis_cache_old_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_cache_old_format_socket.yml b/spec/fixtures/config/redis_cache_old_format_socket.yml deleted file mode 100644 index 26fa0eda245..00000000000 --- a/spec/fixtures/config/redis_cache_old_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml index dc8d74c63fa..3bd91bcee6b 100644 --- a/spec/fixtures/config/redis_new_format_host.yml +++ b/spec/fixtures/config/redis_new_format_host.yml @@ -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 diff --git a/spec/fixtures/config/redis_queues_config_with_env.yml b/spec/fixtures/config/redis_queues_config_with_env.yml deleted file mode 100644 index d16a9d8a7f8..00000000000 --- a/spec/fixtures/config/redis_queues_config_with_env.yml +++ /dev/null @@ -1,2 +0,0 @@ -test: - url: <%= ENV['TEST_GITLAB_REDIS_QUEUES_URL'] %> diff --git a/spec/fixtures/config/redis_queues_new_format_host.yml b/spec/fixtures/config/redis_queues_new_format_host.yml deleted file mode 100644 index bd0d82a5066..00000000000 --- a/spec/fixtures/config/redis_queues_new_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_queues_new_format_socket.yml b/spec/fixtures/config/redis_queues_new_format_socket.yml deleted file mode 100644 index b488d84d022..00000000000 --- a/spec/fixtures/config/redis_queues_new_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_queues_old_format_host.yml b/spec/fixtures/config/redis_queues_old_format_host.yml deleted file mode 100644 index 6531748a8d7..00000000000 --- a/spec/fixtures/config/redis_queues_old_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_queues_old_format_socket.yml b/spec/fixtures/config/redis_queues_old_format_socket.yml deleted file mode 100644 index 53f5db72758..00000000000 --- a/spec/fixtures/config/redis_queues_old_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_shared_state_config_with_env.yml b/spec/fixtures/config/redis_shared_state_config_with_env.yml deleted file mode 100644 index eab7203d0de..00000000000 --- a/spec/fixtures/config/redis_shared_state_config_with_env.yml +++ /dev/null @@ -1,2 +0,0 @@ -test: - url: <%= ENV['TEST_GITLAB_REDIS_SHARED_STATE_URL'] %> diff --git a/spec/fixtures/config/redis_shared_state_new_format_host.yml b/spec/fixtures/config/redis_shared_state_new_format_host.yml deleted file mode 100644 index 1c690567ae9..00000000000 --- a/spec/fixtures/config/redis_shared_state_new_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_shared_state_new_format_socket.yml b/spec/fixtures/config/redis_shared_state_new_format_socket.yml deleted file mode 100644 index 1b0e699729e..00000000000 --- a/spec/fixtures/config/redis_shared_state_new_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_shared_state_old_format_host.yml b/spec/fixtures/config/redis_shared_state_old_format_host.yml deleted file mode 100644 index fef5e768c5d..00000000000 --- a/spec/fixtures/config/redis_shared_state_old_format_host.yml +++ /dev/null @@ -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 diff --git a/spec/fixtures/config/redis_shared_state_old_format_socket.yml b/spec/fixtures/config/redis_shared_state_old_format_socket.yml deleted file mode 100644 index 4746afbb5ef..00000000000 --- a/spec/fixtures/config/redis_shared_state_old_format_socket.yml +++ /dev/null @@ -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 diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb new file mode 100644 index 00000000000..134d29d3106 --- /dev/null +++ b/spec/frontend/fixtures/startup_css.rb @@ -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 diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 6bfcfa21289..cffa62c3a52 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -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 diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 5f73b84288d..bc33bbe115a 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -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 diff --git a/spec/lib/gitlab/redis/queues_spec.rb b/spec/lib/gitlab/redis/queues_spec.rb index 8a32c991943..85fca9320cf 100644 --- a/spec/lib/gitlab/redis/queues_spec.rb +++ b/spec/lib/gitlab/redis/queues_spec.rb @@ -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 diff --git a/spec/lib/gitlab/redis/shared_state_spec.rb b/spec/lib/gitlab/redis/shared_state_spec.rb index bd90e4c750d..2543c4d9678 100644 --- a/spec/lib/gitlab/redis/shared_state_spec.rb +++ b/spec/lib/gitlab/redis/shared_state_spec.rb @@ -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 diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb index ec233c022ee..39156d06849 100644 --- a/spec/lib/gitlab/redis/wrapper_spec.rb +++ b/spec/lib/gitlab/redis/wrapper_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 5ca794a2076..c8acc85cfd9 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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 diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d1d42892bec..6de1a246173 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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 diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb index 070782992e7..28d9fa85c7e 100644 --- a/spec/services/user_project_access_changed_service_spec.rb +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -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 diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 28375c1d51e..8fd8a548011 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -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 diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index f5f6a69738b..494a895e929 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 155db1e0bf5..403ee6b3aac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"