diff options
67 files changed, 982 insertions, 446 deletions
diff --git a/INSTALLATION_TYPE b/INSTALLATION_TYPE new file mode 100644 index 00000000000..5a18cd2fbf6 --- /dev/null +++ b/INSTALLATION_TYPE @@ -0,0 +1 @@ +source diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 5648cb9a888..d33e3a37580 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,7 +1,12 @@ import $ from 'jquery'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { __ } from './locale'; + +const tooltipTitles = { + group: __('Unsubscribe at group level'), + project: __('Unsubscribe at project level'), +}; export default class GroupLabelSubscription { constructor(container) { @@ -35,6 +40,7 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); axios.post(url) + .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); } @@ -44,4 +50,14 @@ export default class GroupLabelSubscription { this.$subscribeButtons.toggleClass('hidden'); this.$unsubscribeButtons.toggleClass('hidden'); } + + static setNewTooltip($button) { + if (!$button.hasClass('js-subscribe-button')) return; + + const type = $button.hasClass('js-group-level') ? 'group' : 'project'; + const newTitle = tooltipTitles[type]; + + $('.js-unsubscribe-button', $button.closest('.label-actions-list')) + .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 8c3de6e4045..8b01024b7d4 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -13,6 +13,7 @@ export default class LabelManager { this.otherLabels = otherLabels || $('.js-other-labels'); this.errorMessage = 'Unable to update label prioritization at this time'; this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.$badgeItemTemplate = $('#js-badge-item-template'); this.sortable = Sortable.create(this.prioritizedLabels.get(0), { filter: '.empty-message', forceFallback: true, @@ -63,7 +64,11 @@ export default class LabelManager { $target = this.otherLabels; $from = this.prioritizedLabels; } - $label.detach().appendTo($target); + + const $detachedLabel = $label.detach(); + this.toggleLabelPriorityBadge($detachedLabel, action); + $detachedLabel.appendTo($target); + if ($from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } @@ -88,6 +93,14 @@ export default class LabelManager { } } + toggleLabelPriorityBadge($label, action) { + if (action === 'remove') { + $('.js-priority-badge', $label).remove(); + } else { + $('.label-links', $label).append(this.$badgeItemTemplate.clone().html()); + } + } + onPrioritySortUpdate() { this.savePrioritySort() .catch(() => flash(this.errorMessage)); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index dd17544b656..72b72f4247d 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,9 +85,9 @@ export function redirectTo(url) { } export function webIDEUrl(route = undefined) { - let returnUrl = `${gon.relative_url_root}/-/ide/`; + let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { - returnUrl += `project${route}`; + returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } return returnUrl; } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 6f06944ebb6..9049f87e037 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -3,6 +3,17 @@ import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +const tooltipTitles = { + group: { + subscribed: __('Unsubscribe at group level'), + unsubscribed: __('Subscribe at group level'), + }, + project: { + subscribed: __('Unsubscribe at project level'), + unsubscribed: __('Subscribe at project level'), + }, +}; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -15,12 +26,10 @@ export default class ProjectLabelSubscription { event.preventDefault(); const $btn = $(event.currentTarget); - const $span = $btn.find('span'); const url = $btn.attr('data-url'); const oldStatus = $btn.attr('data-status'); $btn.addClass('disabled'); - $span.toggleClass('hidden'); axios.post(url).then(() => { let newStatus; @@ -32,21 +41,28 @@ export default class ProjectLabelSubscription { [newStatus, newAction] = ['unsubscribed', 'Subscribe']; } - $span.toggleClass('hidden'); $btn.removeClass('disabled'); this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { + this.$buttons.map((i, button) => { const $button = $(button); + const originalTitle = $button.attr('data-original-title'); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle'); + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); } return button; }); }).catch(() => flash(__('There was an error subscribing to this label.'))); } + + static setNewTitle($button, originalTitle, newStatus) { + const type = /group/.test(originalTitle) ? 'group' : 'project'; + const newTitle = tooltipTitles[type][newStatus]; + + $button.attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f2ac77819d5..6fbc624dee4 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -139,6 +139,8 @@ } .nav { + flex-wrap: nowrap; + > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; @@ -158,11 +160,12 @@ } .navbar-toggler { + position: relative; right: -10px; border-radius: 0; min-width: 45px; padding: 0; - margin-right: -7px; + margin: $gl-padding-8 -7px $gl-padding-8 0; font-size: 14px; text-align: center; color: currentColor; @@ -186,6 +189,7 @@ display: -webkit-flex; display: flex; padding-right: 10px; + flex-direction: row; } li { @@ -290,6 +294,10 @@ margin: 8px; } } + + .dropdown-menu { + position: absolute; + } } .navbar-sub-nav { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e6e74d55f64..dd7374c503e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -808,3 +808,5 @@ $modal-body-height: 134px; Prometheus */ $prometheus-table-row-highlight-color: $theme-gray-100; + +$priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e178371d21f..785df23a355 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -57,69 +57,8 @@ border-bottom-left-radius: $border-radius-base; } -.label-row { - .label-name { - display: inline-block; - margin-bottom: 10px; - - @include media-breakpoint-up(sm) { - width: 200px; - margin-left: $gl-padding * 2; - margin-bottom: 0; - } - - .badge { - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - } - } - - .label-type { - display: block; - margin-bottom: 10px; - margin-left: 50px; - - @include media-breakpoint-up(sm) { - display: inline-block; - width: 100px; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .label-description { - display: block; - margin-bottom: 10px; - - .description-text { - margin-bottom: $gl-padding; - } - - a { - color: $blue-600; - } - - @include media-breakpoint-up(sm) { - display: inline-block; - max-width: 50%; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .badge { - padding: 4px $grid-size; - font-size: $label-font-size; - position: relative; - top: ($grid-size / 2); - } -} - .color-label { - padding: 0 $grid-size; + padding: $gl-padding-4 $grid-size; line-height: 16px; border-radius: $label-border-radius; color: $white-light; @@ -133,26 +72,29 @@ } .manage-labels-list { - @media(min-width: map-get($grid-breakpoints, md)) { - &.content-list li { - padding: $gl-padding 0; - } - } - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - - &:active { - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } + margin-bottom: 5px; + display: flex; + justify-content: space-between; + padding: $gl-padding; + border-radius: $border-radius-default; &.sortable-ghost { opacity: 0.3; } + + .prioritized-labels & { + box-shadow: 0 1px 2px $issue-boards-card-shadow; + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + } } .btn-action { @@ -170,27 +112,6 @@ } } } - - .dropdown { - @include media-breakpoint-up(sm) { - float: right; - } - } - - @include media-breakpoint-down(xs) { - .dropdown-menu { - min-width: 100%; - } - } -} - -.draggable-handler { - display: inline-block; - vertical-align: top; - margin: 5px 0; - opacity: 0; - transition: opacity .3s; - color: $gray-darkest; } .prioritized-labels { @@ -215,22 +136,6 @@ } } -.toggle-priority { - display: inline-block; - vertical-align: top; - - button { - border-color: transparent; - padding: 5px 8px; - vertical-align: top; - font-size: 14px; - - &:hover { - border-color: transparent; - } - } -} - .filtered-labels { font-size: 0; padding: 12px 16px; @@ -284,10 +189,8 @@ } .label-subscribe-button { - @media(min-width: map-get($grid-breakpoints, md)) { - min-width: 105px; - margin-left: $gl-padding; - } + width: 105px; + font-weight: 200; .label-subscribe-button-icon { &[disabled] { @@ -324,3 +227,95 @@ font-size: $label-font-size; } } + +.labels-container { + background-color: $gray-light; + border-radius: $border-radius-default; + padding: $gl-padding $gl-padding-8; +} + +.label-actions-list { + list-style: none; + flex-shrink: 0; + padding: 0; +} + +.label-badge { + color: $theme-gray-900; + font-weight: $gl-font-weight-normal; + padding: $gl-padding-4 $gl-padding-8; + border-radius: $border-radius-default; + font-size: $label-font-size; +} + +.label-badge-blue { + background-color: $theme-blue-100; +} + +.label-badge-gray { + background-color: $theme-gray-100; +} + +.label-links { + list-style: none; + padding: 0; + white-space: nowrap; +} + +.label-link-item { + padding: 0; +} + +.label-list-item { + .content-list &::before, + .content-list &::after { + content: none; + } + + .label-name { + width: 150px; + flex-shrink: 0; + + .label { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + } + + .label-description { + flex-grow: 1; + + a { + color: $blue-600; + } + } + + .label { + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: $gl-padding-4; + } + + .label-action { + color: $theme-gray-800; + cursor: pointer; + + svg { + fill: $theme-gray-800; + } + + &:hover { + color: $blue-600; + + svg { + fill: $blue-600; + } + } + } +} + +.priority-labels-empty-state .svg-content img { + max-width: $priority-label-empty-state-width; +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 06ef58531d7..8cdf2275551 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -15,6 +15,7 @@ color: $perf-bar-text; select { + color: $perf-bar-text; width: 200px; } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index c925b4aada5..d04eb192129 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,6 +7,19 @@ module IssuableActions before_action :authorize_admin_issuable!, only: :bulk_update end + def permitted_keys + [ + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] + ] + end + def show respond_to do |format| format.html @@ -140,24 +153,15 @@ module IssuableActions end def bulk_update_params - permitted_keys = [ - :issuable_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ] + permitted_keys_array = permitted_keys.dup if resource_name == 'issue' - permitted_keys << { assignee_ids: [] } + permitted_keys_array << { assignee_ids: [] } else - permitted_keys.unshift(:assignee_id) + permitted_keys_array.unshift(:assignee_id) end - params.require(:update).permit(permitted_keys) + params.require(:update).permit(permitted_keys_array) end def resource_name diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 58be330f466..863f50e8e66 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] + before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController format.html do @labels = @group.labels.page(params[:page]) end - format.json do - available_labels = LabelsFinder.new( - current_user, - group_id: @group.id, - only_group_labels: params[:only_group_labels], - include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute - - render json: LabelSerializer.new.represent_appearance(available_labels) + render json: LabelSerializer.new.represent_appearance(@available_labels) end end end @@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController def save_previous_label_path session[:previous_labels_path] = URI(request.referer || '').path end + + def available_labels + @available_labels ||= + LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + end end diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 4d758402850..a5c82caa897 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -42,6 +42,6 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll owner: current_user } - Applications::CreateService.new(current_user, oauth_application_params).execute + Applications::CreateService.new(current_user, oauth_application_params).execute(request) end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1b0e7a4a3e..c7df25cecef 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -211,6 +211,14 @@ module LabelsHelper end end + def label_status_tooltip(label, status) + type = label.is_a?(ProjectLabel) ? 'project' : 'group' + level = status.unsubscribed? ? type : status.sub('-level', '') + action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe' + + "#{action} at #{level} level" + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/group.rb b/app/models/group.rb index 8fb77a7869d..aeb50fb9bb7 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -141,13 +141,14 @@ class Group < Namespace ) end - def add_user(user, access_level, current_user: nil, expires_at: nil) + def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false) GroupMember.add_user( self, user, access_level, current_user: current_user, - expires_at: expires_at + expires_at: expires_at, + ldap: ldap ) end @@ -195,6 +196,10 @@ class Group < Namespace owners.include?(user) && owners.size == 1 end + def ldap_synced? + false + end + def post_create_hook Gitlab::AppLogger.info("Group \"#{name}\" was created") diff --git a/app/models/label.rb b/app/models/label.rb index de7f1d56c64..1cf04976602 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -137,6 +137,10 @@ class Label < ActiveRecord::Base priority.try(:priority) end + def priority? + priorities.present? + end + def template? template end diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index e67af929954..94a434b95dd 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -5,7 +5,7 @@ module Applications @params = params.except(:ip_address) end - def execute(request = nil) + def execute(request) Doorkeeper::Application.create(@params) end end diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index dc4dccc9e0d..c8008771236 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,6 +2,9 @@ = form_errors(@group) = render 'shared/group_form', f: f + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group + = render_if_exists 'admin/namespace_plan', f: f + .form-group.row.group-description-holder = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' .col-sm-10 @@ -15,6 +18,8 @@ = render 'groups/group_admin_settings', f: f + = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f + - if @group.new_record? .form-group.row .offset-sm-2.col-sm-10 @@ -28,3 +33,5 @@ .form-actions = f.submit 'Save changes', class: "btn btn-save" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" + += render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index e7c70a6f187..3f96988c203 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,3 +1,4 @@ +- group = local_assigns.fetch(:group) - css_class = 'no-description' if group.description.blank? %li.group-row{ class: css_class } @@ -8,6 +9,8 @@ %span.badge.badge-pill = storage_counter(group.storage_size) + = render_if_exists 'admin/namespace_plan_badge', namespace: group + %span = icon('bookmark') = number_with_delimiter(group.projects.count) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 6d75ccd5add..a40f98ad24f 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -40,6 +40,8 @@ %strong = @group.created_at.to_s(:medium) + = render_if_exists 'admin/namespace_plan_info', namespace: @group + %li %span.light Storage: %strong= storage_counter(@group.storage_size) @@ -58,6 +60,10 @@ = group_lfs_status(@group) = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + = render_if_exists 'namespaces/shared_runner_status', namespace: @group + + = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group + .card .card-header %h3.card-title @@ -104,7 +110,7 @@ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div - = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) + = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index ac7e12fcd0b..db7eaff6658 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,21 +1,32 @@ -- page_title 'Labels' - +- @no_container = true +- page_title "Labels" +- can_admin_label = can?(current_user, :admin_label, @group) +- hide_class = '' +- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] -.top-area.adjust - .nav-text - = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence } +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" + +- if @labels.exists? + #promote-label-modal + %div{ class: container_class } + .top-area.adjust + .nav-text + = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } - .nav-controls - - if can?(current_user, :admin_label, @group) - = link_to "New label", new_group_label_path(@group), class: "btn btn-new" + .labels-container.prepend-top-5 + .other-labels + - if can_admin_label + %h5{ class: ('hide' if hide) } Labels + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } + = paginate @labels, theme: 'gitlab' +- else + = render 'shared/empty_states/labels' -.labels - .other-labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - = _("No labels created yet.") +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index ca7a6d5a886..59c4eeec17a 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -15,7 +15,7 @@ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| .form-group - = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project') .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } = provider_gcp_field.hidden_field :gcp_project_id .dropdown diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 2e92524ce8f..db57da99ec7 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,27 +1,27 @@ = form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 77d7a055474..4d117f435dc 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -1,20 +1,20 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' .input-group = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off' %span.input-group-append.clipboard-addon @@ -23,7 +23,7 @@ = s_('ClusterIntegration|Show') .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 9c78bade254..fb5b0fc15c9 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,40 +1,44 @@ - @no_container = true - page_title "Labels" -- hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) +- hide_class = '' + +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" - if @labels.exists? || @prioritized_labels.exists? #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + = _('Labels can be applied to issues and merge requests.') - if can_admin_label - Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') - - if can_admin_label - .nav-controls - = link_to new_project_label_path(@project), class: "btn btn-new" do - New label - - .labels + .labels-container.prepend-top-5 - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } - %h5 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } - #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } + %h5.prepend-top-10= _('Prioritized Labels') + .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } + #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } = render 'shared/empty_states/priority_labels' - if @prioritized_labels.present? - = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } - if @labels.present? .other-labels - if can_admin_label - %h5{ class: ('hide' if hide) } Other Labels - %ul.content-list.manage-labels-list.js-other-labels + %h5{ class: ('hide' if hide) }= _('Other Labels') + .content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else = render 'shared/empty_states/labels' + +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ba5b65a209d..5eec7b02b54 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,93 +1,70 @@ - label_css_id = dom_id(label) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] +- use_label_priority = local_assigns.fetch(:use_label_priority, false) +- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) +- tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .d-inline-block.d-sm-none.dropdown - %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-right - %ul - - if show_label_merge_requests_link - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - - if show_label_issues_link - %li - = link_to_label(label, subject: subject) do - View open issues - - if current_user - %li.label-subscription - - if can_subscribe_to_label_in_different_levels?(label) - %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - %span Subscribe at project level - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - %span Subscribe at group level - - else - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } } - %span= label_subscription_toggle_button_text(label, @project) - - - if can?(current_user, :admin_label, label) - %li - = link_to 'Edit', edit_label_path(label) - %li - = link_to 'Delete', - destroy_label_path(label), - title: 'Delete', - method: :delete, - data: {confirm: 'Remove this label? Are you sure?'}, - class: 'text-danger' - - .float-right.d-none.d-sm-none.d-md-block - - if can?(current_user, :admin_label, label) - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = sprite_icon('pencil') - %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } - = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do - %span.sr-only Delete - = sprite_icon('remove') + = render "shared/label_row", label: label, subject: subject, force_priority: force_priority + %ul.label-actions-list + - if @project + %li.inline + .label-badge.label-badge-gray= label.model_name.human.capitalize + - if can?(current_user, :admin_label, @project) + %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), + dom_id: dom_id(label), type: label.type } } + %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') } + = sprite_icon('star-o') + %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') } + = sprite_icon('star') + %li.inline + = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do + = sprite_icon('pencil') + %li.inline + .dropdown + %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') } + = sprite_icon('ellipsis_v') + .dropdown-menu.dropdown-open-left + %ul + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %li + %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = _('Promote to group label') + %li + %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } + %button.text-danger.remove-row{ type: 'button' }= _('Delete') - if current_user - .label-subscription.inline + %li.inline.label-subscription - if can_subscribe_to_label_in_different_levels?(label) - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - = icon('spinner spin', class: 'label-subscribe-button-loading') - + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } + %span= _('Unsubscribe') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span Subscribe - = icon('chevron-down') - %ul.dropdown-menu - %li - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - Project level - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - Group level + %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } } + %span + = _('Subscribe') + = sprite_icon('chevron-down') + .dropdown-menu.dropdown-open-left + %ul + %li + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } + %span= _('Subscribe at project level') + %li + %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } + %span= _('Subscribe at group level') - else - %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } %span= label_subscription_toggle_button_text(label, @project) - = icon('spinner spin', class: 'label-subscribe-button-loading') = render 'shared/delete_label_modal', label: label diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index f1c1ca9b2c9..0ae3ab8f090 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,30 +1,23 @@ - subject = local_assigns[:subject] +- force_priority = local_assigns.fetch(:force_priority, false) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -%span.label-row - - if can?(current_user, :admin_label, @project) - .draggable-handler - = icon('bars') - .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), - dom_id: dom_id(label), type: label.type } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' } - = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' } - = icon('star') - %span.label-name - = link_to_label(label, subject: @project, tooltip: false) - - if defined?(@project) && @project.group.present? - %span.label-type - = label.model_name.human.titleize - - %span.label-description +.label-name + = link_to_label(label, subject: @project, tooltip: false) +.label-description + .append-right-default.prepend-left-default - if label.description.present? - .description-text + .description-text.append-bottom-10 = markdown_field(label, :description) - .d-none.d-sm-none.d-md-block + %ul.label-links - if show_label_issues_link - = link_to_label(label, subject: subject) { 'Issues' } + %li.label-link-item.inline + = link_to_label(label, subject: subject) { 'Issues' } - if show_label_merge_requests_link · - = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' } + %li.label-link-item.inline + = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') } + - if force_priority + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb index f92421a667d..0aff0c4c7c6 100644 --- a/app/workers/storage_migrator_worker.rb +++ b/app/workers/storage_migrator_worker.rb @@ -1,29 +1,8 @@ class StorageMigratorWorker include ApplicationWorker - BATCH_SIZE = 100 - def perform(start, finish) - projects = build_relation(start, finish) - - projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| - Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." - - begin - project.migrate_to_hashed_storage! - rescue => err - Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") - end - end - end - - def build_relation(start, finish) - relation = Project - table = Project.arel_table - - relation = relation.where(table[:id].gteq(start)) if start - relation = relation.where(table[:id].lteq(finish)) if finish - - relation + migrator = Gitlab::HashedStorage::Migrator.new + migrator.bulk_migrate(start, finish) end end diff --git a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml new file mode 100644 index 00000000000..fb4fbf80575 --- /dev/null +++ b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml @@ -0,0 +1,5 @@ +--- +title: Label list page redesign +merge_request: 18466 +author: +type: changed diff --git a/changelogs/unreleased/44790-disabled-emails-logging.yml b/changelogs/unreleased/44790-disabled-emails-logging.yml new file mode 100644 index 00000000000..90125dc0300 --- /dev/null +++ b/changelogs/unreleased/44790-disabled-emails-logging.yml @@ -0,0 +1,5 @@ +--- +title: Stop logging email information when emails are disabled +merge_request: 18521 +author: Marc Shaw +type: fixed diff --git a/changelogs/unreleased/46922-hashed-storage-single-project.yml b/changelogs/unreleased/46922-hashed-storage-single-project.yml new file mode 100644 index 00000000000..c293238a5a4 --- /dev/null +++ b/changelogs/unreleased/46922-hashed-storage-single-project.yml @@ -0,0 +1,5 @@ +--- +title: 'Hashed Storage: migration rake task now can be executed to specific project' +merge_request: 19268 +author: +type: changed diff --git a/changelogs/unreleased/ide-url-util-relative-url-fix.yml b/changelogs/unreleased/ide-url-util-relative-url-fix.yml new file mode 100644 index 00000000000..9f0f4a0f7be --- /dev/null +++ b/changelogs/unreleased/ide-url-util-relative-url-fix.yml @@ -0,0 +1,6 @@ +--- +title: Fixes Web IDE button on merge requests when GitLab is installed with relative + URL +merge_request: +author: +type: fixed diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb index c76a6b8b19f..e8770c8d460 100644 --- a/config/initializers/disable_email_interceptor.rb +++ b/config/initializers/disable_email_interceptor.rb @@ -1,2 +1,5 @@ # Interceptor in lib/disable_email_interceptor.rb -ActionMailer::Base.register_interceptor(DisableEmailInterceptor) unless Gitlab.config.gitlab.email_enabled +unless Gitlab.config.gitlab.email_enabled + ActionMailer::Base.register_interceptor(DisableEmailInterceptor) + ActionMailer::Base.logger = nil +end diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md index cfd601b8866..7ad38abe4f5 100644 --- a/doc/administration/raketasks/storage.md +++ b/doc/administration/raketasks/storage.md @@ -17,13 +17,21 @@ This task will schedule all your existing projects and attachments associated wi **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:migrate_to_hashed +sudo gitlab-rake gitlab:storage:migrate_to_hashed ``` **Source Installation** ```bash -rake gitlab:storage:migrate_to_hashed +sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production +``` + +They both also accept a range as environment variable: + +```bash +# to migrate any non migrated project from ID 20 to 50. +export ID_FROM=20 +export ID_TO=50 ``` You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen. @@ -44,13 +52,13 @@ To have a simple summary of projects using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:legacy_projects +sudo gitlab-rake gitlab:storage:legacy_projects ``` **Source Installation** ```bash -rake gitlab:storage:legacy_projects +sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production ``` ------ @@ -60,13 +68,13 @@ To list projects using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_legacy_projects +sudo gitlab-rake gitlab:storage:list_legacy_projects ``` **Source Installation** ```bash -rake gitlab:storage:list_legacy_projects +sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production ``` @@ -77,13 +85,13 @@ To have a simple summary of projects using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:hashed_projects +sudo gitlab-rake gitlab:storage:hashed_projects ``` **Source Installation** ```bash -rake gitlab:storage:hashed_projects +sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production ``` ------ @@ -93,14 +101,13 @@ To list projects using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_hashed_projects +sudo gitlab-rake gitlab:storage:list_hashed_projects ``` **Source Installation** ```bash -rake gitlab:storage:list_hashed_projects - +sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production ``` ## List attachments on Legacy storage @@ -110,13 +117,13 @@ To have a simple summary of project attachments using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:legacy_attachments +sudo gitlab-rake gitlab:storage:legacy_attachments ``` **Source Installation** ```bash -rake gitlab:storage:legacy_attachments +sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production ``` ------ @@ -126,13 +133,13 @@ To list project attachments using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_legacy_attachments +sudo gitlab-rake gitlab:storage:list_legacy_attachments ``` **Source Installation** ```bash -rake gitlab:storage:list_legacy_attachments +sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production ``` ## List attachments on Hashed storage @@ -142,13 +149,13 @@ To have a simple summary of project attachments using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:hashed_attachments +sudo gitlab-rake gitlab:storage:hashed_attachments ``` **Source Installation** ```bash -rake gitlab:storage:hashed_attachments +sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production ``` ------ @@ -158,13 +165,13 @@ To list project attachments using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_hashed_attachments +sudo gitlab-rake gitlab:storage:list_hashed_attachments ``` **Source Installation** ```bash -rake gitlab:storage:list_hashed_attachments +sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production ``` [storage-types]: ../repository_storage_types.md diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 36fd8affa5b..8ea2e0a81dc 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -114,7 +114,7 @@ Let's now see how that information is exposed within GitLab. ## Viewing the current status of an environment -The environment list under your project's **Pipelines âž” Environments**, is +The environment list under your project's **Operations > Environments**, is where you can find information of the last deployment status of an environment. Here's how the Environments page looks so far. @@ -167,7 +167,7 @@ that works. You can't control everything, so sometimes things go wrong. When that unfortunate time comes GitLab has you covered. Simply by clicking the **Rollback** button that can be found in the deployments page -(**Pipelines âž” Environments âž” `environment name`**) you can relaunch the +(**Operations > Environments > `environment name`**) you can relaunch the job with the commit associated with it. >**Note:** diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index de60cd27cd1..aa31e172641 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -19,7 +19,9 @@ There's also a collection of repositories with [example projects](https://gitlab - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) - **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) -- **Java**: [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) +- **Java**: + - [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md) + - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) - **Scala**: [Test a Scala application](test-scala-application.md) - **Clojure**: [Test a Clojure application](test-clojure-application.md) - **Elixir**: diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png Binary files differnew file mode 100644 index 00000000000..5b5d91ec07a --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png Binary files differnew file mode 100644 index 00000000000..f3761632556 --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md new file mode 100644 index 00000000000..b88761be56b --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -0,0 +1,142 @@ +--- +author: Dylan Griffith +author_gitlab: DylanGriffith +level: intermediary +article_type: tutorial +date: 2018-06-07 +description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD" +--- + +# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD + +## Introduction + +In this article, we'll demonstrate how to deploy a [Spring +Boot](https://projects.spring.io/spring-boot/) application to [Cloud +Foundry (CF)](https://www.cloudfoundry.org/) with GitLab CI/CD using the [Continuous +Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment) +method. + +All the code for this project can be found in this [GitLab +repo](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo). + +In case you're interested in deploying Spring Boot applications to Kubernetes +using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/). + +## Requirements + +_We assume you are familiar with Java, GitLab, Cloud Foundry, and GitLab CI/CD._ + +To follow along with this tutorial you will need the following: + +- An account on [Pivotal Web Services (PWS)](https://run.pivotal.io/) or any + other Cloud Foundry instance +- An account on GitLab + +NOTE: **Note:** +You will need to replace the `api.run.pivotal.io` URL in the all below +commands with the [API +URL](https://docs.cloudfoundry.org/running/cf-api-endpoint.html) of your CF +instance if you're not deploying to PWS. + +## Create your project + +To create your Spring Boot application you can use the Spring template in +GitLab when creating a new project: + +![New Project From Template](img/create_from_template.png) + +## Configure the deployment to Cloud Foundry + +To deploy to Cloud Foundry we need to add a `manifest.yml` file. This +is the configuration for the CF CLI we will use to deploy the application. We +will create this in the root directory of our project with the following +content: + +```yaml +--- +applications: +- name: gitlab-hello-world + random-route: true + memory: 1G + path: target/demo-0.0.1-SNAPSHOT.jar +``` + +## Configure GitLab CI/CD to deploy your application + +Now we need to add the the GitLab CI/CD configuration file +([`.gitlab-ci.yml`](../../yaml/README.md)) to our +project's root. This is how GitLab figures out what commands need to be run whenever +code is pushed to our repository. We will add the following `.gitlab-ci.yml` +file to the root directory of the repository, GitLab will detect it +automatically and run the steps defined once we push our code: + +```yaml +image: java:8 + +stages: + - build + - deploy + +build: + stage: build + script: ./mvnw package + artifacts: + paths: + - target/demo-0.0.1-SNAPSHOT.jar + +production: + stage: deploy + script: + - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx + - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io + - ./cf push + only: + - master +``` + +We've used the `java:8` [docker +image](../../docker/using_docker_images.md) to build +our application as it provides the up-to-date Java 8 JDK on [Docker +Hub](https://hub.docker.com/). We've also added the [`only` +clause](../../yaml/README.md#only-and-except-simplified) +to ensure our deployments only happen when we push to the master branch. + +Now, since the steps defined in `.gitlab-ci.yml` require credentials to login +to CF, you'll need to add your CF credentials as [environment +variables](../../variables/README.md#predefined-variables-environment-variables) +on GitLab CI/CD. To set the environment variables, navigate to your project's +**Settings > CI/CD** and expand **Secret Variables**. Name the variables +`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values. + +![Secret Variable Settings in GitLab](img/cloud_foundry_secret_variables.png) + +Once set up, GitLab CI/CD will deploy your app to CF at every push to your +repository's deafult branch. To see the build logs or watch your builds running +live, navigate to **CI/CD > Pipelines**. + +CAUTION: **Caution:** +It is considered best practice for security to create a separate deploy +user for your application and add its credentials to GitLab instead of using +a developer's credentials. + +To start a manual deployment in GitLab go to **CI/CD > Pipelines** then click +on **Run Pipeline**. Once the app is finished deploying it will display the URL +of your application in the logs for the `production` job like: + +```shell +requested state: started +instances: 1/1 +usage: 1G x 1 instances +urls: gitlab-hello-world-undissembling-hotchpot.cfapps.io +last uploaded: Mon Nov 6 10:02:25 UTC 2017 +stack: cflinuxfs2 +buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1... + + state since cpu memory disk details +#0 running 2017-11-06 09:03:22 PM 120.4% 291.9M of 1G 137.6M of 1G +``` + +You can then visit your deployed application (for this example, +https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/) and you should +see the "Spring is here!" message. diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png Binary files differnew file mode 100644 index 00000000000..b6f8864796f --- /dev/null +++ b/doc/topics/autodevops/img/autodevops_domain_variables.png diff --git a/doc/topics/autodevops/img/autodevops_multiple_clusters.png b/doc/topics/autodevops/img/autodevops_multiple_clusters.png Binary files differnew file mode 100644 index 00000000000..f4d101ca921 --- /dev/null +++ b/doc/topics/autodevops/img/autodevops_multiple_clusters.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 478b9c9c0c6..4bcc7fc6512 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -41,6 +41,7 @@ project in an easy and automatic way: 1. [Auto Code Quality](#auto-code-quality) 1. [Auto SAST (Static Application Security Testing)](#auto-sast) 1. [Auto Dependency Scanning](#auto-dependency-scanning) +1. [Auto License Management](#auto-license-management) 1. [Auto Container Scanning](#auto-container-scanning) 1. [Auto Review Apps](#auto-review-apps) 1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) @@ -62,7 +63,7 @@ Auto DevOps provides great defaults for all the stages; you can, however, For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/). -## Prerequisites +## Requirements TIP: **Tip:** For self-hosted installations, the easiest way to make use of Auto DevOps is to @@ -112,25 +113,26 @@ NOTE: **Note:** If you do not have Kubernetes or Prometheus installed, then Auto Review Apps, Auto Deploy, and Auto Monitoring will be silently skipped. -### Auto DevOps base domain +## Auto DevOps base domain The Auto DevOps base domain is required if you want to make use of [Auto -Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined -either under the project's CI/CD settings while -[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in -the CI/CD section. -It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. +Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined +in three places: -A wildcard DNS A record matching the base domain is required, for example, +- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) +- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section +- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters)) + +A wildcard DNS A record matching the base domain(s) is required, for example, given a base domain of `example.com`, you'd need a DNS entry like: ``` *.example.com 3600 A 1.2.3.4 ``` -where `example.com` is the domain name under which the deployed apps will be served, +In this case, `example.com` is the domain name under which the deployed apps will be served, and `1.2.3.4` is the IP address of your load balancer; generally NGINX -([see prerequisites](#prerequisites)). How to set up the DNS record is beyond +([see requirements](#requirements)). How to set up the DNS record is beyond the scope of this document; you should check with your DNS provider. Alternatively you can use free public services like [xip.io](http://xip.io) or @@ -146,6 +148,56 @@ If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two options: provide a static IP, or have one assigned. For more information see the relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites). +## Using multiple Kubernetes clusters **[PREMIUM]** + +When using Auto DevOps, you may want to deploy different environments to +different Kubernetes clusters. This is possible due to the 1:1 connection that +[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters). + +In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml) +(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know: + +- `review/` (every environment starting with `review/`) +- `staging` +- `production` + +Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so +except for the environment scope, they would also need to have a different +domain they would be deployed to. This is why you need to define a separate +`AUTO_DEVOPS_DOMAIN` variable for all the above +[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables). + +The following table is an example of how the three different clusters would +be configured. + +| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes | +| ------------ | -------------- | ----------------------------- | ------------- | ------ | +| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. | +| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). | +| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). | + +To add a different cluster for each environment: + +1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters + with their respective environment scope as described from the table above. + + ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png) + +1. After the clusters are created, navigate to each one and install Helm Tiller + and Ingress. +1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the + specified Auto DevOps domains. +1. Navigate to your project's **Settings > CI/CD > Variables** and add + the `AUTO_DEVOPS_DOMAIN` variables with their respective environment + scope. + + ![Auto DevOps domain variables](img/autodevops_domain_variables.png) + +Now that all is configured, you can test your setup by creating a merge request +and verifying that your app is deployed as a review app in the Kubernetes +cluster with the `review/*` environment scope. Similarly, you can check the +other environments. + ## Quick start If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) @@ -154,13 +206,13 @@ Google Cloud. ## Enabling Auto DevOps -If you haven't done already, read the [prerequisites](#prerequisites) to make +If you haven't done already, read the [requirements](#requirements) to make full use of Auto DevOps. If this is your fist time, we recommend you follow the -[quick start guide](#quick-start). +[quick start guide](quick_start_guide.md). To enable Auto DevOps to your project: -1. Check that your project doesn't have a `.gitlab-ci.yml`, and remove it otherwise +1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise 1. Go to your project's **Settings > CI/CD > General pipelines settings** and find the Auto DevOps section 1. Select "Enable Auto DevOps" @@ -230,7 +282,7 @@ In GitLab Starter, differences between the source and target branches are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). -### Auto SAST +### Auto SAST **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.3. @@ -241,9 +293,9 @@ report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html). -### Auto Dependency Scanning +### Auto Dependency Scanning **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.7. @@ -254,7 +306,20 @@ report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html). + +### Auto License Management **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 11.0. + +License Management uses the +[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management) +to search the project dependencies for their license. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any licenses are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html). ### Auto Container Scanning @@ -267,13 +332,13 @@ created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html). ### Auto Review Apps NOTE: **Note:** This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [prerequisites](#prerequisites) are not met, the job will +available. If the [requirements](#requirements) are not met, the job will silently be skipped. CAUTION: **Caution:** @@ -295,7 +360,7 @@ up in the merge request widget for easy discovery. When the branch is deleted, for example after the merge request is merged, the Review App will automatically be deleted. -### Auto DAST +### Auto DAST **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.4. @@ -306,9 +371,9 @@ issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html). -### Auto Browser Performance Testing +### Auto Browser Performance Testing **[PREMIUM]** > Introduced in [GitLab Premium][ee] 10.4. @@ -320,13 +385,14 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h /direction ``` -In GitLab Premium, performance differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html). +In GitLab Premium, performance differences between the source +and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). ### Auto Deploy NOTE: **Note:** This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [prerequisites](#prerequisites) are not met, the job will +available. If the [requirements](#requirements) are not met, the job will silently be skipped. CAUTION: **Caution:** @@ -363,7 +429,7 @@ executed somewhere else, it cannot be accessed again. ### Auto Monitoring NOTE: **Note:** -Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage +Check the [requirements](#requirements) for Auto Monitoring to make this stage work. Once your application is deployed, Auto Monitoring makes it possible to monitor @@ -437,7 +503,7 @@ repo or by specifying a project variable: file in it, Auto DevOps will detect the chart and use it instead of the [default one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). This can be a great way to control exactly how your application is deployed. -- **Project variable** - Create a [variable](../../ci/variables/README.md#variables) +- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables) `AUTO_DEVOPS_CHART` with the URL of a custom chart to use. ### Customizing `.gitlab-ci.yml` @@ -493,22 +559,23 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | | `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | +| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| +| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| | `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | | `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | | `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | | `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. | -| `CODE_QUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `code_quality` job. If the variable is present, the job will not be created. | -| `LICENSE_MANAGEMENT_DISABLED` | From GitLab 11.0, this variable can be used to disable the `license_management` job. If the variable is present, the job will not be created. | +| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. | | `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. | | `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. | -| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `container_scanning` job. If the variable is present, the job will not be created. | +| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. | | `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. | | `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. | | `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. | TIP: **Tip:** Set up the replica variables using a -[project variable](../../ci/variables/README.md#variables) +[project variable](../../ci/variables/README.md#secret-variables) and scale your application by just redeploying it! CAUTION: **Caution:** @@ -583,7 +650,7 @@ staging environment and deploy to production manually. For this scenario, the `STAGING_ENABLED` environment variable was introduced. If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to -`1` as a variable), then the application will be automatically deployed +`1` as a secret variable), then the application will be automatically deployed to a `staging` environment, and a `production_manual` job will be created for you when you're ready to manually deploy to production. @@ -596,7 +663,7 @@ A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployment before any changes are deployed to production. If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to -`1` as a variable) then two manual jobs will be created: +`1` as a secret variable) then two manual jobs will be created: - `canary` which will deploy the application to the canary environment - `production_manual` which is to be used by you when you're ready to manually @@ -612,7 +679,7 @@ This will allow you to first check how the app is behaving, and later manually increasing the rollout up to 100%. If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set -`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a variable), then instead of the +`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the standard `production` job, 4 different [manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) will be created: diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 0c1cd113686..d054561d5f3 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -60,6 +60,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md). | Setting | GitLab.com | Default | | ----------- | ----------------- | ------------- | | Artifacts maximum size | 1G | 100M | +| Artifacts [expiry time](../../ci/yaml/README.md#artifacts-expire_in) | kept forever | deleted after 30 days unless otherwise specified | ## Repository size limit diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md index bd4d15dddcb..2d8fdf0d1da 100644 --- a/doc/user/project/clusters/eks_and_gitlab/index.md +++ b/doc/user/project/clusters/eks_and_gitlab/index.md @@ -38,7 +38,7 @@ Give the project a name, and then select `Create project`. ## Connecting the EKS cluster -From the left side bar, hover over `CI/CD` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`. +From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`. A few details from the EKS cluster will be required to connect it to GitLab. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index e44ac6a3d01..1e909e9f5f7 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -39,7 +39,7 @@ Before proceeding, make sure the following requirements are met: If all of the above requirements are met, you can proceed to create and add a new Kubernetes cluster that will be hosted on GKE to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Navigate to your project's **Operations > Kubernetes** page. 1. Click on **Add Kubernetes cluster**. 1. Click on **Create with GKE**. 1. Connect your Google account if you haven't done already by clicking the @@ -70,7 +70,7 @@ You need Maintainer [permissions] and above to access the Kubernetes page. To add an existing Kubernetes cluster to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Navigate to your project's **Operations > Kubernetes** page. 1. Click on **Add Kubernetes cluster**. 1. Click on **Add an existing Kubernetes cluster** and fill in the details: - **Kubernetes cluster name** (required) - The name you wish to give the cluster. diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index cad85881c4d..fcd6192e82f 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -29,7 +29,9 @@ The following aspects of a project are imported: * Regular issue and pull request comments References to pull requests and issues are preserved (GitLab.com & 8.7+), and -each imported repository defaults to `private` but [can be made public](../settings/index.md#sharing-and-permissions), as needed. +each imported repository maintains visibility level unless that [visibility +level is restricted](../../../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects), +in which case it defaults to the default project visibility. ## How it works diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index fa7e504c4aa..f687027e8c8 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -30,7 +30,7 @@ GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cl Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click. -1. Go to the `CI/CD > Kubernetes` page, to view your connected clusters +1. Go to the `Operations > Kubernetes` page, to view your connected clusters 1. Select the cluster you would like to deploy Prometheus to 1. Click the **Install** button to deploy Prometheus to the cluster diff --git a/lib/backup/files.rb b/lib/backup/files.rb index d769a3ee7b0..e287aa1e392 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -29,10 +29,10 @@ module Backup raise Backup::Error, 'Backup failed' end - run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) FileUtils.rm_rf(@backup_files_dir) else - run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) end end @@ -43,7 +43,12 @@ module Backup end def tar - system(*%w[gtar --version], out: '/dev/null') ? 'gtar' : 'tar' + if system(*%w[gtar --version], out: '/dev/null') + # It looks like we can get GNU tar by running 'gtar' + 'gtar' + else + 'tar' + end end def backup_existing_files_dir diff --git a/lib/gitlab.rb b/lib/gitlab.rb index a129746e2c6..b9a148f35bf 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -33,6 +33,7 @@ module Gitlab APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} VERSION = File.read(root.join("VERSION")).strip.freeze + INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb index 320b2ad007b..b230289e7bf 100644 --- a/lib/gitlab/git/lfs_changes.rb +++ b/lib/gitlab/git/lfs_changes.rb @@ -39,7 +39,11 @@ module Gitlab end def git_all_pointers - params = { options: ["--filter=blob:limit=#{Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE}"], require_path: true } + params = { require_path: true } + + if Gitlab::Git.version >= Gitlab::VersionInfo.parse('2.16.0') + params[:options] = ["--filter=blob:limit=#{Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE}"] + end rev_list.all_objects(params) do |object_ids| Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids) diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb new file mode 100644 index 00000000000..9251ed654cd --- /dev/null +++ b/lib/gitlab/hashed_storage/migrator.rb @@ -0,0 +1,57 @@ +module Gitlab + module HashedStorage + # Hashed Storage Migrator + # + # This is responsible for scheduling and flagging projects + # to be migrated from Legacy to Hashed storage, either one by one or in bulk. + class Migrator + BATCH_SIZE = 100 + + # Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously + # + # @param [Object] start first project id for the range + # @param [Object] finish last project id for the range + def bulk_schedule(start, finish) + StorageMigratorWorker.perform_async(start, finish) + end + + # Start migration of projects from specified range + # + # Flagging a project to be migrated is a synchronous action, + # but the migration runs through async jobs + # + # @param [Object] start first project id for the range + # @param [Object] finish last project id for the range + def bulk_migrate(start, finish) + projects = build_relation(start, finish) + + projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| + migrate(project) + end + end + + # Flag a project to me migrated + # + # @param [Object] project that will be migrated + def migrate(project) + Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." + + project.migrate_to_hashed_storage! + rescue => err + Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") + end + + private + + def build_relation(start, finish) + relation = Project + table = Project.arel_table + + relation = relation.where(table[:id].gteq(start)) if start + relation = relation.where(table[:id].lteq(finish)) if finish + + relation + end + end + end +end diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb index 8aba42ccfce..303b05e6a9a 100644 --- a/lib/gitlab/hashed_storage/rake_helper.rb +++ b/lib/gitlab/hashed_storage/rake_helper.rb @@ -9,8 +9,20 @@ module Gitlab ENV.fetch('LIMIT', 500).to_i end + def self.range_from + ENV['ID_FROM'] + end + + def self.range_to + ENV['ID_TO'] + end + + def self.range_single_item? + !range_from.nil? && range_from == range_to + end + def self.project_id_batches(&block) - Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches + Project.with_unmigrated_storage.in_batches(of: batch_size, start: range_from, finish: range_to) do |relation| # rubocop: disable Cop/InBatches ids = relation.pluck(:id) yield ids.min, ids.max diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index 9277b57f46f..694b01b272c 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -13,15 +13,15 @@ module Gitlab # All available Themes THEMES = [ Theme.new(1, 'Indigo', 'ui-indigo'), - Theme.new(2, 'Light Indigo', 'ui-light-indigo'), - Theme.new(3, 'Blue', 'ui-blue'), - Theme.new(4, 'Light Blue', 'ui-light-blue'), + Theme.new(6, 'Light Indigo', 'ui-light-indigo'), + Theme.new(4, 'Blue', 'ui-blue'), + Theme.new(7, 'Light Blue', 'ui-light-blue'), Theme.new(5, 'Green', 'ui-green'), - Theme.new(6, 'Light Green', 'ui-light-green'), - Theme.new(7, 'Red', 'ui-red'), - Theme.new(8, 'Light Red', 'ui-light-red'), - Theme.new(9, 'Dark', 'ui-dark'), - Theme.new(10, 'Light', 'ui-light') + Theme.new(8, 'Light Green', 'ui-light-green'), + Theme.new(9, 'Red', 'ui-red'), + Theme.new(10, 'Light Red', 'ui-light-red'), + Theme.new(2, 'Dark', 'ui-dark'), + Theme.new(3, 'Light', 'ui-light') ].freeze # Convenience method to get a space-separated String of all the theme diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index e294f3c4ebc..59a222b086c 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -21,6 +21,7 @@ module Gitlab uuid: Gitlab::CurrentSettings.uuid, hostname: Gitlab.config.gitlab.host, version: Gitlab::VERSION, + installation_type: Gitlab::INSTALLATION_TYPE, active_user_count: User.active.count, recorded_at: Time.now, mattermost_enabled: Gitlab.config.mattermost.enabled, diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake index 68d6f9d7cb1..f539b1df955 100644 --- a/lib/tasks/gitlab/storage.rake +++ b/lib/tasks/gitlab/storage.rake @@ -2,9 +2,26 @@ namespace :gitlab do namespace :storage do desc 'GitLab | Storage | Migrate existing projects to Hashed Storage' task migrate_to_hashed: :environment do - legacy_projects_count = Project.with_unmigrated_storage.count + storage_migrator = Gitlab::HashedStorage::Migrator.new helper = Gitlab::HashedStorage::RakeHelper + if helper.range_single_item? + project = Project.with_unmigrated_storage.find_by(id: helper.range_from) + + unless project + puts "There are no projects requiring storage migration with ID=#{helper.range_from}" + + next + end + + puts "Enqueueing storage migration of #{project.full_path} (ID=#{project.id})..." + storage_migrator.migrate(project) + + next + end + + legacy_projects_count = Project.with_unmigrated_storage.count + if legacy_projects_count == 0 puts 'There are no projects requiring storage migration. Nothing to do!' @@ -14,7 +31,7 @@ namespace :gitlab do print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}" helper.project_id_batches do |start, finish| - StorageMigratorWorker.perform_async(start, finish) + storage_migrator.bulk_schedule(start, finish) print '.' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f63f2a89aa9..43afb140051 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1086,7 +1086,7 @@ msgstr "" msgid "ClusterIntegration|GitLab Runner" msgstr "" -msgid "ClusterIntegration|Google Cloud Platform project ID" +msgid "ClusterIntegration|Google Cloud Platform project" msgstr "" msgid "ClusterIntegration|Google Kubernetes Engine" diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index de849b3eee8..babc0079f3f 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -24,10 +24,10 @@ module QA::Page end end - def has_build?(name, status: :success, wait:) + def has_build?(name, status: :success, wait: nil) within('.pipeline-graph') do within('.ci-job-component', text: name) do - has_selector?(".ci-status-icon-#{status}", wait: wait) + has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) end end end diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index 70e8d436dcb..fafd338e448 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -36,7 +36,7 @@ feature 'Labels subscription' do within "#group_label_#{feature.id}" do expect(page).not_to have_button 'Unsubscribe' - click_link_on_dropdown('Group level') + click_link_on_dropdown('Subscribe at group level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -45,7 +45,7 @@ feature 'Labels subscription' do expect(page).to have_selector('.dropdown-group-label') - click_link_on_dropdown('Project level') + click_link_on_dropdown('Subscribe at project level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -68,7 +68,7 @@ feature 'Labels subscription' do find('.dropdown-group-label').click page.within('.dropdown-group-label') do - find('a.js-subscribe-button', text: text).click + find('.js-subscribe-button', text: text).click end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index ae8b1364ec7..359381c391c 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -102,16 +102,16 @@ feature 'Prioritize labels' do drag_to(selector: '.label-list-item', from_index: 1, to_index: 2) page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end refresh wait_for_requests page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end end diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb index f4fda6de465..efa74015c6e 100644 --- a/spec/features/projects/labels/user_removes_labels_spec.rb +++ b/spec/features/projects/labels/user_removes_labels_spec.rb @@ -17,8 +17,9 @@ describe "User removes labels" do end it "removes label" do - page.within(".labels") do + page.within(".other-labels") do page.first(".label-list-item") do + first('.js-label-options-dropdown').click first(".remove-row").click first(:link, "Delete label").click end @@ -36,17 +37,16 @@ describe "User removes labels" do end it "removes all labels" do - page.within(".labels") do - loop do - li = page.first(".label-list-item") - break unless li + loop do + li = page.first(".label-list-item") + break unless li - li.click_link("Delete") - click_link("Delete label") - end - - expect(page).to have_content("Generate a default set of labels").and have_content("New label") + li.find('.js-label-options-dropdown').click + li.click_button("Delete") + click_link("Delete label") end + + expect(page).to have_content("Generate a default set of labels").and have_content("New label") end end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 9940656fb68..363ebc88afd 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -31,7 +31,7 @@ describe PreferencesHelper do describe '#user_application_theme' do context 'with a user' do it "returns user's theme's css_class" do - stub_user(theme_id: 10) + stub_user(theme_id: 3) expect(helper.user_application_theme).to eq 'ui-light' end diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js new file mode 100644 index 00000000000..c7f4092911c --- /dev/null +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -0,0 +1,29 @@ +import { webIDEUrl } from '~/lib/utils/url_utility'; + +describe('URL utility', () => { + describe('webIDEUrl', () => { + afterEach(() => { + gon.relative_url_root = ''; + }); + + describe('without relative_url_root', () => { + it('returns IDE path with route', () => { + expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', + ); + }); + }); + + describe('with relative_url_root', () => { + beforeEach(() => { + gon.relative_url_root = '/gitlab'; + }); + + it('returns IDE path with route', () => { + expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', + ); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 9b9c9656979..3d36e46d863 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -12,6 +12,7 @@ describe('MRWidgetHeader', () => { afterEach(() => { vm.$destroy(); + gon.relative_url_root = ''; }); describe('computed', () => { @@ -145,7 +146,16 @@ describe('MRWidgetHeader', () => { const button = vm.$el.querySelector('.js-web-ide'); expect(button.textContent.trim()).toEqual('Web IDE'); - expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc'); + expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); + }); + + it('renders web ide button with relative URL', () => { + gon.relative_url_root = '/gitlab'; + + const button = vm.$el.querySelector('.js-web-ide'); + + expect(button.textContent.trim()).toEqual('Web IDE'); + expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); }); it('renders download dropdown with links', () => { diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb new file mode 100644 index 00000000000..813ae43b4d3 --- /dev/null +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Gitlab::HashedStorage::Migrator do + describe '#bulk_schedule' do + it 'schedules job to StorageMigratorWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1) + end + end + end + + describe '#bulk_migrate' do + let(:projects) { create_list(:project, 2, :legacy_storage) } + let(:ids) { projects.map(&:id) } + + it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2) + end + end + + it 'sets projects as read only' do + allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice + subject.bulk_migrate(ids.min, ids.max) + + projects.each do |project| + expect(project.reload.repository_read_only?).to be_truthy + end + end + + it 'rescues and log exceptions' do + allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) + expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error + end + + it 'delegates each project in specified range to #migrate' do + projects.each do |project| + expect(subject).to receive(:migrate).with(project) + end + + subject.bulk_migrate(ids.min, ids.max) + end + end + + describe '#migrate' do + let(:project) { create(:project, :legacy_storage, :empty_repo) } + + it 'enqueues job to ProjectMigrateHashedStorageWorker' do + Sidekiq::Testing.fake! do + expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1) + end + end + + it 'rescues and log exceptions' do + allow(project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) + + expect { subject.migrate(project) }.not_to raise_error + end + + it 'sets project as read only' do + allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async) + subject.migrate(project) + + expect(project.reload.repository_read_only?).to be_truthy + end + + it 'migrate project' do + Sidekiq::Testing.inline! do + subject.migrate(project) + end + + expect(project.reload.hashed_storage?(:attachments)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb index af2f4568017..a8213988f70 100644 --- a/spec/lib/gitlab/themes_spec.rb +++ b/spec/lib/gitlab/themes_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Themes, lib: true do css = described_class.body_classes expect(css).to include('ui-indigo') - expect(css).to include('ui-dark ') + expect(css).to include('ui-dark') expect(css).to include('ui-blue') end end @@ -14,7 +14,7 @@ describe Gitlab::Themes, lib: true do describe '.by_id' do it 'returns a Theme by its ID' do expect(described_class.by_id(1).name).to eq 'Indigo' - expect(described_class.by_id(10).name).to eq 'Light' + expect(described_class.by_id(3).name).to eq 'Light' end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a716e6f5434..22d921716aa 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -32,6 +32,7 @@ describe Gitlab::UsageData do mattermost_enabled edition version + installation_type uuid hostname signup @@ -156,6 +157,7 @@ describe Gitlab::UsageData do it "gathers license data" do expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) + expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE) expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) end diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index 35e451b2f9a..233076ad6fa 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -1,6 +1,6 @@ require 'rake_helper' -describe 'gitlab:storage:*' do +describe 'rake gitlab:storage:*' do before do Rake.application.rake_require 'tasks/gitlab/storage' @@ -44,16 +44,18 @@ describe 'gitlab:storage:*' do end describe 'gitlab:storage:migrate_to_hashed' do + let(:task) { 'gitlab:storage:migrate_to_hashed' } + context '0 legacy projects' do it 'does nothing' do expect(StorageMigratorWorker).not_to receive(:perform_async) - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end context '3 legacy projects' do - let(:projects) { create_list(:project, 3, storage_version: 0) } + let(:projects) { create_list(:project, 3, :legacy_storage) } context 'in batches of 1' do before do @@ -65,7 +67,7 @@ describe 'gitlab:storage:*' do expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id) end - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end @@ -80,23 +82,48 @@ describe 'gitlab:storage:*' do expect(StorageMigratorWorker).to receive(:perform_async).with(first, last) end - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end end + + context 'with same id in range' do + it 'displays message when project cant be found' do + stub_env('ID_FROM', 99999) + stub_env('ID_TO', 99999) + + expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stdout + end + + it 'displays a message when project exists but its already migrated' do + project = create(:project) + stub_env('ID_FROM', project.id) + stub_env('ID_TO', project.id) + + expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stdout + end + + it 'enqueues migration when project can be found' do + project = create(:project, :legacy_storage) + stub_env('ID_FROM', project.id) + stub_env('ID_TO', project.id) + + expect { run_rake_task(task) }.to output(/Enqueueing storage migration .* \(ID=#{project.id}\)/).to_stdout + end + end end describe 'gitlab:storage:legacy_projects' do it_behaves_like 'rake entities summary', 'projects', 'Legacy' do let(:task) { 'gitlab:storage:legacy_projects' } - let(:create_collection) { create_list(:project, 3, storage_version: 0) } + let(:create_collection) { create_list(:project, 3, :legacy_storage) } end end describe 'gitlab:storage:list_legacy_projects' do it_behaves_like 'rake listing entities', 'projects', 'Legacy' do let(:task) { 'gitlab:storage:list_legacy_projects' } - let(:create_collection) { create_list(:project, 3, storage_version: 0) } + let(:create_collection) { create_list(:project, 3, :legacy_storage) } end end @@ -133,7 +160,7 @@ describe 'gitlab:storage:*' do describe 'gitlab:storage:hashed_attachments' do it_behaves_like 'rake entities summary', 'attachments', 'Hashed' do let(:task) { 'gitlab:storage:hashed_attachments' } - let(:project) { create(:project, storage_version: 2) } + let(:project) { create(:project) } let(:create_collection) { create_list(:upload, 3, model: project) } end end @@ -141,7 +168,7 @@ describe 'gitlab:storage:*' do describe 'gitlab:storage:list_hashed_attachments' do it_behaves_like 'rake listing entities', 'attachments', 'Hashed' do let(:task) { 'gitlab:storage:list_hashed_attachments' } - let(:project) { create(:project, storage_version: 2) } + let(:project) { create(:project) } let(:create_collection) { create_list(:upload, 3, model: project) } end end diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb index ff625164142..815432aacce 100644 --- a/spec/workers/storage_migrator_worker_spec.rb +++ b/spec/workers/storage_migrator_worker_spec.rb @@ -2,29 +2,24 @@ require 'spec_helper' describe StorageMigratorWorker do subject(:worker) { described_class.new } - let(:projects) { create_list(:project, 2, :legacy_storage) } + let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) } + let(:ids) { projects.map(&:id) } describe '#perform' do - let(:ids) { projects.map(&:id) } + it 'delegates to MigratorService' do + expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10) - it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do - expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice - - worker.perform(ids.min, ids.max) + worker.perform(5, 10) end - it 'sets projects as read only' do - allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice - worker.perform(ids.min, ids.max) + it 'migrates projects in the specified range' do + Sidekiq::Testing.inline! do + worker.perform(ids.min, ids.max) + end projects.each do |project| - expect(project.reload.repository_read_only?).to be_truthy + expect(project.reload.hashed_storage?(:attachments)).to be_truthy end end - - it 'rescues and log exceptions' do - allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) - expect { worker.perform(ids.min, ids.max) }.not_to raise_error - end end end |