+
## Configure Consul
The following IPs will be used as an example:
@@ -662,52 +748,6 @@ The following IPs will be used as an example:
-### Configure the internal load balancer
-
-If you're running more than one PgBouncer node as recommended, then at this time you'll need to set
-up a TCP internal load balancer to serve each correctly.
-
-The following IP will be used as an example:
-
-- `10.6.0.40`: Internal Load Balancer
-
-Here's how you could do it with [HAProxy](https://www.haproxy.org/):
-
-```plaintext
-global
- log /dev/log local0
- log localhost local1 notice
- log stdout format raw local0
-
-defaults
- log global
- default-server inter 10s fall 3 rise 2
- balance leastconn
-
-frontend internal-pgbouncer-tcp-in
- bind *:6432
- mode tcp
- option tcplog
-
- default_backend pgbouncer
-
-backend pgbouncer
- mode tcp
- option tcp-check
-
- server pgbouncer1 10.6.0.21:6432 check
- server pgbouncer2 10.6.0.22:6432 check
- server pgbouncer3 10.6.0.23:6432 check
-```
-
-Refer to your preferred Load Balancer's documentation for further guidance.
-
-
-
## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
@@ -1302,19 +1342,283 @@ To configure the Sentinel Queues server:
-## Configure Gitaly
+## Configure Gitaly Cluster
-NOTE:
-[Gitaly Cluster](../gitaly/praefect.md) support
-for the Reference Architectures is being
-worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
-some Architecture specs will likely change as a result to support the new
-and improved designed.
+[Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
+In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
-[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
-specifically the number of projects and those projects' sizes. It's recommended
-that a Gitaly server node stores no more than 5 TB of data. Depending on your
-repository storage requirements, you may require additional Gitaly server nodes.
+The recommended cluster setup includes the following components:
+
+- 3 Gitaly nodes: Replicated storage of Git repositories.
+- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
+- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
+ is required for Praefect database connections to be made highly available.
+- 1 load balancer: A load balancer is required for Praefect. The
+ [internal load balancer](#configure-the-internal-load-balancer) will be used.
+
+This section will detail how to configure the recommended standard setup in order.
+For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
+
+### Configure Praefect PostgreSQL
+
+Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
+
+If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+
+#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
+
+The following IPs will be used as an example:
+
+- `10.6.0.141`: Praefect PostgreSQL
+
+First, make sure to [install](https://about.gitlab.com/install/)
+the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
+install the necessary dependencies from step 1, and add the
+GitLab package repository from step 2. When installing GitLab
+in the second step, do not supply the `EXTERNAL_URL` value.
+
+1. SSH in to the Praefect PostgreSQL node.
+1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as ``.
+1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
+ username of `praefect` (recommended). The command will request the password ``
+ and confirmation. Use the value that is output by this command in the next
+ step as the value of ``:
+
+ ```shell
+ sudo gitlab-ctl pg-password-md5 praefect
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+
+ ```ruby
+ # Disable all components except PostgreSQL and Consul
+ roles ['postgres_role']
+ repmgr['enable'] = false
+ patroni['enable'] = false
+
+ # PostgreSQL configuration
+ postgresql['listen_address'] = '0.0.0.0'
+ postgresql['max_connections'] = 200
+
+ gitlab_rails['auto_migrate'] = false
+
+ # Configure the Consul agent
+ consul['enable'] = true
+ ## Enable service discovery for Prometheus
+ consul['monitoring_service_discovery'] = true
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ #
+ # Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
+ postgresql['sql_user_password'] = ""
+
+ # Replace XXX.XXX.XXX.XXX/YY with Network Address
+ postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
+
+ # Set the network addresses that the exporters will listen on for monitoring
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ postgres_exporter['listen_address'] = '0.0.0.0:9187'
+
+ ## The IPs of the Consul server nodes
+ ## You can also use FQDNs and intermix them with IPs
+ consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
+ }
+ #
+ # END user configuration
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+1. Follow the [post configuration](#praefect-postgresql-post-configuration).
+
+
+
+#### Praefect HA PostgreSQL third-party solution
+
+[As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
+Praefect's database is recommended if aiming for full High Availability.
+
+There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
+
+- A static IP for all connections that doesn't change on failover.
+- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
+
+Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
+
+Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
+
+#### Praefect PostgreSQL post-configuration
+
+After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
+
+We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
+The password for the user is the same as the one you configured earlier as ``.
+
+This is how this would work with a Omnibus GitLab PostgreSQL setup:
+
+1. SSH in to the Praefect PostgreSQL node.
+1. Connect to the PostgreSQL server with administrative access.
+ The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
+ The database `template1` is used because it is created by default on all PostgreSQL servers.
+
+ ```shell
+ /opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
+ ```
+
+1. Create the new user `praefect`, replacing ``:
+
+ ```shell
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD ;
+ ```
+
+1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
+
+ ```shell
+ /opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
+ ```
+
+1. Create a new database `praefect_production`:
+
+ ```shell
+ CREATE DATABASE praefect_production WITH ENCODING=UTF8;
+ ```
+
+
+
+### Configure Praefect
+
+Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
+it. This section details how to configure it.
+
+Praefect requires several secret tokens to secure communications across the Cluster:
+
+- ``: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
+- ``: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
+- ``: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
+
+Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
+the details of each Gitaly node that makes up the cluster. Each storage is also given a name
+and this name is used in several areas of the config. In this guide, the name of the storage will be
+`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
+to use Gitaly Cluster, you may need to use a different name.
+Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
+
+The following IPs will be used as an example:
+
+- `10.6.0.131`: Praefect 1
+- `10.6.0.132`: Praefect 2
+- `10.6.0.133`: Praefect 3
+
+To configure the Praefect nodes, on each one:
+
+1. SSH in to the Praefect server.
+1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
+ package of your choice. Be sure to follow _only_ installation steps 1 and 2
+ on the page.
+1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
+
+ ```ruby
+ # Avoid running unnecessary services on the Gitaly server
+ postgresql['enable'] = false
+ redis['enable'] = false
+ nginx['enable'] = false
+ puma['enable'] = false
+ unicorn['enable'] = false
+ sidekiq['enable'] = false
+ gitlab_workhorse['enable'] = false
+ grafana['enable'] = false
+
+ # If you run a separate monitoring node you can disable these services
+ alertmanager['enable'] = false
+ prometheus['enable'] = false
+
+ # Praefect Configuration
+ praefect['enable'] = true
+ praefect['listen_addr'] = '0.0.0.0:2305'
+
+ gitlab_rails['rake_cache_clear'] = false
+ gitlab_rails['auto_migrate'] = false
+
+ # Configure the Consul agent
+ consul['enable'] = true
+ ## Enable service discovery for Prometheus
+ consul['monitoring_service_discovery'] = true
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ #
+
+ # Praefect External Token
+ # This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
+ praefect['auth_token'] = ''
+
+ # Praefect Database Settings
+ praefect['database_host'] = '10.6.0.141'
+ praefect['database_port'] = 5432
+ # `no_proxy` settings must always be a direct connection for caching
+ praefect['database_host_no_proxy'] = '10.6.0.141'
+ praefect['database_port_no_proxy'] = 5432
+ praefect['database_dbname'] = 'praefect_production'
+ praefect['database_user'] = 'praefect'
+ praefect['database_password'] = ''
+
+ # Praefect Virtual Storage config
+ # Name of storage hash must match storage name in git_data_dirs on GitLab
+ # server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
+ praefect['virtual_storages'] = {
+ 'default' => {
+ 'gitaly-1' => {
+ 'address' => 'tcp://10.6.0.91:8075',
+ 'token' => '',
+ 'primary' => true
+ },
+ 'gitaly-2' => {
+ 'address' => 'tcp://10.6.0.92:8075',
+ 'token' => ''
+ },
+ 'gitaly-3' => {
+ 'address' => 'tcp://10.6.0.93:8075',
+ 'token' => ''
+ },
+ }
+ }
+
+ # Set the network addresses that the exporters will listen on for monitoring
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
+
+ ## The IPs of the Consul server nodes
+ ## You can also use FQDNs and intermix them with IPs
+ consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
+ }
+ #
+ # END user configuration
+ ```
+
+ 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
+ then replace the file of the same name on this server. If that file isn't on
+ this server, add the file from your Consul server to this server.
+
+ 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+### Configure Gitaly
+
+The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
+requirements that are dependent on data, specifically the number of projects
+and those projects' sizes. It's recommended that a Gitaly Cluster stores
+no more than 5 TB of data on each node. Depending on your
+repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
@@ -1325,36 +1629,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly.
-Be sure to note the following items:
+Gitaly servers must not be exposed to the public internet, as Gitaly's network
+traffic is unencrypted by default. The use of a firewall is highly recommended
+to restrict access to the Gitaly server. Another option is to
+[use TLS](#gitaly-cluster-tls-support).
-- The GitLab Rails application shards repositories into
- [repository storage paths](../repository_storage_paths.md).
-- A Gitaly server can host one or more storage paths.
-- A GitLab server can use one or more Gitaly server nodes.
-- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
- clients.
-- Gitaly servers must not be exposed to the public internet, as Gitaly's network
- traffic is unencrypted by default. The use of a firewall is highly recommended
- to restrict access to the Gitaly server. Another option is to
- [use TLS](#gitaly-tls-support).
+For configuring Gitaly you should note the following:
-NOTE:
-The token referred to throughout the Gitaly documentation is an arbitrary
-password selected by the administrator. This token is unrelated to tokens
-created for the GitLab API or other similar web API tokens.
+- `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
+- `auth_token` should be the same as `praefect_internal_token`
-This section describes how to configure two Gitaly servers, with the following
-IPs and domain names:
+The following IPs will be used as an example:
-- `10.6.0.91`: Gitaly 1 (`gitaly1.internal`)
-- `10.6.0.92`: Gitaly 2 (`gitaly2.internal`)
-
-Assumptions about your servers include having the secret token be `gitalysecret`,
-and that your GitLab installation has three repository storages:
-
-- `default` on Gitaly 1
-- `storage1` on Gitaly 1
-- `storage2` on Gitaly 2
+- `10.6.0.91`: Gitaly 1
+- `10.6.0.92`: Gitaly 2
+- `10.6.0.93`: Gitaly 3
On each node:
@@ -1364,21 +1653,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token:
-
-
```ruby
# /etc/gitlab/gitlab.rb
- # Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
- # to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
- # The following two values must be the same as their respective values
- # of the GitLab Rails application setup
- gitaly['auth_token'] = 'gitalysecret'
- gitlab_shell['secret_token'] = 'shellsecret'
-
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
@@ -1407,36 +1684,42 @@ On each node:
# firewalls to restrict access to this address/port.
# Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075"
+
+ # Gitaly Auth Token
+ # Should be the same as praefect_internal_token
+ gitaly['auth_token'] = ''
```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- - On `gitaly1.internal`:
+ - On Gitaly node 1:
```ruby
git_data_dirs({
- 'default' => {
- 'path' => '/var/opt/gitlab/git-data'
- },
- 'storage1' => {
- 'path' => '/mnt/gitlab/git-data'
- },
+ "gitaly-1" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
})
```
- - On `gitaly2.internal`:
+ - On Gitaly node 2:
```ruby
git_data_dirs({
- 'storage2' => {
- 'path' => '/mnt/gitlab/git-data'
- },
+ "gitaly-2" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
})
```
-
+ - On Gitaly node 3:
+
+ ```ruby
+ git_data_dirs({
+ "gitaly-3" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
+ })
+ ```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
@@ -1444,34 +1727,44 @@ On each node:
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-### Gitaly TLS support
+### Gitaly Cluster TLS support
-Gitaly supports TLS encryption. To be able to communicate
-with a Gitaly instance that listens for secure connections you will need to use `tls://` URL
-scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
+Praefect supports TLS encryption. To communicate with a Praefect instance that listens
+for secure connections, you must:
-You will need to bring your own certificates as this isn't provided automatically.
-The certificate, or its certificate authority, must be installed on all Gitaly
-nodes (including the Gitaly node using the certificate) and on all client nodes
-that communicate with it following the procedure described in
-[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
+- Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
+ in the GitLab configuration.
+- Bring your own certificates because this isn't provided automatically. The certificate
+ corresponding to each Praefect server must be installed on that Praefect server.
-NOTE:
-The self-signed certificate must specify the address you use to access the
-Gitaly server. If you are addressing the Gitaly server by a hostname, you can
-either use the Common Name field for this, or add it as a Subject Alternative
-Name. If you are addressing the Gitaly server by its IP address, you must add it
-as a Subject Alternative Name to the certificate.
-[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
+Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
+and on all Praefect clients that communicate with it following the procedure described in
+[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
-It's possible to configure Gitaly servers with both an unencrypted listening
-address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
-at the same time. This allows you to do a gradual transition from unencrypted to
-encrypted traffic, if necessary.
+Note the following:
-To configure Gitaly with TLS:
+- The certificate must specify the address you use to access the Praefect server. If
+ addressing the Praefect server by:
-1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+ - Hostname, you can either use the Common Name field for this, or add it as a Subject
+ Alternative Name.
+ - IP address, you must add it as a Subject Alternative Name to the certificate.
+
+- You can configure Praefect servers with both an unencrypted listening address
+ `listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
+ This allows you to do a gradual transition from unencrypted to encrypted traffic, if
+ necessary.
+
+- The Internal Load Balancer will also access to the certificates and need to be configured
+ to allow for TLS passthrough.
+ Refer to the load balancers documentation on how to configure this.
+
+To configure Praefect with TLS:
+
+1. Create certificates for Praefect servers.
+
+1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
+ and certificate there:
```shell
sudo mkdir -p /etc/gitlab/ssl
@@ -1480,27 +1773,34 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem
```
-1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when
- calling into itself:
-
- ```shell
- sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/
- ```
-
1. Edit `/etc/gitlab/gitlab.rb` and add:
-
-
```ruby
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
- gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
+ praefect['tls_listen_addr'] = "0.0.0.0:3305"
+ praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
+ praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
```
-1. Delete `gitaly['listen_addr']` to allow only encrypted connections.
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+1. On the Praefect clients (including each Gitaly server), copy the certificates,
+ or their certificate authority, into `/etc/gitlab/trusted-certs`:
+
+ ```shell
+ sudo cp cert.pem /etc/gitlab/trusted-certs/
+ ```
+
+1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
+ `/etc/gitlab/gitlab.rb` as follows:
+
+ ```ruby
+ git_data_dirs({
+ "default" => {
+ "gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
+ "gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
+ }
+ })
+ ```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
@@ -1586,17 +1886,20 @@ To configure the Sidekiq nodes, on each one:
### Gitaly ###
#######################################
+ # git_data_dirs get configured for the Praefect virtual storage
+ # Address is Internal Load Balancer for Praefect
+ # Token is praefect_external_token
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
+ "default" => {
+ "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => ''
+ }
})
- gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
#######################################
### Postgres ###
#######################################
- gitlab_rails['db_host'] = '10.6.0.20' # internal load balancer IP
+ gitlab_rails['db_host'] = '10.6.0.40' # internal load balancer IP
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = ''
gitlab_rails['db_adapter'] = 'postgresql'
@@ -1669,17 +1972,14 @@ On each node perform the following:
```ruby
external_url 'https://gitlab.example.com'
- # Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
- # to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
- # The following two values must be the same as their respective values
- # of the Gitaly setup
- gitlab_rails['gitaly_token'] = 'gitalysecret'
- gitlab_shell['secret_token'] = 'shellsecret'
-
+ # git_data_dirs get configured for the Praefect virtual storage
+ # Address is Interal Load Balancer for Praefect
+ # Token is praefect_external_token
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
+ "default" => {
+ "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => ''
+ }
})
## Disable components that will not be on the GitLab application server
@@ -1739,14 +2039,15 @@ On each node perform the following:
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the
+1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
- 'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
- 'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' },
+ "default" => {
+ "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => ''
+ }
})
```
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index 8f09d9b469c..bed584e94bd 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -18,6 +18,7 @@ many organizations .
> - **Supported users (approximate):** 1,000
> - **High Availability:** No. For a highly-available environment, you can
> follow the [3K reference architecture](3k_users.md).
+> - **Test requests per second (RPS) rates:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
| Users | Configuration | GCP | AWS | Azure |
|--------------|-------------------------|----------------|-----------------|----------------|
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 66a9c4adbac..12519f47cce 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -13,7 +13,7 @@ full list of reference architectures, see
> - **Supported users (approximate):** 25,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
+> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 1d11f972c2a..353160bea9a 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -14,7 +14,7 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 2,000
> - **High Availability:** No. For a highly-available environment, you can
> follow the [3K reference architecture](3k_users.md).
-> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
+> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|------------------------------------------|--------|-------------------------|----------------|--------------|---------|
@@ -27,44 +27,32 @@ For a full list of reference architectures, see
| Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 |
-```mermaid
-stateDiagram-v2
- [*] --> LoadBalancer
- LoadBalancer --> ApplicationServer
+```plantuml
+@startuml 2k
+card "**External Load Balancer**" as elb #6a9be7
- ApplicationServer --> Gitaly
- ApplicationServer --> Redis
- ApplicationServer --> Database
- ApplicationServer --> ObjectStorage
+collections "**GitLab Rails** x3" as gitlab #32CD32
+card "**Prometheus + Grafana**" as monitor #7FFFD4
+card "**Gitaly**" as gitaly #FF8C00
+card "**PostgreSQL**" as postgres #4EA7FF
+card "**Redis**" as redis #FF6347
+cloud "**Object Storage**" as object_storage #white
- ApplicationMonitoring -->ApplicationServer
- ApplicationMonitoring -->Redis
- ApplicationMonitoring -->Database
+elb -[#6a9be7]-> gitlab
+elb -[#6a9be7]--> monitor
+gitlab -[#32CD32]--> gitaly
+gitlab -[#32CD32]--> postgres
+gitlab -[#32CD32]-> object_storage
+gitlab -[#32CD32]--> redis
- state Database {
- "PG_Node"
- }
- state Redis {
- "Redis_Node"
- }
+monitor .[#7FFFD4]u-> gitlab
+monitor .[#7FFFD4]-> gitaly
+monitor .[#7FFFD4]-> postgres
+monitor .[#7FFFD4,norank]--> redis
+monitor .[#7FFFD4,norank]u--> elb
- state Gitaly {
- "Gitaly"
- }
-
- state ApplicationServer {
- "AppServ_1..2"
- }
-
- state LoadBalancer {
- "LoadBalancer"
- }
-
- state ApplicationMonitoring {
- "Prometheus"
- "Grafana"
- }
+@enduml
```
The Google Cloud Platform (GCP) architectures were built and tested using the
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 0fc48657c05..d6344d0fa4e 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -21,7 +21,7 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 3,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS
+> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-----------------------|----------------|-------------|---------|
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 7eb56f62774..cd23d1d82c6 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -13,7 +13,7 @@ full list of reference architectures, see
> - **Supported users (approximate):** 50,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
+> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index d617920e2e4..e00456b9be1 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -20,7 +20,7 @@ costly-to-operate environment by using the
> - **Supported users (approximate):** 5,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
+> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|----------------|-------------|----------|
diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md
index 3be7b350469..b149cbc6e9d 100644
--- a/doc/administration/reference_architectures/index.md
+++ b/doc/administration/reference_architectures/index.md
@@ -29,7 +29,8 @@ per 1,000 users:
- API: 20 RPS
- Web: 2 RPS
-- Git: 2 RPS
+- Git (Pull): 2 RPS
+- Git (Push): 0.4 RPS (rounded to nearest integer)
For GitLab instances with less than 2,000 users, it's recommended that you use
the [default setup](#automated-backups) by
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 82b947746de..9f79d3cc675 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -173,7 +173,7 @@ delete the files:
sudo gitlab-rake gitlab:cleanup:orphan_job_artifact_files DRY_RUN=false
```
-You can also limit the number of files to delete with `LIMIT`:
+You can also limit the number of files to delete with `LIMIT` (default `100`):
```shell
sudo gitlab-rake gitlab:cleanup:orphan_job_artifact_files LIMIT=100
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 60fb9a7d6ea..f988d825c9f 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -534,14 +534,16 @@ limiting responses](#rate-limiting-responses).
The following table describes the rate limits for GitLab.com, both before and
after the limits change in January, 2021:
-| Rate limit | Before 2021-01-18 | From 2021-01-18 |
-|:--------------------------------------------------------------------------|:----------------------------|:------------------------------|
-| **Protected paths** (for a given **IP address**) | **10** requests per minute | **10** requests per minute |
-| **Raw endpoint** traffic (for a given **project, commit, and file path**) | **300** requests per minute | **300** requests per minute |
-| **Unauthenticated** traffic (from a given **IP address**) | No specific limit | **500** requests per minute |
-| **Authenticated** API traffic (for a given **user**) | No specific limit | **2,000** requests per minute |
-| **Authenticated** non-API HTTP traffic (for a given **user**) | No specific limit | **1,000** requests per minute |
-| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute |
+| Rate limit | Before 2021-01-18 | From 2021-01-18 | From 2021-02-12 |
+|:--------------------------------------------------------------------------|:----------------------------|:------------------------------|:------------------------------|
+| **Protected paths** (for a given **IP address**) | **10** requests per minute | **10** requests per minute | **10** requests per minute |
+| **Raw endpoint** traffic (for a given **project, commit, and file path**) | **300** requests per minute | **300** requests per minute | **300** requests per minute |
+| **Unauthenticated** traffic (from a given **IP address**) | No specific limit | **500** requests per minute | **500** requests per minute |
+| **Authenticated** API traffic (for a given **user**) | No specific limit | **2,000** requests per minute | **2,000** requests per minute |
+| **Authenticated** non-API HTTP traffic (for a given **user**) | No specific limit | **1,000** requests per minute | **1,000** requests per minute |
+| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute |
+| **Issue creation** | | **300** requests per minute | **300** requests per minute |
+| **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute |
More details are available on the rate limits for [protected
paths](#protected-paths-throttle) and [raw
diff --git a/lib/gitlab/instrumentation/elasticsearch_transport.rb b/lib/gitlab/instrumentation/elasticsearch_transport.rb
index a1c5bb32fb5..56179eda22d 100644
--- a/lib/gitlab/instrumentation/elasticsearch_transport.rb
+++ b/lib/gitlab/instrumentation/elasticsearch_transport.rb
@@ -9,15 +9,12 @@ module Gitlab
start = Time.now
headers = (headers || {})
.reverse_merge({ 'X-Opaque-Id': Labkit::Correlation::CorrelationId.current_or_new_id })
- response = super
+ super
ensure
if ::Gitlab::SafeRequestStore.active?
duration = (Time.now - start)
::Gitlab::Instrumentation::ElasticsearchTransport.increment_request_count
-
- ::Gitlab::Instrumentation::ElasticsearchTransport.increment_timed_out_count if response&.body&.dig('timed_out')
-
::Gitlab::Instrumentation::ElasticsearchTransport.add_duration(duration)
::Gitlab::Instrumentation::ElasticsearchTransport.add_call_details(duration, method, path, params, body)
end
@@ -28,7 +25,6 @@ module Gitlab
ELASTICSEARCH_REQUEST_COUNT = :elasticsearch_request_count
ELASTICSEARCH_CALL_DURATION = :elasticsearch_call_duration
ELASTICSEARCH_CALL_DETAILS = :elasticsearch_call_details
- ELASTICSEARCH_TIMED_OUT_COUNT = :elasticsearch_timed_out_count
def self.get_request_count
::Gitlab::SafeRequestStore[ELASTICSEARCH_REQUEST_COUNT] || 0
@@ -53,15 +49,6 @@ module Gitlab
::Gitlab::SafeRequestStore[ELASTICSEARCH_CALL_DURATION] += duration
end
- def self.increment_timed_out_count
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] ||= 0
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] += 1
- end
-
- def self.get_timed_out_count
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] || 0
- end
-
def self.add_call_details(duration, method, path, params, body)
return unless Gitlab::PerformanceBar.enabled_for_request?
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 61de6b02453..0d1831ebf9d 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -15,7 +15,6 @@ module Gitlab
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
- :elasticsearch_timed_out_count,
*::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
*::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,
@@ -80,7 +79,6 @@ module Gitlab
payload[:elasticsearch_calls] = elasticsearch_calls
payload[:elasticsearch_duration_s] = Gitlab::Instrumentation::ElasticsearchTransport.query_time
- payload[:elasticsearch_timed_out_count] = Gitlab::Instrumentation::ElasticsearchTransport.get_timed_out_count
end
def instrument_external_http(payload)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4d3a62b20a1..3b948e7fdda 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16,7 +16,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-msgid " %{project_name}#%{issuable_iid} · opened %{issuable_created} by %{author}"
+msgid " %{project_name}#%{issuable_iid} · opened %{issuable_created} by %{author} · updated %{issuable_updated}"
msgstr ""
msgid " %{start} to %{end}"
@@ -8481,6 +8481,9 @@ msgstr ""
msgid "Create new"
msgstr ""
+msgid "Create new %{name} by email"
+msgstr ""
+
msgid "Create new Value Stream"
msgstr ""
@@ -10863,6 +10866,9 @@ msgstr ""
msgid "Email Notification"
msgstr ""
+msgid "Email a new %{name} to this project"
+msgstr ""
+
msgid "Email address to use for Support Desk"
msgstr ""
@@ -10935,12 +10941,6 @@ msgstr ""
msgid "EmailParticipantsWarning|and %{moreCount} more"
msgstr ""
-msgid "EmailToken|reset it"
-msgstr ""
-
-msgid "EmailToken|resetting..."
-msgstr ""
-
msgid "Emails"
msgstr ""
@@ -11226,21 +11226,15 @@ msgstr ""
msgid "Enter one or more user ID separated by commas"
msgstr ""
+msgid "Enter the %{name} description"
+msgstr ""
+
+msgid "Enter the %{name} title"
+msgstr ""
+
msgid "Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes."
msgstr ""
-msgid "Enter the issue description"
-msgstr ""
-
-msgid "Enter the issue title"
-msgstr ""
-
-msgid "Enter the merge request description"
-msgstr ""
-
-msgid "Enter the merge request title"
-msgstr ""
-
msgid "Enter the name of your application, and we'll return a unique %{type}."
msgstr ""
@@ -23934,6 +23928,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!"
+msgstr ""
+
msgid "Protocol"
msgstr ""
@@ -29320,6 +29317,9 @@ msgstr ""
msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr ""
+msgid "The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported."
+msgstr ""
+
msgid "The tag name can't be changed for an existing release."
msgstr ""
@@ -29926,6 +29926,9 @@ msgstr ""
msgid "This is a merge train pipeline"
msgstr ""
+msgid "This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens."
+msgstr ""
+
msgid "This is a security log of important events involving your account."
msgstr ""
@@ -30223,6 +30226,9 @@ msgstr ""
msgid "Threat Monitoring"
msgstr ""
+msgid "ThreatMonitoring|Alert Details"
+msgstr ""
+
msgid "ThreatMonitoring|Alerts"
msgstr ""
@@ -33349,6 +33355,9 @@ msgstr ""
msgid "You can create a new %{link}."
msgstr ""
+msgid "You can create a new %{name} inside this project by sending an email to the following email address:"
+msgstr ""
+
msgid "You can create a new Personal Access Token by visiting %{link}"
msgstr ""
diff --git a/scripts/flaky_examples/detect-new-flaky-examples b/scripts/flaky_examples/detect-new-flaky-examples
index 3bee4f9a34b..4805c5054a5 100755
--- a/scripts/flaky_examples/detect-new-flaky-examples
+++ b/scripts/flaky_examples/detect-new-flaky-examples
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
diff --git a/scripts/flaky_examples/prune-old-flaky-examples b/scripts/flaky_examples/prune-old-flaky-examples
index 4df49c6d8fa..8c09c4cc860 100755
--- a/scripts/flaky_examples/prune-old-flaky-examples
+++ b/scripts/flaky_examples/prune-old-flaky-examples
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# lib/rspec_flaky/flaky_examples_collection.rb is requiring
# `active_support/hash_with_indifferent_access`, and we install the `activesupport`
diff --git a/scripts/gather-test-memory-data b/scripts/gather-test-memory-data
index 9992a83e6a6..3156365ac19 100755
--- a/scripts/gather-test-memory-data
+++ b/scripts/gather-test-memory-data
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'csv'
diff --git a/scripts/generate-gems-memory-metrics-static b/scripts/generate-gems-memory-metrics-static
index aa7ce3615bf..42191f078f1 100755
--- a/scripts/generate-gems-memory-metrics-static
+++ b/scripts/generate-gems-memory-metrics-static
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} " unless ARGV.length == 1
memory_bundle_objects_file_name = ARGV.first
diff --git a/scripts/generate-gems-size-metrics-static b/scripts/generate-gems-size-metrics-static
index ceec8aaccf1..2406e720916 100755
--- a/scripts/generate-gems-size-metrics-static
+++ b/scripts/generate-gems-size-metrics-static
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} " unless ARGV.length == 1
memory_bundle_mem_file_name = ARGV.first
diff --git a/scripts/generate-memory-metrics-on-boot b/scripts/generate-memory-metrics-on-boot
index 5197a8fcdcd..945661aa057 100755
--- a/scripts/generate-memory-metrics-on-boot
+++ b/scripts/generate-memory-metrics-on-boot
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} " unless ARGV.length == 1
memory_bundle_mem_file_name = ARGV.first
diff --git a/scripts/generate-test-mapping b/scripts/generate-test-mapping
index eabe6a5b513..c4d0dfea4d8 100755
--- a/scripts/generate-test-mapping
+++ b/scripts/generate-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_generator'
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index 62d3dbda911..bb561e2906a 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'fileutils'
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index caa41a9a0c3..8547d0b13e4 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# This script is used both in CI and in local development 'rspec' runs.
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index c7b3f72d590..2262870eb96 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file contains environment settings for gitaly when it's running
# as part of the gitlab-ce/ee test suite.
#
@@ -52,7 +54,7 @@ module GitalyTest
if ENV['CI']
bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__)
- env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}"
+ env_hash['BUNDLE_FLAGS'] += " --path=#{bundle_path}"
end
env_hash
diff --git a/scripts/insert-rspec-profiling-data b/scripts/insert-rspec-profiling-data
index 3af5fe763a2..b2011858558 100755
--- a/scripts/insert-rspec-profiling-data
+++ b/scripts/insert-rspec-profiling-data
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'csv'
require 'rspec_profiling'
diff --git a/scripts/lint-rugged b/scripts/lint-rugged
index d7af5499e1c..038fd5199c2 100755
--- a/scripts/lint-rugged
+++ b/scripts/lint-rugged
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
ALLOWED = [
# https://gitlab.com/gitlab-org/gitaly/issues/760
diff --git a/scripts/merge-html-reports b/scripts/merge-html-reports
index 7d1e15186c8..de300851990 100755
--- a/scripts/merge-html-reports
+++ b/scripts/merge-html-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'nokogiri'
diff --git a/scripts/merge-reports b/scripts/merge-reports
index 3a421f1f1fc..a1164495f2f 100755
--- a/scripts/merge-reports
+++ b/scripts/merge-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov
index c00dae81c4d..38dd2dfe2e9 100755
--- a/scripts/merge-simplecov
+++ b/scripts/merge-simplecov
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require_relative '../spec/simplecov_env'
SimpleCovEnv.configure_profile
diff --git a/scripts/no-ee-check b/scripts/no-ee-check
index 29d319dc822..a878a4424e9 100755
--- a/scripts/no-ee-check
+++ b/scripts/no-ee-check
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
ee_path = File.join(File.expand_path(__dir__), '../ee')
if Dir.exist?(ee_path)
diff --git a/scripts/pack-test-mapping b/scripts/pack-test-mapping
index 58ace3eca67..b5148cd1882 100755
--- a/scripts/pack-test-mapping
+++ b/scripts/pack-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 9103a9c14af..febfdbd1da4 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# We don't have auto-loading here
require_relative '../lib/gitlab'
diff --git a/scripts/sync-reports b/scripts/sync-reports
index 5ed65e78005..73afd276e6c 100755
--- a/scripts/sync-reports
+++ b/scripts/sync-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'rubygems'
require 'fog/aws'
diff --git a/scripts/unpack-test-mapping b/scripts/unpack-test-mapping
index c0f706c3f9f..7176f9cecea 100755
--- a/scripts/unpack-test-mapping
+++ b/scripts/unpack-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
diff --git a/scripts/update-feature-categories b/scripts/update-feature-categories
index ed5d8dccdd6..88520b9f95f 100755
--- a/scripts/update-feature-categories
+++ b/scripts/update-feature-categories
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'uri'
require 'net/http'
diff --git a/scripts/used-feature-flags b/scripts/used-feature-flags
index 7ef3dbafd36..aebd007dda9 100755
--- a/scripts/used-feature-flags
+++ b/scripts/used-feature-flags
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'set'
diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping
index 8bf25ea3b5f..9931e14008a 100755
--- a/scripts/verify-tff-mapping
+++ b/scripts/verify-tff-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'set'
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 6c89b08d9c0..2d6b669f28b 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -18,9 +18,9 @@ RSpec.describe 'Issue Boards', :js do
project.add_maintainer(user)
project.add_maintainer(user2)
- set_cookie('sidebar_collapsed', 'true')
-
sign_in(user)
+
+ set_cookie('sidebar_collapsed', 'true')
end
context 'no lists' do
diff --git a/spec/features/issues/user_creates_issue_by_email_spec.rb b/spec/features/issues/user_creates_issue_by_email_spec.rb
index 5a0036170ab..c47f24ab836 100644
--- a/spec/features/issues/user_creates_issue_by_email_spec.rb
+++ b/spec/features/issues/user_creates_issue_by_email_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Issues > User creates issue by email' do
project.add_developer(user)
end
- describe 'new issue by email' do
+ describe 'new issue by email', :js do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
@@ -28,7 +28,7 @@ RSpec.describe 'Issues > User creates issue by email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
- expect(page).to have_selector("input[value='#{email}']")
+ expect(page.find('input[type="text"]').value).to eq email
end
end
end
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
index a20f65abebf..2b1c25174c2 100644
--- a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -16,17 +16,17 @@ RSpec.describe 'Issues > User resets their incoming email token' do
end
it 'changes incoming email address token', :js do
- find('.issuable-email-modal-btn').click
- previous_token = find('input#issuable_email').value
- find('.incoming-email-token-reset').click
+ page.find('[data-testid="issuable-email-modal-btn"]').click
- wait_for_requests
+ page.within '#issuable-email-modal' do
+ previous_token = page.find('input[type="text"]').value
+ page.find('[data-testid="incoming-email-token-reset"]').click
- expect(page).to have_no_field('issuable_email', with: previous_token)
- new_token = project.new_issuable_address(user.reload, 'issue')
- expect(page).to have_field(
- 'issuable_email',
- with: new_token
- )
+ wait_for_requests
+
+ expect(page.find('input[type="text"]').value).not_to eq previous_token
+ new_token = project.new_issuable_address(user.reload, 'issue')
+ expect(page.find('input[type="text"]').value).to eq new_token
+ end
end
end
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 7cdecefab05..ecd67247362 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -38,7 +38,9 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
required: false,
default: () => [],
},
- ...Object.fromEntries(['target', 'triggers', 'placement'].map((prop) => [prop, {}])),
+ ...Object.fromEntries(
+ ['target', 'triggers', 'placement', 'boundary', 'container'].map((prop) => [prop, {}]),
+ ),
},
render(h) {
return h(
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
new file mode 100644
index 00000000000..0730cfd453e
--- /dev/null
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -0,0 +1,80 @@
+import { mount } from '@vue/test-utils';
+import { GlPopover, GlLink, GlButton } from '@gitlab/ui';
+import FeatureHighlightPopover from '~/feature_highlight/feature_highlight_popover.vue';
+import { dismiss } from '~/feature_highlight/feature_highlight_helper';
+import { POPOVER_TARGET_ID } from '~/feature_highlight/constants';
+
+jest.mock('~/feature_highlight/feature_highlight_helper');
+
+describe('feature_highlight/feature_highlight_popover', () => {
+ let wrapper;
+ const props = {
+ autoDevopsHelpPath: '/help/autodevops',
+ highlightId: '123',
+ dismissEndpoint: '/api/dismiss',
+ };
+
+ const buildWrapper = (propsData = props) => {
+ wrapper = mount(FeatureHighlightPopover, {
+ propsData,
+ });
+ };
+ const findPopoverTarget = () => wrapper.find(`#${POPOVER_TARGET_ID}`);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findAutoDevopsHelpLink = () => wrapper.findComponent(GlLink);
+ const findDismissButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders popover target', () => {
+ expect(findPopoverTarget().exists()).toBe(true);
+ });
+
+ it('renders popover', () => {
+ expect(findPopover().props()).toMatchObject({
+ target: POPOVER_TARGET_ID,
+ cssClasses: ['feature-highlight-popover'],
+ triggers: 'hover',
+ container: 'body',
+ placement: 'right',
+ boundary: 'viewport',
+ });
+ });
+
+ it('renders link that points to the autodevops help page', () => {
+ expect(findAutoDevopsHelpLink().attributes().href).toBe(props.autoDevopsHelpPath);
+ expect(findAutoDevopsHelpLink().text()).toBe('Auto DevOps');
+ });
+
+ it('renders dismiss button', () => {
+ expect(findDismissButton().props()).toMatchObject({
+ size: 'small',
+ icon: 'thumb-up',
+ variant: 'confirm',
+ });
+ });
+
+ it('dismisses popover when dismiss button is clicked', async () => {
+ await findDismissButton().trigger('click');
+
+ expect(findPopover().emitted('close')).toHaveLength(1);
+ expect(dismiss).toHaveBeenCalledWith(props.dismissEndpoint, props.highlightId);
+ });
+
+ describe('when popover is dismissed and hidden', () => {
+ it('hides the popover target', async () => {
+ await findDismissButton().trigger('click');
+ findPopover().vm.$emit('hidden');
+ await wrapper.vm.$nextTick();
+
+ expect(findPopoverTarget().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
new file mode 100644
index 00000000000..7e40b903754
--- /dev/null
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -0,0 +1,164 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlSprintf, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+
+const initialEmail = 'user@gitlab.com';
+
+const mockToastShow = jest.fn();
+
+describe('IssuableByEmail', () => {
+ let wrapper;
+ let mockAxios;
+ let glModalDirective;
+
+ function createComponent(injectedProperties = {}) {
+ glModalDirective = jest.fn();
+
+ return extendedWrapper(
+ shallowMount(IssuableByEmail, {
+ stubs: {
+ GlModal,
+ GlSprintf,
+ GlFormInputGroup,
+ GlButton,
+ },
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ provide: {
+ issuableType: 'issue',
+ initialEmail,
+ ...injectedProperties,
+ },
+ }),
+ );
+ }
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ const findFormInputGroup = () => wrapper.find(GlFormInputGroup);
+
+ const clickResetEmail = async () => {
+ wrapper.findByTestId('incoming-email-token-reset').vm.$emit('click');
+
+ await waitForPromises();
+ };
+
+ describe('modal button', () => {
+ it.each`
+ issuableType | buttonText
+ ${'issue'} | ${'Email a new issue to this project'}
+ ${'merge_request'} | ${'Email a new merge request to this project'}
+ `(
+ 'renders a link with "$buttonText" when type is "$issuableType"',
+ ({ issuableType, buttonText }) => {
+ wrapper = createComponent({ issuableType });
+ expect(wrapper.findByTestId('issuable-email-modal-btn').text()).toBe(buttonText);
+ },
+ );
+
+ it('opens the modal when the user clicks the button', () => {
+ wrapper = createComponent();
+
+ wrapper.findByTestId('issuable-email-modal-btn').vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+ });
+
+ describe('modal', () => {
+ it('renders a read-only email input field', () => {
+ wrapper = createComponent();
+
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+
+ it.each`
+ issuableType | subject | body
+ ${'issue'} | ${'Enter the issue title'} | ${'Enter the issue description'}
+ ${'merge_request'} | ${'Enter the merge request title'} | ${'Enter the merge request description'}
+ `('renders a mailto button when type is "$issuableType"', ({ issuableType, subject, body }) => {
+ wrapper = createComponent({
+ issuableType,
+ initialEmail,
+ });
+
+ expect(wrapper.findByTestId('mail-to-btn').attributes('href')).toBe(
+ `mailto:${initialEmail}?subject=${subject}&body=${body}`,
+ );
+ });
+
+ describe('reset email', () => {
+ const resetPath = 'gitlab-test/new_issuable_address?issuable_type=issue';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+ it('should send request to reset email token', async () => {
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(axios.put).toHaveBeenCalledWith(resetPath);
+ });
+
+ it('should update the email when the request succeeds', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.OK, { new_address: 'foo@bar.com' });
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(findFormInputGroup().props('value')).toBe('foo@bar.com');
+ });
+
+ it('should show a toast message when the request fails', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.NOT_FOUND, {});
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ 'There was an error when reseting email token.',
+ { type: 'error' },
+ );
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable_spec.js b/spec/frontend/issuable_spec.js
index 6712b8bfd34..9c8f1e04609 100644
--- a/spec/frontend/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -1,6 +1,3 @@
-import $ from 'jquery';
-import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
@@ -22,43 +19,4 @@ describe('Issuable', () => {
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
});
});
-
- describe('resetIncomingEmailToken', () => {
- let mock;
-
- beforeEach(() => {
- const element = document.createElement('a');
- element.classList.add('incoming-email-token-reset');
- element.setAttribute('href', 'foo');
- document.body.appendChild(element);
-
- const input = document.createElement('input');
- input.setAttribute('id', 'issuable_email');
- document.body.appendChild(input);
-
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- mock = new MockAdaptor(axios);
-
- mock.onPut('foo').reply(200, {
- new_address: 'testing123',
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should send request to reset email token', (done) => {
- jest.spyOn(axios, 'put');
- document.querySelector('.incoming-email-token-reset').click();
-
- setImmediate(() => {
- expect(axios.put).toHaveBeenCalledWith('foo');
- expect($('#issuable_email').val()).toBe('testing123');
-
- done();
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 40af8e60008..d2ab5878172 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -90,6 +90,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable);
+ const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -175,6 +176,15 @@ describe('AlertDetails', () => {
});
});
+ describe('Threat Monitoring details', () => {
+ it('should not render the metrics tab', () => {
+ mountComponent({
+ data: { alert: mockAlert, provide: { isThreatMonitoringPage: true } },
+ });
+ expect(findMetricsTab().exists()).toBe(false);
+ });
+ });
+
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index c2df37821d3..70cf2597963 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import SidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
- function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
+ function mountComponent({
+ mountMethod = shallowMount,
+ stubs = {},
+ alert = {},
+ provide = {},
+ } = {}) {
wrapper = mountMethod(AlertSidebar, {
data() {
return {
@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => {
provide: {
projectPath: 'projectPath',
projectId: '1',
+ ...provide,
},
stubs,
mocks: {
@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => {
});
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
});
+
+ it('should render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(true);
+ });
+ });
+
+ describe('the sidebar renders for threat monitoring', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mountComponent();
+ });
+
+ it('should not render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ provide: { isThreatMonitoringPage: true },
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(false);
+ });
});
});
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c629643e248..264bad92d56 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -200,7 +200,13 @@ RSpec.describe EventsHelper do
it 'returns a project snippet note url' do
event.target = create(:note_on_project_snippet, note: 'keep going')
- expect(subject).to eq("#{project_base_url}/-/snippets/#{event.note_target.id}#note_#{event.target.id}")
+ expect(subject).to eq("#{project_snippet_url(event.note_target.project, event.note_target)}#note_#{event.target.id}")
+ end
+
+ it 'returns a personal snippet note url' do
+ event.target = create(:note_on_personal_snippet, note: 'keep going')
+
+ expect(subject).to eq("#{snippet_url(event.note_target)}#note_#{event.target.id}")
end
it 'returns a project issue url' do
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 5afa693b400..0df194e460a 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -113,7 +113,8 @@ RSpec.describe Projects::AlertManagementHelper do
'alert-id' => alert_id,
'project-path' => project_path,
'project-id' => project_id,
- 'project-issues-path' => issues_path
+ 'project-issues-path' => issues_path,
+ 'page' => 'OPERATIONS'
)
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 69b1499d63c..d0282e14d5f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -564,6 +564,7 @@ project:
- incident_management_oncall_rotations
- debian_distributions
- merge_request_metrics
+- security_orchestration_policy_configuration
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index a5c9cde4c37..5cc911accbb 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe Gitlab::InstrumentationHelper do
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
- :elasticsearch_timed_out_count,
:mem_objects,
:mem_bytes,
:mem_mallocs,
diff --git a/spec/migrations/add_new_post_eoa_plans_spec.rb b/spec/migrations/add_new_post_eoa_plans_spec.rb
new file mode 100644
index 00000000000..5ae227a6617
--- /dev/null
+++ b/spec/migrations/add_new_post_eoa_plans_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210205104425_add_new_post_eoa_plans.rb')
+
+RSpec.describe AddNewPostEoaPlans do
+ let(:plans) { table(:plans) }
+
+ subject(:migration) { described_class.new }
+
+ describe '#up' do
+ it 'creates the two new records' do
+ expect { migration.up }.to change { plans.count }.by(2)
+
+ new_plans = plans.last(2)
+ expect(new_plans.map(&:name)).to contain_exactly('premium', 'ultimate')
+ end
+ end
+
+ describe '#down' do
+ it 'removes these two new records' do
+ plans.create!(name: 'premium', title: 'Premium (Formerly Silver)')
+ plans.create!(name: 'ultimate', title: 'Ultimate (Formerly Gold)')
+
+ expect { migration.down }.to change { plans.count }.by(-2)
+
+ expect(plans.find_by(name: 'premium')).to be_nil
+ expect(plans.find_by(name: 'ultimate')).to be_nil
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
new file mode 100644
index 00000000000..ee1f718849f
--- /dev/null
+++ b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe CleanupProjectsWithBadHasExternalWikiData, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
+ let(:projects) { table(:projects) }
+ let(:services) { table(:services) }
+
+ def create_projects!(num)
+ Array.new(num) do
+ projects.create!(namespace_id: namespace.id)
+ end
+ end
+
+ def create_active_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ def create_disabled_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: false)
+ end
+ end
+
+ def create_active_other_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'NotAnExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ it 'sets `projects.has_external_wiki` correctly' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_without_external_wiki_1,
+ project_without_external_wiki_2 = create_projects!(6)
+
+ create_active_external_wiki_integrations!(
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ )
+
+ create_disabled_external_wiki_integrations!(
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ )
+
+ create_active_other_integrations!(
+ project_without_external_wiki_1,
+ project_without_external_wiki_2
+ )
+
+ # PG triggers on the services table added in a previous migration
+ # will have set the `has_external_wiki` columns to correct data when
+ # the services records were created above.
+ #
+ # We set the `has_external_wiki` columns for projects to incorrect
+ # data manually below to emulate projects in a state before the PG
+ # triggers were added.
+ project_with_external_wiki_2.update!(has_external_wiki: false)
+ project_with_disabled_external_wiki_2.update!(has_external_wiki: true)
+ project_without_external_wiki_2.update!(has_external_wiki: true)
+
+ migrate!
+
+ expected_true = [
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expected_not_true = [
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expect(expected_true).to all(eq(true))
+ expect(expected_not_true).to all(be_falsey)
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 47492715c11..a726c6e6513 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -907,6 +907,58 @@ RSpec.describe Event do
end
end
+ context 'with snippet note' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:note_on_project_snippet) { create(:note_on_project_snippet, author: user) }
+ let_it_be(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: user) }
+ let_it_be(:other_note) { create(:note_on_issue, author: user)}
+ let_it_be(:personal_snippet_event) { create(:event, :commented, project: nil, target: note_on_personal_snippet, author: user) }
+ let_it_be(:project_snippet_event) { create(:event, :commented, project: note_on_project_snippet.project, target: note_on_project_snippet, author: user) }
+ let_it_be(:other_event) { create(:event, :commented, project: other_note.project, target: other_note, author: user) }
+
+ describe '#snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.snippet_note?).to be false
+ end
+ end
+
+ describe '#personal_snippet_note?' do
+ it 'returns false for a project snippet event' do
+ expect(project_snippet_event.personal_snippet_note?).to be false
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.personal_snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.personal_snippet_note?).to be false
+ end
+ end
+
+ describe '#project_snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.project_snippet_note?).to be true
+ end
+
+ it 'returns false for a personal snippet event' do
+ expect(personal_snippet_event.project_snippet_note?).to be false
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.project_snippet_note?).to be false
+ end
+ end
+ end
+
describe '#action_name' do
it 'handles all valid design events' do
created, updated, destroyed, archived = %i[created updated destroyed archived].map do |trait|
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 5c14a3db54e..364b80e8601 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -837,6 +837,16 @@ RSpec.describe Note do
end
end
+ describe '#for_project_snippet?' do
+ it 'returns true for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_project_snippet?).to be true
+ end
+
+ it 'returns false for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_project_snippet?).to be false
+ end
+ end
+
describe '#for_personal_snippet?' do
it 'returns false for a project snippet note' do
expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy