Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue23
-rw-r--r--app/controllers/groups/settings/repository_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/settings/repository_controller.rb2
-rw-r--r--app/mailers/emails/groups.rb19
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/services/groups/import_export/export_service.rb8
-rw-r--r--app/services/notification_service.rb12
-rw-r--r--app/views/groups/settings/_export.html.haml2
-rw-r--r--app/views/groups/settings/repository/show.html.haml2
-rw-r--r--app/views/notify/group_was_exported_email.html.haml9
-rw-r--r--app/views/notify/group_was_exported_email.text.erb6
-rw-r--r--app/views/notify/group_was_not_exported_email.html.haml10
-rw-r--r--app/views/notify/group_was_not_exported_email.text.erb7
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml10
-rw-r--r--changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml5
-rw-r--r--changelogs/unreleased/package-deploy-tokens-fe.yml5
-rw-r--r--doc/administration/gitaly/praefect.md39
-rw-r--r--doc/administration/high_availability/gitaly.md8
-rw-r--r--doc/administration/high_availability/nfs.md3
-rw-r--r--doc/api/deploy_tokens.md4
-rw-r--r--doc/topics/git/partial_clone.md6
-rw-r--r--doc/user/group/settings/import_export.md7
-rw-r--r--doc/user/project/deploy_tokens/index.md19
-rw-r--r--lib/api/deploy_tokens.rb6
-rw-r--r--locale/gitlab.pot36
-rw-r--r--rubocop/cop/performance/ar_exists_and_present_blank.rb57
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js4
-rw-r--r--spec/mailers/emails/groups_spec.rb41
-rw-r--r--spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb111
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb20
33 files changed, 418 insertions, 74 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 435b59f91ef..1a25f1cef39 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -1,16 +1,6 @@
<script>
import * as Sentry from '@sentry/browser';
-import {
- GlAlert,
- GlIcon,
- GlLoadingIcon,
- GlNewDropdown,
- GlNewDropdownItem,
- GlSprintf,
- GlTabs,
- GlTab,
- GlButton,
-} from '@gitlab/ui';
+import { GlAlert, GlIcon, GlLoadingIcon, GlSprintf, GlTabs, GlTab, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
@@ -38,8 +28,6 @@ export default {
GlAlert,
GlIcon,
GlLoadingIcon,
- GlNewDropdown,
- GlNewDropdownItem,
GlSprintf,
GlTab,
GlTabs,
@@ -148,15 +136,6 @@ export default {
class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<h2 data-testid="title">{{ alert.title }}</h2>
- <gl-new-dropdown right>
- <gl-new-dropdown-item
- v-for="(label, field) in $options.statuses"
- :key="field"
- data-testid="statusDropdownItem"
- class="gl-vertical-align-middle"
- >{{ label }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
</div>
<gl-tabs v-if="alert" data-testid="alertDetailsTabs">
<gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
diff --git a/app/controllers/groups/settings/repository_controller.rb b/app/controllers/groups/settings/repository_controller.rb
index 6e8c5628d24..4af5e613296 100644
--- a/app/controllers/groups/settings/repository_controller.rb
+++ b/app/controllers/groups/settings/repository_controller.rb
@@ -46,7 +46,7 @@ module Groups
end
def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :username)
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :read_package_registry, :write_package_registry, :username)
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3dbf57285b0..2cf5d61ec3a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -146,7 +146,7 @@ class GroupsController < Groups::ApplicationController
export_service = Groups::ImportExport::ExportService.new(group: @group, user: current_user)
if export_service.async_execute
- redirect_to edit_group_path(@group), notice: _('Group export started.')
+ redirect_to edit_group_path(@group), notice: _('Group export started. A download link will be sent by email.')
else
redirect_to edit_group_path(@group), alert: _('Group export could not be started.')
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 0aa55dcc5b9..35ca9336613 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -89,7 +89,7 @@ module Projects
end
def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :username)
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :read_package_registry, :write_package_registry, :username)
end
def access_levels_options
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
new file mode 100644
index 00000000000..07812a01202
--- /dev/null
+++ b/app/mailers/emails/groups.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Emails
+ module Groups
+ def group_was_exported_email(current_user, group)
+ group_email(current_user, group, _('Group was exported'))
+ end
+
+ def group_was_not_exported_email(current_user, group, errors)
+ group_email(current_user, group, _('Group export error'), errors: errors)
+ end
+
+ def group_email(current_user, group, subj, errors: nil)
+ @group = group
+ @errors = errors
+ mail(to: current_user.notification_email_for(@group), subject: subject(subj))
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 49eacc44519..d9483bab543 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -17,6 +17,7 @@ class Notify < ApplicationMailer
include Emails::AutoDevops
include Emails::RemoteMirrors
include Emails::Releases
+ include Emails::Groups
helper MilestonesHelper
helper MergeRequestsHelper
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index 9848d8ad5c0..0f2e3bb65f9 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -96,6 +96,8 @@ module Groups
group_name: @group.name,
message: 'Group Import/Export: Export succeeded'
)
+
+ notification_service.group_was_exported(@group, @current_user)
end
def notify_error
@@ -105,6 +107,12 @@ module Groups
error: @shared.errors.join(', '),
message: 'Group Import/Export: Export failed'
)
+
+ notification_service.group_was_not_exported(@group, @current_user, @shared.errors)
+ end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 0a8d8e769ec..4c1db03fab8 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -545,6 +545,18 @@ class NotificationService
end
end
+ def group_was_exported(group, current_user)
+ return true unless notifiable?(current_user, :mention, group: group)
+
+ mailer.group_was_exported_email(current_user, group).deliver_later
+ end
+
+ def group_was_not_exported(group, current_user, errors)
+ return true unless notifiable?(current_user, :mention, group: group)
+
+ mailer.group_was_not_exported_email(current_user, group, errors).deliver_later
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 895c2115240..ef7bf562c69 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -17,7 +17,7 @@
%li= _('Projects')
%li= _('Runner tokens')
%li= _('SAML discovery tokens')
- %p= _('Once the exported file is ready you can download it from this page.')
+ %p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.')
- if group.export_file_exists?
= link_to _('Regenerate export'), export_group_path(group),
method: :post, class: 'btn btn-default', data: { qa_selector: 'regenerate_export_group_link' }
diff --git a/app/views/groups/settings/repository/show.html.haml b/app/views/groups/settings/repository/show.html.haml
index 1f1d7779267..ff0c9de4fef 100644
--- a/app/views/groups/settings/repository/show.html.haml
+++ b/app/views/groups/settings/repository/show.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title _('Repository Settings')
- page_title _('Repository')
-- deploy_token_description = s_('DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group.')
+- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
diff --git a/app/views/notify/group_was_exported_email.html.haml b/app/views/notify/group_was_exported_email.html.haml
new file mode 100644
index 00000000000..a2f34537662
--- /dev/null
+++ b/app/views/notify/group_was_exported_email.html.haml
@@ -0,0 +1,9 @@
+%p
+ = _('Group %{group_name} was exported successfully.') % { group_name: @group.name }
+
+%p
+ = _('The group export can be downloaded from:')
+ = link_to download_export_group_url(@group), rel: 'nofollow', download: '' do
+ #{@group.full_name} export
+%p
+ = _('The download link will expire in 24 hours.')
diff --git a/app/views/notify/group_was_exported_email.text.erb b/app/views/notify/group_was_exported_email.text.erb
new file mode 100644
index 00000000000..02571459af0
--- /dev/null
+++ b/app/views/notify/group_was_exported_email.text.erb
@@ -0,0 +1,6 @@
+<%= _('Group %{group_name} was exported successfully.') % { group_name: @group.name } %>
+
+<%= _('The group export can be downloaded from:') %>
+<%= download_export_group_url(@group) %>
+
+<%= _('The download link will expire in 24 hours.') %>
diff --git a/app/views/notify/group_was_not_exported_email.html.haml b/app/views/notify/group_was_not_exported_email.html.haml
new file mode 100644
index 00000000000..58fc34d41a3
--- /dev/null
+++ b/app/views/notify/group_was_not_exported_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ = _("Group %{group_name} couldn't be exported.") % { group_name: @group.name }
+
+%p
+ = _('The errors we encountered were:')
+
+ %ul
+ - @errors.each do |error|
+ %li
+ #{error}
diff --git a/app/views/notify/group_was_not_exported_email.text.erb b/app/views/notify/group_was_not_exported_email.text.erb
new file mode 100644
index 00000000000..92bd79b7b85
--- /dev/null
+++ b/app/views/notify/group_was_not_exported_email.text.erb
@@ -0,0 +1,7 @@
+<%= _("Group %{group_name} couldn't be exported.") % { group_name: @group.name } %>
+
+<%= _('The errors we encountered were:') %>
+
+<% @errors.each do |error| -%>
+ - <%= error %>
+<% end -%>
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 0373e37818d..760d81136c6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -22,13 +22,13 @@
.content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels'
- - if @prioritized_labels.present?
+ - if @prioritized_labels.any?
= render partial: 'shared/label', collection: @prioritized_labels, as: :label, locals: { force_priority: true, subject: @project }
- elsif search.present?
.nothing-here-block
= _('No prioritized labels with such name or description')
- - if @labels.present?
+ - if @labels.any?
.other-labels
%h5{ class: ('hide' if hide) }= _('Other Labels')
.content-list.manage-labels-list.js-other-labels
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 193053c8c97..24fc137fd29 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("Repository Settings")
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
-- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to your repository and registry images.')
+- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to packages, your repository, and registry images.')
= render "projects/default_branch/show"
= render_if_exists "projects/push_rules/index"
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 5751ed9cb7a..512644518fa 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -35,5 +35,15 @@
= label_tag ("deploy_token_write_registry"), 'write_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to the registry images')
+ %fieldset.form-group.form-check
+ = f.check_box :read_package_registry, class: 'form-check-input'
+ = label_tag ("deploy_token_read_package_registry"), 'read_package_registry', class: 'label-bold form-check-label'
+ .text-secondary= s_('DeployTokens|Allows read access to the package registry')
+
+ %fieldset.form-group.form-check
+ = f.check_box :write_package_registry, class: 'form-check-input'
+ = label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label'
+ .text-secondary= s_('DeployTokens|Allows write access to the package registry')
+
.prepend-top-default
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success qa-create-deploy-token'
diff --git a/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml b/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml
new file mode 100644
index 00000000000..ea968288f28
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml
@@ -0,0 +1,5 @@
+---
+title: Add email notification on group export complete
+merge_request: 30522
+author:
+type: added
diff --git a/changelogs/unreleased/package-deploy-tokens-fe.yml b/changelogs/unreleased/package-deploy-tokens-fe.yml
new file mode 100644
index 00000000000..9d9557cd06f
--- /dev/null
+++ b/changelogs/unreleased/package-deploy-tokens-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Add read and write package registry scopes to deploy tokens
+merge_request: 31267
+author:
+type: added
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 3d09291d9b5..b552f7d88af 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -1,17 +1,26 @@
-# Praefect: High Availability
+---
+type: reference
+---
-NOTE: **Note:** Praefect is a
-[beta](https://about.gitlab.com/handbook/product/#alpha-beta-ga) component that
-allows Gitaly to be run in a highly available configuration. While unexpected
-data loss is not likely, Praefect is not yet ready for production environments.
+# Gitaly Cluster
-[Gitaly](index.md) is the service that provides storage for Git repositories in
-the GitLab application. Praefect is an optional reverse proxy for Gitaly to
-manage multiple Gitaly nodes for high availability.
+[Gitaly](index.md), the service that provides storage for Git repositories, can
+be run in a clustered configuration to increase fault tolerance. In this
+configuration, every Git repository is stored on every Gitaly node in the
+cluster. Multiple clusters (or shards), can be configured.
-High availability is currently implemented through **asynchronous replication**.
-If a Gitaly node becomes unavailable, Praefect will automatically route traffic
-to a warm Gitaly replica.
+Praefect is a router and transaction manager for Gitaly, and a required
+component for running a Gitaly Cluster.
+
+![Architecture diagram](img/praefect_architecture_v12_10.png)
+
+Using a Gitaly Cluster increase fault tolerance by:
+
+- Replicating write operations to warm standby Gitaly nodes.
+- Detecting Gitaly node failures.
+- Automatically routing Git requests to an available Gitaly node.
+
+The availability objectives for Gitaly clusters are:
- **Recovery Point Objective (RPO):** Less than 1 minute.
@@ -35,22 +44,22 @@ The current version supports:
- Eventual consistency of the secondary replicas.
- Automatic failover from the primary to the secondary.
- Reporting of possible data loss if replication queue is non empty.
+- Marking the newly promoted primary read only if possible data loss is
+ detected.
Follow the [HA Gitaly epic](https://gitlab.com/groups/gitlab-org/-/epics/1489)
for improvements including
[horizontally distributing reads](https://gitlab.com/groups/gitlab-org/-/epics/2013).
-## Requirements for configuring Gitaly for High Availability
+## Requirements for configuring a Gitaly Cluster
-A minimum highly available configuration requires:
+The minimum recommended configuration for a Gitaly Cluster requires:
- 1 highly available load balancer
- 1 highly available PostgreSQL server (PostgreSQL 9.6 or newer)
- 3 Praefect nodes
- 3 Gitaly nodes (1 primary, 2 secondary)
-![Architecture diagram](img/praefect_architecture_v12_10.png)
-
See the [design
document](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md)
for implementation details.
diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md
index f1ebc730e5b..2e6bcabeb06 100644
--- a/doc/administration/high_availability/gitaly.md
+++ b/doc/administration/high_availability/gitaly.md
@@ -4,12 +4,8 @@ type: reference
# Configuring Gitaly for Scaled and High Availability
-Gitaly does not yet support full high availability. However, Gitaly is quite
-stable and is in use on GitLab.com. Scaled and highly available GitLab environments
-should consider using Gitaly on a separate node.
-
-See the [Gitaly HA Epic](https://gitlab.com/groups/gitlab-org/-/epics/289) to
-track plans and progress toward high availability support.
+A [Gitaly Cluster](../gitaly/praefect.md) can be used to increase the fault
+tolerance of Gitaly in high availability configurations.
This document is relevant for [scalable and highly available setups](../reference_architectures/index.md).
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index a91f3d64a4c..c2fa99e67f4 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -12,9 +12,6 @@ performance, especially for actions that read or write to Git repositories. See
[Filesystem Performance Benchmarking](../operations/filesystem_benchmarking.md)
for steps to test filesystem performance.
-NOTE: **Note:** [Cloud Object Storage service](object_storage.md) with [Gitaly](gitaly.md)
-is recommended over NFS wherever possible for improved performance.
-
## NFS Server features
### Required features
diff --git a/doc/api/deploy_tokens.md b/doc/api/deploy_tokens.md
index 461957847df..6e732a43da0 100644
--- a/doc/api/deploy_tokens.md
+++ b/doc/api/deploy_tokens.md
@@ -92,7 +92,7 @@ POST /projects/:id/deploy_tokens
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
-| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, or `write_registry`. |
+| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "My deploy token", "expires_at": "2021-01-01", "username": "custom-user", "scopes": ["read_repository"]}' "https://gitlab.example.com/api/v4/projects/5/deploy_tokens/"
@@ -193,7 +193,7 @@ POST /groups/:id/deploy_tokens
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
-| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, or `write_registry`. |
+| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request:
diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md
index c776824f433..46318a7f30d 100644
--- a/doc/topics/git/partial_clone.md
+++ b/doc/topics/git/partial_clone.md
@@ -9,6 +9,8 @@ is a performance optimization that "allows Git to function without having a
complete copy of the repository. The goal of this work is to allow Git better
handle extremely large repositories."
+Git 2.22.0 or later is required.
+
## Filter by file size
> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/2553) in GitLab 12.10.
@@ -118,7 +120,7 @@ enabled on the Git server:
many applications, each in a different subdirectory in the root. Create a file
`shiny-app/.filterspec` using the GitLab web interface:
- ```.gitignore
+ ```plaintext
# Only the paths listed in the file will be downloaded when performing a
# partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
@@ -136,7 +138,7 @@ enabled on the Git server:
shared-component-b/
```
-1. *Create a new Git repository and fetch.* Support for `--filter=sparse:oid`
+1. **Create a new Git repository and fetch.** Support for `--filter=sparse:oid`
using the clone command is incomplete, so we will emulate the clone command
by hand, using `git init` and `git fetch`. Follow
[issue tracking support for `--filter=sparse:oid`](https://gitlab.com/gitlab-org/git/issues/4)
diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md
index d50327f2a90..a8aec841072 100644
--- a/doc/user/group/settings/import_export.md
+++ b/doc/user/group/settings/import_export.md
@@ -63,8 +63,11 @@ For more details on the specific data persisted in a group export, see the
![Export group panel](img/export_panel.png)
-1. Once the export is generated, you can click **Download export** to download the [exported contents](#exported-contents)
-in a compressed tar archive, with contents in JSON format. You can also return to this page to regenerate the export data.
+1. Once the export is generated, you should receive an e-mail with a link to the [exported contents](#exported-contents)
+ in a compressed tar archive, with contents in JSON format.
+
+1. Alternatively, you can come back to the project settings and download the
+ file from there by clicking **Download export**, or generate a new file by clicking **Regenerate export**.
## Rate Limits
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 44e2d210910..2d42debed68 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -4,8 +4,9 @@
> - [Moved](https://gitlab.com/gitlab-org/gitlab/issues/199370) from **Settings > Repository** in GitLab 12.9.
> - [Added `write_registry` scope](https://gitlab.com/gitlab-org/gitlab/-/issues/22743) in GitLab 12.10.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29280) from **Settings > CI / CD** in GitLab 12.10.1.
+> - [Added package registry scopes](https://gitlab.com/gitlab-org/gitlab/-/issues/213566) from **Settings > CI / CD** in GitLab 13.0.
-Deploy tokens allow you to download (`git clone`) or push and pull the container registry images of a project without having a user and a password.
+Deploy tokens allow you to download (`git clone`) or push and pull packages and container registry images of a project without having a user and a password.
Deploy tokens can be managed by [maintainers only](../../permissions.md).
@@ -101,6 +102,22 @@ To push the container registry images, you'll need to:
Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply
push images to your Container Registry.
+### Read or pull packages
+
+To pull packages in the GitLab package registry, you'll need to:
+
+1. Create a Deploy Token with `read_package_registry` as a scope.
+1. Take note of your `username` and `token`.
+1. For the [package type of your choice](./../../packages/index.md), follow the authentication instructions for deploy tokens.
+
+### Push or upload packages
+
+To upload packages in the GitLab package registry, you'll need to:
+
+1. Create a Deploy Token with `write_package_registry` as a scope.
+1. Take note of your `username` and `token`.
+1. For the [package type of your choice](./../../packages/index.md), follow the authentication instructions for deploy tokens.
+
### Group Deploy Token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/21765) in GitLab 12.9.
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index f3a08ae970a..0fbbd96cf02 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -11,6 +11,8 @@ module API
result_hash = Hashie::Mash.new
result_hash[:read_registry] = scopes.include?('read_registry')
result_hash[:write_registry] = scopes.include?('write_registry')
+ result_hash[:read_package_registry] = scopes.include?('read_package_registry')
+ result_hash[:write_package_registry] = scopes.include?('write_package_registry')
result_hash[:read_repository] = scopes.include?('read_repository')
result_hash
end
@@ -55,7 +57,7 @@ module API
params do
requires :name, type: String, desc: "New deploy token's name"
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", or "write_registry".'
+ desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
@@ -118,7 +120,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the deploy token'
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", or "write_registry".'
+ desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8cc3dc734a2..6aff418ae37 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7128,12 +7128,18 @@ msgstr ""
msgid "DeployTokens|Add a deploy token"
msgstr ""
+msgid "DeployTokens|Allows read access to the package registry"
+msgstr ""
+
msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
+msgid "DeployTokens|Allows write access to the package registry"
+msgstr ""
+
msgid "DeployTokens|Allows write access to the registry images"
msgstr ""
@@ -7155,13 +7161,13 @@ msgstr ""
msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "DeployTokens|Deploy tokens allow access to your repository and registry images."
+msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images."
msgstr ""
msgid "DeployTokens|Expires"
msgstr ""
-msgid "DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group."
+msgid "DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group."
msgstr ""
msgid "DeployTokens|Name"
@@ -10496,6 +10502,12 @@ msgstr ""
msgid "Group"
msgstr ""
+msgid "Group %{group_name} couldn't be exported."
+msgstr ""
+
+msgid "Group %{group_name} was exported successfully."
+msgstr ""
+
msgid "Group %{group_name} was scheduled for deletion."
msgstr ""
@@ -10547,10 +10559,13 @@ msgstr ""
msgid "Group export could not be started."
msgstr ""
+msgid "Group export error"
+msgstr ""
+
msgid "Group export link has expired. Please generate a new export from your group settings."
msgstr ""
-msgid "Group export started."
+msgid "Group export started. A download link will be sent by email."
msgstr ""
msgid "Group has been already marked for deletion"
@@ -10595,6 +10610,9 @@ msgstr ""
msgid "Group variables (inherited)"
msgstr ""
+msgid "Group was exported"
+msgstr ""
+
msgid "Group was successfully updated."
msgstr ""
@@ -14492,9 +14510,6 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
-msgid "Once the exported file is ready you can download it from this page."
-msgstr ""
-
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -21027,9 +21042,15 @@ msgstr ""
msgid "The domain you entered is not allowed."
msgstr ""
+msgid "The download link will expire in 24 hours."
+msgstr ""
+
msgid "The entered user map is not a valid JSON user map."
msgstr ""
+msgid "The errors we encountered were:"
+msgstr ""
+
msgid "The file has been successfully created."
msgstr ""
@@ -21068,6 +21089,9 @@ msgstr ""
msgid "The group can be fully restored"
msgstr ""
+msgid "The group export can be downloaded from:"
+msgstr ""
+
msgid "The group has already been shared with this group"
msgstr ""
diff --git a/rubocop/cop/performance/ar_exists_and_present_blank.rb b/rubocop/cop/performance/ar_exists_and_present_blank.rb
new file mode 100644
index 00000000000..54c2d6bf95a
--- /dev/null
+++ b/rubocop/cop/performance/ar_exists_and_present_blank.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Performance
+ class ARExistsAndPresentBlank < RuboCop::Cop::Cop
+ def message_present(ivar)
+ "Avoid `#{ivar}.present?`, because it will generate database query 'Select TABLE.*' which is expensive. "\
+ "Suggest to use `#{ivar}.any?` to replace `#{ivar}.present?`"
+ end
+
+ def message_blank(ivar)
+ "Avoid `#{ivar}.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. "\
+ "Suggest to use `#{ivar}.empty?` to replace `#{ivar}.blank?`"
+ end
+
+ def_node_matcher :exists_match, <<~PATTERN
+ (send (ivar $_) :exists?)
+ PATTERN
+
+ def_node_matcher :present_match, <<~PATTERN
+ (send (ivar $_) :present?)
+ PATTERN
+
+ def_node_matcher :blank_match, <<~PATTERN
+ (send (ivar $_) :blank?)
+ PATTERN
+
+ def file_name(node)
+ node.location.expression.source_buffer.name
+ end
+
+ def in_haml_file?(node)
+ file_name(node).end_with?('.haml.rb')
+ end
+
+ def on_send(node)
+ return unless in_haml_file?(node)
+
+ ivar_present = present_match(node)
+ ivar_blank = blank_match(node)
+ return unless ivar_present || ivar_blank
+
+ node.each_ancestor(:begin) do |begin_node|
+ begin_node.each_descendant do |n|
+ ivar_exists = exists_match(n)
+ next unless ivar_exists
+
+ add_offense(node, location: :expression, message: message_present(ivar_exists)) if ivar_exists == ivar_present
+ add_offense(node, location: :expression, message: message_blank(ivar_exists)) if ivar_exists == ivar_blank
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
index 5234be0bf07..2a9d66af7df 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -110,10 +110,6 @@ describe('AlertDetails', () => {
});
});
- it('renders a status dropdown containing three items', () => {
- expect(wrapper.findAll('[data-testid="statusDropdownItem"]').length).toBe(3);
- });
-
describe('Create issue from alert', () => {
describe('createIssueFromAlertEnabled feature flag enabled', () => {
it('should display a button that links to new issue page', () => {
diff --git a/spec/mailers/emails/groups_spec.rb b/spec/mailers/emails/groups_spec.rb
new file mode 100644
index 00000000000..b4746e120e0
--- /dev/null
+++ b/spec/mailers/emails/groups_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Groups do
+ include EmailSpec::Matchers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ describe '#group_was_exported_email' do
+ subject { Notify.group_was_exported_email(user, group) }
+
+ it 'sends success email' do
+ expect(subject).to have_subject "#{group.name} | Group was exported"
+ expect(subject).to have_body_text 'The download link will expire in 24 hours.'
+ expect(subject).to have_body_text "groups/#{group.path}/-/download_export"
+ end
+ end
+
+ describe '#group_was_not_exported_email' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:error) { Gitlab::ImportExport::Error.new('Error!') }
+
+ before do
+ shared.error(error)
+ end
+
+ subject { Notify.group_was_not_exported_email(user, group, shared.errors) }
+
+ it 'sends failure email' do
+ expect(subject).to have_subject "#{group.name} | Group export error"
+ expect(subject).to have_body_text "Group #{group.name} couldn't be exported."
+ end
+ end
+end
diff --git a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
new file mode 100644
index 00000000000..ce4fdac56b0
--- /dev/null
+++ b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/performance/ar_exists_and_present_blank.rb'
+
+describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ context 'when it is not haml file' do
+ it 'does not flag it as an offense' do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+
+ expect_no_offenses <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.present?
+ SOURCE
+ end
+ end
+
+ context 'when it is haml file' do
+ before do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ end
+
+ context 'the same object uses exists? and present?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.present?
+ ^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ context 'the same object uses exists? and blank?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.blank?
+ ^^^^^^^^^^^^^ Avoid `@users.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.empty?` to replace `@users.blank?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ context 'the same object uses exists?, blank? and present?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.blank?
+ ^^^^^^^^^^^^^ Avoid `@users.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.empty?` to replace `@users.blank?`
+ show @users if @users.present?
+ ^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank', 'Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ RSpec.shared_examples 'different object uses exists? and present?/blank?' do |another_method|
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ return unless @users.exists?
+ present @emails if @emails.#{another_method}
+ SOURCE
+ end
+ end
+
+ it_behaves_like 'different object uses exists? and present?/blank?', 'present?'
+ it_behaves_like 'different object uses exists? and present?/blank?', 'blank?'
+
+ RSpec.shared_examples 'Only using one present?/blank? without exists?' do |non_exists_method|
+ it 'does not flag it as an offense' do
+ expect_no_offenses "@users.#{non_exists_method}"
+ end
+ end
+
+ it_behaves_like 'Only using one present?/blank? without exists?', 'present?'
+ it_behaves_like 'Only using one present?/blank? without exists?', 'blank?'
+
+ context 'when using many present?/empty? without exists?' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ @user.present?
+ @user.blank?
+ @user.present?
+ @user.blank?
+ SOURCE
+ end
+ end
+
+ context 'when just using exists? without present?/blank?' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.exists?'
+
+ expect_no_offenses <<~SOURCE
+ @users.exists?
+ @users.some_other_method?
+ @users.exists?
+ SOURCE
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index b78e2f0bc40..7bad68b4e00 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -71,6 +71,14 @@ describe Groups::ImportExport::ExportService do
service.execute
end
+ it 'notifies the user' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:group_was_exported)
+ end
+
+ service.execute
+ end
+
context 'when saver succeeds' do
it 'saves the group in the file system' do
service.execute
@@ -114,16 +122,26 @@ describe Groups::ImportExport::ExportService do
context 'when export fails' do
context 'when file saver fails' do
- it 'removes the remaining exported data' do
+ before do
allow_next_instance_of(Gitlab::ImportExport::Saver) do |saver|
allow(saver).to receive(:save).and_return(false)
end
+ end
+ it 'removes the remaining exported data' do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
expect(File.exist?(shared.archive_path)).to eq(false)
end
+
+ it 'notifies the user about failed group export' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:group_was_not_exported)
+ end
+
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+ end
end
context 'when file compression fails' do