diff options
50 files changed, 1136 insertions, 161 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 91951fd8ad7..76d05362056 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.61.0 +1.62.0 diff --git a/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue new file mode 100644 index 00000000000..b649dac029a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue @@ -0,0 +1,49 @@ +<script> +import { GlToggle } from '@gitlab/ui'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +export default { + name: 'GlToggleVuex', + components: { + GlToggle, + }, + props: { + stateProperty: { + type: String, + required: true, + }, + storeModule: { + type: String, + required: false, + default: null, + }, + setAction: { + type: String, + required: false, + default() { + return `set${capitalizeFirstCharacter(this.stateProperty)}`; + }, + }, + }, + computed: { + value: { + get() { + const { state } = this.$store; + const { stateProperty, storeModule } = this; + return storeModule ? state[storeModule][stateProperty] : state[stateProperty]; + }, + set(value) { + const { stateProperty, storeModule, setAction } = this; + const action = storeModule ? `${storeModule}/${setAction}` : setAction; + this.$store.dispatch(action, { key: stateProperty, value }); + }, + }, + }, +}; +</script> + +<template> + <gl-toggle v-model="value"> + <slot v-bind="{ value }"></slot> + </gl-toggle> +</template> diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 99411641874..f2f72bea5b4 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -85,7 +85,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params[:application_setting][:import_sources]&.delete("") params[:application_setting][:restricted_visibility_levels]&.delete("") + # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204) params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] + params.delete(:domain_blacklist_raw) if params[:domain_blacklist] + params.delete(:domain_whitelist_raw) if params[:domain_whitelist] params.require(:application_setting).permit( visible_application_setting_attributes diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index ec8077d18e3..bcd771dafcf 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -35,6 +35,12 @@ class Clusters::ClustersController < Clusters::BaseController end def new + return unless Feature.enabled?(:create_eks_clusters) + + @gke_selected = params[:provider] == 'gke' + @eks_selected = params[:provider] == 'eks' + + return redirect_to @authorize_url if @gke_selected && @authorize_url && !@valid_gcp_token end # Overridding ActionController::Metal#status is NOT a good idea @@ -99,7 +105,7 @@ class Clusters::ClustersController < Clusters::BaseController validate_gcp_token user_cluster - render :new, locals: { active_tab: 'gcp' } + render :new, locals: { active_tab: 'create' } end end @@ -116,7 +122,7 @@ class Clusters::ClustersController < Clusters::BaseController validate_gcp_token gcp_cluster - render :new, locals: { active_tab: 'user' } + render :new, locals: { active_tab: 'add' } end end @@ -189,7 +195,8 @@ class Clusters::ClustersController < Clusters::BaseController end def generate_gcp_authorize_url - state = generate_session_key_redirect(clusterable.new_path.to_s) + params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {} + state = generate_session_key_redirect(clusterable.new_path(params).to_s) @authorize_url = GoogleApi::CloudPlatform::Client.new( nil, callback_google_api_auth_url, diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index e0df51590ae..c9f680a4696 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -18,10 +18,23 @@ class Projects::ServicesController < Projects::ApplicationController def update @service.attributes = service_params[:service] - if @service.save(context: :manual_change) - redirect_to(project_settings_integrations_path(@project), notice: success_message) - else - render 'edit' + saved = @service.save(context: :manual_change) + + respond_to do |format| + format.html do + if saved + redirect_to project_settings_integrations_path(@project), + notice: success_message + else + render 'edit' + end + end + + format.json do + status = saved ? :ok : :unprocessable_entity + + render json: serialize_as_json, status: status + end end end @@ -67,4 +80,10 @@ class Projects::ServicesController < Projects::ApplicationController def ensure_service_enabled render_404 unless service end + + def serialize_as_json + @service + .as_json(only: @service.json_fields) + .merge(errors: @service.errors.as_json) + end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 84021d0da56..b1a6e988a1d 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -180,8 +180,12 @@ module ApplicationSettingsHelper :default_projects_limit, :default_snippet_visibility, :disabled_oauth_sign_in_sources, + :domain_blacklist, :domain_blacklist_enabled, + # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204) :domain_blacklist_raw, + :domain_whitelist, + # TODO Remove domain_whitelist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204) :domain_whitelist_raw, :outbound_local_requests_whitelist_raw, :dsa_key_restriction, diff --git a/app/models/service.rb b/app/models/service.rb index 431c5881460..d866a51c42e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -107,6 +107,13 @@ class Service < ApplicationRecord [] end + # Expose a list of fields in the JSON endpoint. + # + # This list is used in `Service#as_json(only: json_fields)`. + def json_fields + %w(active) + end + def test_data(project, user) Gitlab::DataBuilder::Push.build_sample(project, user) end diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index d1bf0344b66..49c64b31fc7 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -25,8 +25,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated polymorphic_path([clusterable, :clusters]) end - def new_path - new_polymorphic_path([clusterable, :cluster]) + def new_path(options = {}) + new_polymorphic_path([clusterable, :cluster], options) end def create_user_clusters_path diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb index f8bbe5216f1..cce400ad2a1 100644 --- a/app/presenters/instance_clusterable_presenter.rb +++ b/app/presenters/instance_clusterable_presenter.rb @@ -18,8 +18,8 @@ class InstanceClusterablePresenter < ClusterablePresenter end override :new_path - def new_path - new_admin_cluster_path + def new_path(options = {}) + new_admin_cluster_path(options) end override :cluster_status_cluster_path diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml new file mode 100644 index 00000000000..f707c6585ec --- /dev/null +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml @@ -0,0 +1,8 @@ +- provider = local_assigns.fetch(:provider) +- logo_path = local_assigns.fetch(:logo_path) +- label = local_assigns.fetch(:label) + += link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do + = image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13' + %span + = label diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml new file mode 100644 index 00000000000..24506205243 --- /dev/null +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml @@ -0,0 +1,11 @@ +- gke_label = s_('ClusterIntegration|Google GKE') +- eks_label = s_('ClusterIntegration|Amazon EKS') +- create_cluster_label = s_('ClusterIntegration|Create cluster on') +.d-flex.flex-column + %h5 + = create_cluster_label + .d-flex + = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button', + locals: { provider: 'gke', label: gke_label, logo_path: '' } + = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button', + locals: { provider: 'eks', label: eks_label, logo_path: '' } diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml new file mode 100644 index 00000000000..ca8e9ba527a --- /dev/null +++ b/app/views/clusters/clusters/eks/_index.html.haml @@ -0,0 +1 @@ +.js-create-eks-cluster-form-container diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index 6a8af23e5e8..fb182d99ff0 100644 --- a/app/views/clusters/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -1,6 +1,8 @@ - breadcrumb_title _('Kubernetes') - page_title _('Kubernetes Cluster') -- active_tab = local_assigns.fetch(:active_tab, 'gcp') +- create_eks_enabled = Feature.enabled?(:create_eks_clusters) +- active_tab = local_assigns.fetch(:active_tab, 'create') +- link_end = '<a/>'.html_safe = javascript_include_tag 'https://apis.google.com/js/api.js' = render_gcp_signup_offer @@ -11,26 +13,36 @@ .col-md-9.js-toggle-container %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' } %span Create new Cluster on GKE %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' } %span Add existing cluster .tab-content.gitlab-tab-content - .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } - = render 'clusters/clusters/gcp/header' - - if @valid_gcp_token - = render 'clusters/clusters/gcp/form' - - elsif @authorize_url - .signin-with-google - = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) - = _('or') - = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - - else - - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') - = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } + - if create_eks_enabled + .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' } + - if @gke_selected && @valid_gcp_token + = render 'clusters/clusters/gcp/header' + = render 'clusters/clusters/gcp/form' + - elsif @eks_selected + = render 'clusters/clusters/eks/index' + - else + = render 'clusters/clusters/cloud_providers/cloud_provider_selector' + - else + .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' } + = render 'clusters/clusters/gcp/header' + - if @valid_gcp_token + = render 'clusters/clusters/gcp/form' + - elsif @authorize_url + .signin-with-google + - create_account_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral' } + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px', alt: _('Sign in with Google')), @authorize_url) + = s_('or %{link_start}create a new Google account%{link_end}').html_safe % { link_start: create_account_link, link_end: link_end } + - else + - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") } + = s_('Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end } - .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' } + .tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' } = render 'clusters/clusters/user/header' = render 'clusters/clusters/user/form' diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index bc0a89bea62..4b82eb2c5ef 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -8,7 +8,7 @@ - else - can_create_fork = current_user.can?(:create_fork) = link_to new_project_fork_path(@project), - class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do = sprite_icon('fork', { css_class: 'icon' }) %span= s_('ProjectOverview|Fork') diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index b4266937a4e..441abd57334 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -17,14 +17,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } .float-left = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) diff --git a/changelogs/unreleased/60724-watch-button.yml b/changelogs/unreleased/60724-watch-button.yml new file mode 100644 index 00000000000..f22f7708ed2 --- /dev/null +++ b/changelogs/unreleased/60724-watch-button.yml @@ -0,0 +1,5 @@ +--- +title: Fix watch button styling and notifications buttons consistency +merge_request: 32827 +author: +type: fixed diff --git a/changelogs/unreleased/api_settings.yml b/changelogs/unreleased/api_settings.yml new file mode 100644 index 00000000000..58830a5ab97 --- /dev/null +++ b/changelogs/unreleased/api_settings.yml @@ -0,0 +1,5 @@ +--- +title: Improve application settings API +merge_request: 31149 +author: Mathieu Parent +type: fixed diff --git a/changelogs/unreleased/fj-11777-lower-search-count-limits.yml b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml new file mode 100644 index 00000000000..c284bc49bfc --- /dev/null +++ b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml @@ -0,0 +1,5 @@ +--- +title: Lower search counters +merge_request: 11777 +author: +type: performance diff --git a/changelogs/unreleased/gitaly-version-v1.62.0.yml b/changelogs/unreleased/gitaly-version-v1.62.0.yml new file mode 100644 index 00000000000..6ae200bd7f4 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.62.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.62.0 +merge_request: 32608 +author: +type: changed diff --git a/changelogs/unreleased/kamil-improve-import-export.yml b/changelogs/unreleased/kamil-improve-import-export.yml new file mode 100644 index 00000000000..9d485e0df2d --- /dev/null +++ b/changelogs/unreleased/kamil-improve-import-export.yml @@ -0,0 +1,5 @@ +--- +title: Reduce N+1 when doing project export +merge_request: 32423 +author: +type: performance diff --git a/changelogs/unreleased/pl-project-service-json.yml b/changelogs/unreleased/pl-project-service-json.yml new file mode 100644 index 00000000000..a273289d871 --- /dev/null +++ b/changelogs/unreleased/pl-project-service-json.yml @@ -0,0 +1,5 @@ +--- +title: Expose update project service endpoint JSON +merge_request: 32759 +author: +type: added diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md index f643d853d10..6620989983f 100644 --- a/doc/administration/database_load_balancing.md +++ b/doc/administration/database_load_balancing.md @@ -148,9 +148,9 @@ The following options can be set: If `record_type` is set to `SRV`, GitLab will continue to use a round-robin algorithm and will ignore the `weight` and `priority` in the record. Since SRV records usually return hostnames instead of IPs, GitLab will look for the IPs of returned hostnames -in the additional section of the SRV response. If no IP is found for a hostname, Gitlab -will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA records, eventually -dropping this hostname from rotation if it can't resolve its IP. +in the additional section of the SRV response. If no IP is found for a hostname, GitLab +will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA +records, eventually dropping this hostname from rotation if it can't resolve its IP. The `interval` value specifies the _minimum_ time between checks. If the A record has a TTL greater than this value, then service discovery will honor said diff --git a/doc/api/settings.md b/doc/api/settings.md index a14b0d3632a..4ad4ebdacb6 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -210,7 +210,7 @@ are listed in the descriptions of the relevant settings. | `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). | | `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. | | `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. | -| `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. | +| `domain_blacklist` | array of strings | no | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. | | `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. | | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. | | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index f6a1b6abdbf..b202dd7e9d2 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -13,7 +13,7 @@ NOTE: **Note:** Support for MySQL was removed in GitLab 12.1. This procedure should be performed **before** installing GitLab 12.1. -[pgloader](https://pgloader.io/) 3.4.1+ is required. +[pgloader](https://pgloader.io/) 3.4.1+ is required, confirm with `pgloader -V`. You can install it directly from your distribution, for example in Debian/Ubuntu: @@ -125,6 +125,10 @@ new PostgreSQL one: create no indexes, preserve index names, no foreign keys, data only + SET MySQL PARAMETERS + net_read_timeout = '90', + net_write_timeout = '180' + ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public' ; @@ -222,6 +226,10 @@ new PostgreSQL one: create no indexes, preserve index names, no foreign keys, data only + SET MySQL PARAMETERS + net_read_timeout = '90', + net_write_timeout = '180' + ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public' ; diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 65b654d1553..e43b1ca6826 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -19,13 +19,11 @@ This namespace: - Is created once. - Has a non-configurable name. -To see a list of available applications to install: +To see a list of available applications to install. For a: -1. For a: - - [Project-level cluster](../project/clusters/index.md), - navigate to your project's **Operations > Kubernetes**. - - [Group-level cluster](../group/clusters/index.md), - navigate to your group's **Kubernetes** page. +- [Project-level cluster](../project/clusters/index.md), navigate to your project's + **Operations > Kubernetes**. +- [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes** page. Install Helm first as it's used to install other applications. @@ -61,8 +59,8 @@ can lead to confusion during deployments. ### Helm -> - Available for project-level clusters since GitLab 10.2. -> - Available for group-level clusters since GitLab 11.6. +> - Introduced in GitLab 10.2 for project-level clusters. +> - Introduced in GitLab 11.6 for group-level clusters. [Helm](https://docs.helm.sh/) is a package manager for Kubernetes and is required to install all the other applications. It is installed in its @@ -71,8 +69,7 @@ environment. ### Cert-Manager -> - Available for project-level clusters since GitLab 11.6. -> - Available for group-level clusters since GitLab 11.6. +> Introduced in GitLab 11.6 for project- and group-level clusters. [Cert-Manager](https://docs.cert-manager.io/en/latest/) is a native Kubernetes certificate management controller that helps with issuing @@ -91,8 +88,8 @@ chart was used. ### GitLab Runner -> - Available for project-level clusters since GitLab 10.6. -> - Available for group-level clusters since GitLab 11.10. +> - Introduced in GitLab 10.6 for project-level clusters. +> - Introduced in GitLab 11.10 for group-level clusters. [GitLab Runner](https://docs.gitlab.com/runner/) is the open source project that is used to run your jobs and send the results back to @@ -112,8 +109,8 @@ file. ### Ingress -> - Available for project-level clusters since GitLab 10.2. -> - Available for group-level clusters since GitLab 11.6. +> - Introduced in GitLab 10.2 for project-level clusters. +> - Introduced in GitLab 11.6 for group-level clusters. [Ingress](https://kubernetes.github.io/ingress-nginx/) can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a @@ -129,8 +126,8 @@ file. ### JupyterHub -> - Available for project-level clusters since GitLab 11.0. -> - Available for group-level clusters since GitLab 12.3. +> - Introduced in GitLab 11.0 for project-level clusters. +> - Introduced in GitLab 12.3 for group-level clusters. [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter @@ -163,7 +160,7 @@ file. #### Jupyter Git Integration -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12 for project-level clusters. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12.0 for project-level clusters. When installing JupyterHub onto your Kubernetes cluster, [JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git) is automatically provisioned and configured using the authenticated user's: @@ -190,8 +187,8 @@ You can clone repositories from the files tab in Jupyter: ### Knative -> - Available for project-level clusters since GitLab 11.5. -> - Available for group-level and instance-level clusters since GitLab 12.3. +> - Introduced in GitLab 11.5 for project-level clusters. +> - Introduced in GitLab 12.3 for group- and instance-level clusters. [Knative](https://cloud.google.com/knative) provides a platform to create, deploy, and manage serverless workloads from a Kubernetes @@ -214,8 +211,8 @@ chart is used to install this application. ### Prometheus -> - Available for project-level clusters since GitLab 10.4. -> - Available for group-level clusters since GitLab 11.11. +> - Introduced in GitLab 10.4 for project-level clusters. +> - Introduced in GitLab 11.11 for group-level clusters. [Prometheus](https://prometheus.io/docs/introduction/overview/) is an open-source monitoring and alerting system useful to supervise your @@ -255,8 +252,7 @@ chart plus the values set by ## Uninstalling applications -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in -> GitLab 11.11. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in GitLab 11.11. The applications below can be uninstalled. diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index cdb7f837158..dad53a600dc 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -6,14 +6,15 @@ your self-hosted GitLab instance. ## Overview NOTE: **Note:** -While these instructions will always work for users on GitLab.com, if you are an -administrator of a self-hosted GitLab instance, you will need to enable the -[GitHub integration][gh-import] in order for users to follow the preferred -import method described on this page. If this is not enabled, users can alternatively import their -GitHub repositories using a [personal access token](#using-a-github-token) from GitHub, -but this method will not be able to associate all user activity (such as issues and pull requests) -with matching GitLab users. As an administrator of a self-hosted GitLab instance, you can also use -the [GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from +These instructions work for users on GitLab.com, but if you are an +administrator of a self-hosted GitLab instance or if you are importing from GitHub Enterprise, +you must enable [GitHub integration][gh-import]. GitHub integration is the only method for +importing from GitHub Enterprise. If you are using GitLab.com, you can alternatively import +GitHub repositories using a [personal access token](#using-a-github-token), +but this method is not recommended because it cannot associate all user activity +(such as issues and pull requests) with matching GitLab users. +If you are an administrator of a self-hosted GitLab instance, you can also use the +[GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from GitHub without the constraints of a Sidekiq worker. The following aspects of a project are imported: @@ -76,7 +77,7 @@ User-matching attempts occur in that order, and if a user is not identified eith the user account that is performing the import. NOTE: **Note:** -If you are using a self-hosted GitLab instance, this process requires that you have configured the +If you are using a self-hosted GitLab instance or if you are importing from GitHub Enterprise, this process requires that you have configured [GitHub integration][gh-import]. 1. From the top navigation bar, click **+** and select **New project**. @@ -88,9 +89,13 @@ If you are using a self-hosted GitLab instance, this process requires that you h ### Using a GitHub token NOTE: **Note:** -For a proper author/assignee mapping for issues and pull requests, the [GitHub integration method (above)](#using-the-github-integration) -should be used instead of the personal access token. If you are using GitLab.com or a self-hosted GitLab instance with the GitHub -integration enabled, that should be the preferred method to import your repositories. Read more in the [How it works](#how-it-works) section. +Using a personal access token to import projects is not recommended. If you are a GitLab.com user, +you can use a personal access token to import your project from GitHub, but this method cannot +associate all user activity (such as issues and pull requests) with matching GitLab users. +If you are an administrator of a self-hosted GitLab instance or if you are importing from +GitHub Enterprise, you cannot use a personal access token. +The [GitHub integration method (above)](#using-the-github-integration) is recommended for all users. +Read more in the [How it works](#how-it-works) section. If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories: diff --git a/lib/api/settings.rb b/lib/api/settings.rb index dd27ebab83d..acf03051a5b 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -50,10 +50,8 @@ module API optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' - given domain_blacklist_enabled: ->(val) { val } do - requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - end - optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' @@ -74,7 +72,7 @@ module API requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." end optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' - optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project manifest], + optional :import_sources, type: Array[String], values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator], desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 13883ca7f3d..28d48ce6dfe 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -8,6 +8,7 @@ module Gitlab @included_attributes = config[:included_attributes] || {} @excluded_attributes = config[:excluded_attributes] || {} @methods = config[:methods] || {} + @preloads = config[:preloads] || {} end def find_root(model_key) @@ -29,10 +30,26 @@ module Gitlab only: @included_attributes[model_key], except: @excluded_attributes[model_key], methods: @methods[model_key], - include: resolve_model_tree(model_tree) + include: resolve_model_tree(model_tree), + preload: resolve_preloads(model_key, model_tree) }.compact end + def resolve_preloads(model_key, model_tree) + model_tree + .map { |submodel_key, submodel_tree| resolve_preload(model_key, submodel_key, submodel_tree) } + .compact + .to_h + .deep_merge(@preloads[model_key].to_h) + .presence + end + + def resolve_preload(parent_model_key, model_key, model_tree) + return if @methods[parent_model_key]&.include?(model_key) + + [model_key, resolve_preloads(model_key, model_tree)] + end + def resolve_model_tree(model_tree) return unless model_tree diff --git a/lib/gitlab/import_export/fast_hash_serializer.rb b/lib/gitlab/import_export/fast_hash_serializer.rb new file mode 100644 index 00000000000..a6ab4f3a3d9 --- /dev/null +++ b/lib/gitlab/import_export/fast_hash_serializer.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# ActiveModel::Serialization (https://github.com/rails/rails/blob/v5.0.7/activemodel/lib/active_model/serialization.rb#L184) +# is simple in that it recursively calls `as_json` on each object to +# serialize everything. However, for a model like a Project, this can +# generate a query for every single association, which can add up to tens +# of thousands of queries and lead to memory bloat. +# +# To improve this, we can do several things: + +# 1. Use the option tree in http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html +# to generate the necessary preload clauses. +# +# 2. We observe that a single project has many issues, merge requests, +# etc. Instead of serializing everything at once, which could lead to +# database timeouts and high memory usage, we take each top-level +# association and serialize the data in batches. +# +# For example, we serialize the first 100 issues and preload all of +# their associated events, notes, etc. before moving onto the next +# batch. When we're done, we serialize merge requests in the same way. +# We repeat this pattern for the remaining associations specified in +# import_export.yml. +module Gitlab + module ImportExport + class FastHashSerializer + attr_reader :subject, :tree + + BATCH_SIZE = 100 + + def initialize(subject, tree, batch_size: BATCH_SIZE) + @subject = subject + @batch_size = batch_size + @tree = tree + end + + # Serializes the subject into a Hash for the given option tree + # (e.g. Project#as_json) + def execute + simple_serialize.merge(serialize_includes) + end + + private + + def simple_serialize + subject.as_json( + tree.merge(include: nil, preloads: nil)) + end + + def serialize_includes + return {} unless includes + + includes + .map(&method(:serialize_include_definition)) + .compact + .to_h + end + + # definition: + # { labels: { includes: ... } } + def serialize_include_definition(definition) + raise ArgumentError, 'definition needs to be Hash' unless definition.is_a?(Hash) + raise ArgumentError, 'definition needs to have exactly one Hash element' unless definition.one? + + key = definition.first.first + options = definition.first.second + + record = subject.public_send(key) # rubocop: disable GitlabSecurity/PublicSend + return unless record + + serialized_record = serialize_record(key, record, options) + return unless serialized_record + + # `#as_json` always returns keys as `strings` + [key.to_s, serialized_record] + end + + def serialize_record(key, record, options) + unless record.respond_to?(:as_json) + raise "Invalid type of #{key} is #{record.class}" + end + + # no has-many relation + unless record.is_a?(ActiveRecord::Relation) + return record.as_json(options) + end + + # has-many relation + data = [] + + record.in_batches(of: @batch_size) do |batch| # rubocop:disable Cop/InBatches + batch = batch.preload(preloads[key]) if preloads&.key?(key) + data += batch.as_json(options) + end + + data + end + + def includes + tree[:include] + end + + def preloads + tree[:preload] + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 06c94beead8..511b702553e 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -231,6 +231,16 @@ methods: ci_pipelines: - :notes +preloads: + statuses: + # TODO: We cannot preload tags, as they are not part of `GenericCommitStatus` + # tags: # needed by tag_list + project: # deprecated: needed by coverage_regex of Ci::Build + merge_requests: + source_project: # needed by source_branch_sha and diff_head_sha + target_project: # needed by target_branch_sha + assignees: # needed by assigne_id that is implemented by DeprecatedAssignee + # EE specific relationships and settings to include. All of this will be merged # into the previous structures if EE is used. ee: diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index f1b3db6b208..f75f69b2c75 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -41,7 +41,13 @@ module Gitlab end def serialize_project_tree - @project.as_json(reader.project_tree) + if Feature.enabled?(:export_fast_serialize, default_enabled: true) + Gitlab::ImportExport::FastHashSerializer + .new(@project, reader.project_tree) + .execute + else + @project.as_json(reader.project_tree) + end end def reader diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index ce4c1611687..93e172299b9 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -2,7 +2,8 @@ module Gitlab class SearchResults - COUNT_LIMIT = 1001 + COUNT_LIMIT = 101 + COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+" attr_reader :current_user, :query, :per_page @@ -60,7 +61,7 @@ module Gitlab def formatted_limited_count(count) if count >= COUNT_LIMIT - "#{COUNT_LIMIT - 1}+" + COUNT_LIMIT_MESSAGE else count.to_s end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a77d70a5700..9ca3b357f77 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2530,6 +2530,9 @@ msgstr "" msgid "ClusterIntegration|Alternatively" msgstr "" +msgid "ClusterIntegration|Amazon EKS" +msgstr "" + msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later." msgstr "" @@ -2608,6 +2611,9 @@ msgstr "" msgid "ClusterIntegration|Create Kubernetes cluster" msgstr "" +msgid "ClusterIntegration|Create cluster on" +msgstr "" + msgid "ClusterIntegration|Did you know?" msgstr "" @@ -2659,6 +2665,9 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project" msgstr "" +msgid "ClusterIntegration|Google GKE" +msgstr "" + msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" @@ -2989,9 +2998,6 @@ msgstr "" msgid "ClusterIntegration|pricing" msgstr "" -msgid "ClusterIntegration|properly configured" -msgstr "" - msgid "ClusterIntegration|sign up" msgstr "" @@ -5518,7 +5524,7 @@ msgstr "" msgid "Google Takeout" msgstr "" -msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service." +msgid "Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service." msgstr "" msgid "Got it" @@ -10565,6 +10571,9 @@ msgstr "" msgid "Sign in via 2FA code" msgstr "" +msgid "Sign in with Google" +msgstr "" + msgid "Sign out" msgstr "" @@ -14180,7 +14189,7 @@ msgstr "" msgid "nounSeries|%{item}, and %{lastItem}" msgstr "" -msgid "or" +msgid "or %{link_start}create a new Google account%{link_end}" msgstr "" msgid "out of %d total test" diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index e5501535875..afc059d7561 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -73,8 +73,8 @@ describe Admin::ClustersController do end describe 'GET #new' do - def get_new - get :new + def get_new(provider: 'gke') + get :new, params: { provider: provider } end describe 'functionality for new cluster' do @@ -85,6 +85,7 @@ describe Admin::ClustersController do end before do + stub_feature_flags(create_eks_clusters: false) allow(SecureRandom).to receive(:hex).and_return(key) end @@ -94,6 +95,20 @@ describe Admin::ClustersController do expect(assigns(:authorize_url)).to include(key) expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path) end + + context 'when create_eks_clusters feature flag is enabled' do + before do + stub_feature_flags(create_eks_clusters: true) + end + + context 'when selected provider is gke and no valid gcp token exists' do + it 'redirects to gcp authorize_url' do + get_new + + expect(response).to redirect_to(assigns(:authorize_url)) + end + end + end end context 'when omniauth has not configured' do diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 09677b42887..5a3ba51d4df 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -85,8 +85,8 @@ describe Groups::ClustersController do end describe 'GET new' do - def go - get :new, params: { group_id: group } + def go(provider: 'gke') + get :new, params: { group_id: group, provider: provider } end describe 'functionality for new cluster' do @@ -97,6 +97,7 @@ describe Groups::ClustersController do end before do + stub_feature_flags(create_eks_clusters: false) allow(SecureRandom).to receive(:hex).and_return(key) end @@ -106,6 +107,20 @@ describe Groups::ClustersController do expect(assigns(:authorize_url)).to include(key) expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group)) end + + context 'when create_eks_clusters feature flag is enabled' do + before do + stub_feature_flags(create_eks_clusters: true) + end + + context 'when selected provider is gke and no valid gcp token exists' do + it 'redirects to gcp authorize_url' do + go + + expect(response).to redirect_to(assigns(:authorize_url)) + end + end + end end context 'when omniauth has not configured' do diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 35cbab57037..8ac72df5d20 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -79,8 +79,12 @@ describe Projects::ClustersController do end describe 'GET new' do - def go - get :new, params: { namespace_id: project.namespace, project_id: project } + def go(provider: 'gke') + get :new, params: { + namespace_id: project.namespace, + project_id: project, + provider: provider + } end describe 'functionality for new cluster' do @@ -91,6 +95,7 @@ describe Projects::ClustersController do end before do + stub_feature_flags(create_eks_clusters: false) allow(SecureRandom).to receive(:hex).and_return(key) end @@ -100,6 +105,20 @@ describe Projects::ClustersController do expect(assigns(:authorize_url)).to include(key) expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project)) end + + context 'when create_eks_clusters feature flag is enabled' do + before do + stub_feature_flags(create_eks_clusters: true) + end + + context 'when selected provider is gke and no valid gcp token exists' do + it 'redirects to gcp authorize_url' do + go + + expect(response).to redirect_to(assigns(:authorize_url)) + end + end + end end context 'when omniauth has not configured' do diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 180d997a8e8..0c074714bf3 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -19,9 +19,9 @@ describe Projects::ServicesController do it 'renders 404' do allow_any_instance_of(Service).to receive(:can_test?).and_return(false) - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param } + put :test, params: project_params - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(:not_found) end end @@ -29,11 +29,11 @@ describe Projects::ServicesController do let(:service_params) { { active: 'true', url: '' } } it 'returns error messages in JSON response' do - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } + put :test, params: project_params(service: service_params) - expect(json_response['message']).to eq "Validations failed." + expect(json_response['message']).to eq 'Validations failed.' expect(json_response['service_response']).to include "Url can't be blank" - expect(response).to have_gitlab_http_status(200) + expect(response).to be_successful end end @@ -47,9 +47,9 @@ describe Projects::ServicesController do it 'returns success' do allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true) - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param } + put :test, params: project_params - expect(response.status).to eq(200) + expect(response).to be_successful end end @@ -57,11 +57,11 @@ describe Projects::ServicesController do stub_request(:get, 'http://example.com/rest/api/2/serverInfo') .to_return(status: 200, body: '{}') - expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original + expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } + put :test, params: project_params(service: service_params) - expect(response.status).to eq(200) + expect(response).to be_successful end end @@ -69,14 +69,23 @@ describe Projects::ServicesController do stub_request(:get, 'http://example.com/rest/api/2/serverInfo') .to_return(status: 200, body: '{}') - expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original + expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } + put :test, params: project_params(service: service_params) - expect(response.status).to eq(200) + expect(response).to be_successful end context 'when service is configured for the first time' do + let(:service_params) do + { + 'active' => '1', + 'push_events' => '1', + 'token' => 'token', + 'project_url' => 'http://test.com' + } + end + before do allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true) end @@ -84,7 +93,7 @@ describe Projects::ServicesController do it 'persist the object' do do_put - expect(response).to have_gitlab_http_status(200) + expect(response).to be_successful expect(json_response).to be_empty expect(BuildkiteService.first).to be_present end @@ -92,18 +101,14 @@ describe Projects::ServicesController do it 'creates the ServiceHook object' do do_put - expect(response).to have_gitlab_http_status(200) + expect(response).to be_successful expect(json_response).to be_empty expect(BuildkiteService.first.service_hook).to be_present end def do_put - put :test, params: { - namespace_id: project.namespace, - project_id: project, - id: 'buildkite', - service: { 'active' => '1', 'push_events' => '1', token: 'token', 'project_url' => 'http://test.com' } - } + put :test, params: project_params(id: 'buildkite', + service: service_params) end end end @@ -113,9 +118,9 @@ describe Projects::ServicesController do stub_request(:get, 'http://example.com/rest/api/2/serverInfo') .to_return(status: 404) - put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } + put :test, params: project_params(service: service_params) - expect(response).to have_gitlab_http_status(200) + expect(response).to be_successful expect(json_response).to eq( 'error' => true, 'message' => 'Test failed.', @@ -127,39 +132,70 @@ describe Projects::ServicesController do end describe 'PUT #update' do - context 'when param `active` is set to true' do - it 'activates the service and redirects to integrations paths' do - put :update, - params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } } + describe 'as HTML' do + let(:service_params) { { active: true } } - expect(response).to redirect_to(project_settings_integrations_path(project)) - expect(flash[:notice]).to eq 'Jira activated.' + before do + put :update, params: project_params(service: service_params) + end + + context 'when param `active` is set to true' do + it 'activates the service and redirects to integrations paths' do + expect(response).to redirect_to(project_settings_integrations_path(project)) + expect(flash[:notice]).to eq 'Jira activated.' + end + end + + context 'when param `active` is set to false' do + let(:service_params) { { active: false } } + + it 'does not activate the service but saves the settings' do + expect(flash[:notice]).to eq 'Jira settings saved, but not activated.' + end end - end - context 'when param `active` is set to false' do - it 'does not activate the service but saves the settings' do - put :update, - params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: false } } + context 'when activating Jira service from a template' do + let(:service) do + create(:jira_service, project: project, template: true) + end - expect(flash[:notice]).to eq 'Jira settings saved, but not activated.' + it 'activate Jira service from template' do + expect(flash[:notice]).to eq 'Jira activated.' + end end end - context 'when activating Jira service from a template' do - let(:template_service) { create(:jira_service, project: project, template: true) } + describe 'as JSON' do + before do + put :update, params: project_params(service: service_params, format: :json) + end + + context 'when update succeeds' do + let(:service_params) { { url: 'http://example.com' } } + + it 'returns JSON response with no errors' do + expect(response).to be_successful + expect(json_response).to include('active' => true, 'errors' => {}) + end + end - it 'activate Jira service from template' do - put :update, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } } + context 'when update fails' do + let(:service_params) { { url: '' } } - expect(flash[:notice]).to eq 'Jira activated.' + it 'returns JSON response with errors' do + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + 'active' => true, + 'errors' => { 'url' => ['must be a valid URL', %{can't be blank}] } + ) + end end end end - describe "GET #edit" do + describe 'GET #edit' do before do - get :edit, params: { namespace_id: project.namespace, project_id: project, id: 'jira' } + get :edit, params: project_params(id: 'jira') end context 'with approved services' do @@ -168,4 +204,14 @@ describe Projects::ServicesController do end end end + + private + + def project_params(opts = {}) + opts.reverse_merge( + namespace_id: project.namespace, + project_id: project, + id: service.to_param + ) + end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 820ce48e52c..a11237db508 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -18,6 +18,8 @@ describe 'Gcp Cluster', :js do let(:project_id) { 'test-project-1234' } before do + stub_feature_flags(create_eks_clusters: false) + allow_any_instance_of(Projects::ClustersController) .to receive(:token_in_session).and_return('token') allow_any_instance_of(Projects::ClustersController) @@ -147,6 +149,7 @@ describe 'Gcp Cluster', :js do context 'when user has not signed with Google' do before do + stub_feature_flags(create_eks_clusters: false) visit project_clusters_path(project) click_link 'Add Kubernetes cluster' diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index ce382c19fc1..d1cd19dff2d 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -51,6 +51,7 @@ describe 'Clusters', :js do context 'when user has not signed in Google' do before do + stub_feature_flags(create_eks_clusters: false) visit project_clusters_path(project) click_link 'Add Kubernetes cluster' @@ -62,4 +63,27 @@ describe 'Clusters', :js do expect(page).to have_link('Google account') end end + + context 'when create_eks_clusters feature flag is enabled' do + before do + stub_feature_flags(create_eks_clusters: true) + end + + context 'when user access create cluster page' do + before do + visit project_clusters_path(project) + + click_link 'Add Kubernetes cluster' + click_link 'Create new Cluster on GKE' + end + + it 'user sees a link to create a GKE cluster' do + expect(page).to have_link('Google GKE') + end + + it 'user sees a link to create an EKS cluster' do + expect(page).to have_link('Amazon EKS') + end + end + end end diff --git a/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js new file mode 100644 index 00000000000..f076c45e56c --- /dev/null +++ b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js @@ -0,0 +1,115 @@ +import Vuex from 'vuex'; +import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue'; +import { GlToggle } from '@gitlab/ui'; +import { mount, createLocalVue } from '@vue/test-utils'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('GlToggleVuex component', () => { + let wrapper; + let store; + + const findButton = () => wrapper.find('button'); + + const createWrapper = (props = {}) => { + wrapper = mount(GlToggleVuex, { + localVue, + store, + propsData: { + stateProperty: 'toggleState', + ...props, + }, + sync: false, + }); + }; + + beforeEach(() => { + store = new Vuex.Store({ + state: { + toggleState: false, + }, + actions: { + setToggleState: ({ commit }, { key, value }) => commit('setToggleState', { key, value }), + }, + mutations: { + setToggleState: (state, { key, value }) => { + state[key] = value; + }, + }, + }); + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders gl-toggle', () => { + expect(wrapper.find(GlToggle).exists()).toBe(true); + }); + + it('properly computes default value for setAction', () => { + expect(wrapper.props('setAction')).toBe('setToggleState'); + }); + + describe('without a store module', () => { + it('calls action with new value when value changes', () => { + jest.spyOn(store, 'dispatch'); + + findButton().trigger('click'); + expect(store.dispatch).toHaveBeenCalledWith('setToggleState', { + key: 'toggleState', + value: true, + }); + }); + + it('updates store property when value changes', () => { + findButton().trigger('click'); + expect(store.state.toggleState).toBe(true); + }); + }); + + describe('with a store module', () => { + beforeEach(() => { + store = new Vuex.Store({ + modules: { + someModule: { + namespaced: true, + state: { + toggleState: false, + }, + actions: { + setToggleState: ({ commit }, { key, value }) => + commit('setToggleState', { key, value }), + }, + mutations: { + setToggleState: (state, { key, value }) => { + state[key] = value; + }, + }, + }, + }, + }); + + createWrapper({ + storeModule: 'someModule', + }); + }); + + it('calls action with new value when value changes', () => { + jest.spyOn(store, 'dispatch'); + + findButton().trigger('click'); + expect(store.dispatch).toHaveBeenCalledWith('someModule/setToggleState', { + key: 'toggleState', + value: true, + }); + }); + + it('updates store property when value changes', () => { + findButton().trigger('click'); + expect(store.state.someModule.toggleState).toBe(true); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb index 208b60844e3..3cbc1375d6e 100644 --- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb +++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb @@ -20,20 +20,41 @@ describe Gitlab::ImportExport::AttributesFinder do except: [:iid], include: [ { merge_request_diff: { - include: [] + include: [], + preload: { source_project: nil } } }, { merge_request_test: { include: [] } } ], - only: [:id] + only: [:id], + preload: { + merge_request_diff: { source_project: nil }, + merge_request_test: nil + } } }, { commit_statuses: { - include: [{ commit: { include: [] } }] + include: [{ commit: { include: [] } }], + preload: { commit: nil } } }, { project_members: { include: [{ user: { include: [], - only: [:email] } }] + only: [:email] } }], + preload: { user: nil } } } - ] + ], + preload: { + commit_statuses: { + commit: nil + }, + issues: nil, + labels: nil, + merge_requests: { + merge_request_diff: { source_project: nil }, + merge_request_test: nil + }, + project_members: { + user: nil + } + } } end @@ -50,7 +71,8 @@ describe Gitlab::ImportExport::AttributesFinder do setup_yaml(tree: { project: [:issues] }) is_expected.to match( - include: [{ issues: { include: [] } }] + include: [{ issues: { include: [] } }], + preload: { issues: nil } ) end @@ -58,7 +80,8 @@ describe Gitlab::ImportExport::AttributesFinder do setup_yaml(tree: { project: [:project_feature] }) is_expected.to match( - include: [{ project_feature: { include: [] } }] + include: [{ project_feature: { include: [] } }], + preload: { project_feature: nil } ) end @@ -67,7 +90,8 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ issues: { include: [] } }, - { snippets: { include: [] } }] + { snippets: { include: [] } }], + preload: { issues: nil, snippets: nil } ) end @@ -75,7 +99,9 @@ describe Gitlab::ImportExport::AttributesFinder do setup_yaml(tree: { project: [issues: [:notes]] }) is_expected.to match( - include: [{ issues: { include: [{ notes: { include: [] } }] } }] + include: [{ issues: { include: [{ notes: { include: [] } }], + preload: { notes: nil } } }], + preload: { issues: { notes: nil } } ) end @@ -85,7 +111,9 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ merge_requests: { include: [{ notes: { include: [] } }, - { merge_request_diff: { include: [] } }] } }] + { merge_request_diff: { include: [] } }], + preload: { merge_request_diff: nil, notes: nil } } }], + preload: { merge_requests: { merge_request_diff: nil, notes: nil } } ) end @@ -94,8 +122,11 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ merge_requests: { - include: [{ notes: { include: [{ author: { include: [] } }] } }] - } }] + include: [{ notes: { include: [{ author: { include: [] } }], + preload: { author: nil } } }], + preload: { notes: { author: nil } } + } }], + preload: { merge_requests: { notes: { author: nil } } } ) end @@ -105,7 +136,8 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ issues: { include: [], - only: [:name, :description] } }] + only: [:name, :description] } }], + preload: { issues: nil } ) end @@ -115,7 +147,8 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ issues: { except: [:name], - include: [] } }] + include: [] } }], + preload: { issues: nil } ) end @@ -127,7 +160,8 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ issues: { except: [:name], include: [], - only: [:description] } }] + only: [:description] } }], + preload: { issues: nil } ) end @@ -137,7 +171,8 @@ describe Gitlab::ImportExport::AttributesFinder do is_expected.to match( include: [{ issues: { include: [], - methods: [:name] } }] + methods: [:name] } }], + preload: { issues: nil } ) end diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb index e53db37def4..f09a29b84db 100644 --- a/spec/lib/gitlab/import_export/config_spec.rb +++ b/spec/lib/gitlab/import_export/config_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::ImportExport::Config do expect { subject }.not_to raise_error expect(subject).to be_a(Hash) expect(subject.keys).to contain_exactly( - :tree, :excluded_attributes, :included_attributes, :methods) + :tree, :excluded_attributes, :included_attributes, :methods, :preloads) end end end @@ -55,6 +55,10 @@ describe Gitlab::ImportExport::Config do events: - :action + preloads: + statuses: + project: + ee: tree: project: @@ -71,6 +75,9 @@ describe Gitlab::ImportExport::Config do - :type_ee events_ee: - :action_ee + preloads: + statuses: + bridge_ee: EOF end @@ -111,6 +118,11 @@ describe Gitlab::ImportExport::Config do methods: { labels: [:type], events: [:action] + }, + preloads: { + statuses: { + project: nil + } } } ) @@ -150,6 +162,12 @@ describe Gitlab::ImportExport::Config do labels: [:type, :type_ee], events: [:action], events_ee: [:action_ee] + }, + preloads: { + statuses: { + project: nil, + bridge_ee: nil + } } } ) diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb new file mode 100644 index 00000000000..d23b27c9d8e --- /dev/null +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -0,0 +1,272 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::FastHashSerializer do + subject { described_class.new(project, tree).execute } + + let!(:project) { setup_project } + let(:user) { create(:user) } + let(:shared) { project.import_export_shared } + let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } + let(:tree) { reader.project_tree } + + before do + project.add_maintainer(user) + allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD') + allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA') + end + + it 'saves the correct hash' do + is_expected.to include({ 'description' => 'description', 'visibility_level' => 20 }) + end + + it 'has approvals_before_merge set' do + expect(subject['approvals_before_merge']).to eq(1) + end + + it 'has milestones' do + expect(subject['milestones']).not_to be_empty + end + + it 'has merge requests' do + expect(subject['merge_requests']).not_to be_empty + end + + it 'has merge request\'s milestones' do + expect(subject['merge_requests'].first['milestone']).not_to be_empty + end + + it 'has merge request\'s source branch SHA' do + expect(subject['merge_requests'].first['source_branch_sha']).to eq('ABCD') + end + + it 'has merge request\'s target branch SHA' do + expect(subject['merge_requests'].first['target_branch_sha']).to eq('DCBA') + end + + it 'has events' do + expect(subject['merge_requests'].first['milestone']['events']).not_to be_empty + end + + it 'has snippets' do + expect(subject['snippets']).not_to be_empty + end + + it 'has snippet notes' do + expect(subject['snippets'].first['notes']).not_to be_empty + end + + it 'has releases' do + expect(subject['releases']).not_to be_empty + end + + it 'has no author on releases' do + expect(subject['releases'].first['author']).to be_nil + end + + it 'has the author ID on releases' do + expect(subject['releases'].first['author_id']).not_to be_nil + end + + it 'has issues' do + expect(subject['issues']).not_to be_empty + end + + it 'has issue comments' do + notes = subject['issues'].first['notes'] + + expect(notes).not_to be_empty + expect(notes.first['type']).to eq('DiscussionNote') + end + + it 'has issue assignees' do + expect(subject['issues'].first['issue_assignees']).not_to be_empty + end + + it 'has author on issue comments' do + expect(subject['issues'].first['notes'].first['author']).not_to be_empty + end + + it 'has project members' do + expect(subject['project_members']).not_to be_empty + end + + it 'has merge requests diffs' do + expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty + end + + it 'has merge request diff files' do + expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty + end + + it 'has merge request diff commits' do + expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_commits']).not_to be_empty + end + + it 'has merge requests comments' do + expect(subject['merge_requests'].first['notes']).not_to be_empty + end + + it 'has author on merge requests comments' do + expect(subject['merge_requests'].first['notes'].first['author']).not_to be_empty + end + + it 'has pipeline stages' do + expect(subject.dig('ci_pipelines', 0, 'stages')).not_to be_empty + end + + it 'has pipeline statuses' do + expect(subject.dig('ci_pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty + end + + it 'has pipeline builds' do + builds_count = subject + .dig('ci_pipelines', 0, 'stages', 0, 'statuses') + .count { |hash| hash['type'] == 'Ci::Build' } + + expect(builds_count).to eq(1) + end + + it 'has no when YML attributes but only the DB column' do + allow_any_instance_of(Ci::Pipeline) + .to receive(:ci_yaml_file) + .and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) + + expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) + + subject + end + + it 'has pipeline commits' do + expect(subject['ci_pipelines']).not_to be_empty + end + + it 'has ci pipeline notes' do + expect(subject['ci_pipelines'].first['notes']).not_to be_empty + end + + it 'has labels with no associations' do + expect(subject['labels']).not_to be_empty + end + + it 'has labels associated to records' do + expect(subject['issues'].first['label_links'].first['label']).not_to be_empty + end + + it 'has project and group labels' do + label_types = subject['issues'].first['label_links'].map { |link| link['label']['type'] } + + expect(label_types).to match_array(%w(ProjectLabel GroupLabel)) + end + + it 'has priorities associated to labels' do + priorities = subject['issues'].first['label_links'].flat_map { |link| link['label']['priorities'] } + + expect(priorities).not_to be_empty + end + + it 'has issue resource label events' do + expect(subject['issues'].first['resource_label_events']).not_to be_empty + end + + it 'has merge request resource label events' do + expect(subject['merge_requests'].first['resource_label_events']).not_to be_empty + end + + it 'saves the correct service type' do + expect(subject['services'].first['type']).to eq('CustomIssueTrackerService') + end + + it 'saves the properties for a service' do + expect(subject['services'].first['properties']).to eq('one' => 'value') + end + + it 'has project feature' do + project_feature = subject['project_feature'] + expect(project_feature).not_to be_empty + expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED) + expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED) + expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE) + end + + it 'has custom attributes' do + expect(subject['custom_attributes'].count).to eq(2) + end + + it 'has badges' do + expect(subject['project_badges'].count).to eq(2) + end + + it 'does not complain about non UTF-8 characters in MR diff files' do + ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") + + expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty + end + + context 'project attributes' do + it 'does not contain the runners token' do + expect(subject).not_to include("runners_token" => 'token') + end + end + + it 'has a board and a list' do + expect(subject['boards'].first['lists']).not_to be_empty + end + + def setup_project + issue = create(:issue, assignees: [user]) + snippet = create(:project_snippet) + release = create(:release) + group = create(:group) + + project = create(:project, + :public, + :repository, + :issues_disabled, + :wiki_enabled, + :builds_private, + description: 'description', + issues: [issue], + snippets: [snippet], + releases: [release], + group: group, + approvals_before_merge: 1 + ) + project_label = create(:label, project: project) + group_label = create(:group_label, group: group) + create(:label_link, label: project_label, target: issue) + create(:label_link, label: group_label, target: issue) + create(:label_priority, label: group_label, priority: 1) + milestone = create(:milestone, project: project) + merge_request = create(:merge_request, source_project: project, milestone: milestone) + + ci_build = create(:ci_build, project: project, when: nil) + ci_build.pipeline.update(project: project) + create(:commit_status, project: project, pipeline: ci_build.pipeline) + + create(:milestone, project: project) + create(:discussion_note, noteable: issue, project: project) + create(:note, noteable: merge_request, project: project) + create(:note, noteable: snippet, project: project) + create(:note_on_commit, + author: user, + project: project, + commit_id: ci_build.pipeline.sha) + + create(:resource_label_event, label: project_label, issue: issue) + create(:resource_label_event, label: group_label, merge_request: merge_request) + + create(:event, :created, target: milestone, project: project, author: user) + create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) + + create(:project_custom_attribute, project: project) + create(:project_custom_attribute, project: project) + + create(:project_badge, project: project) + create(:project_badge, project: project) + + board = create(:board, project: project, name: 'TestBoard') + create(:list, board: board, position: 0, label: project_label) + + project + end +end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index fefbed93316..ff46e062a5d 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -23,12 +23,65 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(project_tree_saver.save).to be true end + context ':export_fast_serialize feature flag checks' do + before do + expect(Gitlab::ImportExport::Reader).to receive(:new).with(shared: shared).and_return(reader) + expect(reader).to receive(:project_tree).and_return(project_tree) + end + + let(:serializer) { instance_double('Gitlab::ImportExport::FastHashSerializer') } + let(:reader) { instance_double('Gitlab::ImportExport::Reader') } + let(:project_tree) do + { + include: [{ issues: { include: [] } }], + preload: { issues: nil } + } + end + + context 'when :export_fast_serialize feature is enabled' do + before do + stub_feature_flags(export_fast_serialize: true) + end + + it 'uses FastHashSerializer' do + expect(Gitlab::ImportExport::FastHashSerializer) + .to receive(:new) + .with(project, project_tree) + .and_return(serializer) + + expect(serializer).to receive(:execute) + + project_tree_saver.save + end + end + + context 'when :export_fast_serialize feature is disabled' do + before do + stub_feature_flags(export_fast_serialize: false) + end + + it 'is serialized via built-in `as_json`' do + expect(project).to receive(:as_json).with(project_tree) + + project_tree_saver.save + end + end + end + + # It is mostly duplicated in + # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb` + # except: + # context 'with description override' do + # context 'group members' do + # ^ These are specific for the ProjectTreeSaver context 'JSON' do let(:saved_project_json) do project_tree_saver.save project_json(project_tree_saver.full_path) end + # It is not duplicated in + # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb` context 'with description override' do let(:params) { { description: 'Foo Bar' } } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared, params: params) } diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 0dbfcf96124..e0b9581c75c 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' describe Gitlab::ProjectSearchResults do + include SearchHelpers + let(:user) { create(:user) } let(:project) { create(:project) } let(:query) { 'hello world' } @@ -31,10 +33,10 @@ describe Gitlab::ProjectSearchResults do where(:scope, :count_method, :expected) do 'blobs' | :blobs_count | '1234' - 'notes' | :limited_notes_count | '1000+' + 'notes' | :limited_notes_count | max_limited_count 'wiki_blobs' | :wiki_blobs_count | '1234' 'commits' | :commits_count | '1234' - 'projects' | :limited_projects_count | '1000+' + 'projects' | :limited_projects_count | max_limited_count 'unknown' | nil | nil end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 5621c686b8a..26cba53502d 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Gitlab::SearchResults do include ProjectForksHelper + include SearchHelpers let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } @@ -35,11 +36,11 @@ describe Gitlab::SearchResults do using RSpec::Parameterized::TableSyntax where(:scope, :count_method, :expected) do - 'projects' | :limited_projects_count | '1000+' - 'issues' | :limited_issues_count | '1000+' - 'merge_requests' | :limited_merge_requests_count | '1000+' - 'milestones' | :limited_milestones_count | '1000+' - 'users' | :limited_users_count | '1000+' + 'projects' | :limited_projects_count | max_limited_count + 'issues' | :limited_issues_count | max_limited_count + 'merge_requests' | :limited_merge_requests_count | max_limited_count + 'milestones' | :limited_milestones_count | max_limited_count + 'users' | :limited_users_count | max_limited_count 'unknown' | nil | nil end @@ -56,9 +57,9 @@ describe Gitlab::SearchResults do where(:count, :expected) do 23 | '23' - 1000 | '1000' - 1001 | '1000+' - 1234 | '1000+' + 100 | '100' + 101 | max_limited_count + 1234 | max_limited_count end with_them do diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index 89d290aaa81..d3353b76c15 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Gitlab::SnippetSearchResults do + include SearchHelpers + let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') } let(:results) { described_class.new(Snippet.all, 'foo') } @@ -25,7 +27,7 @@ describe Gitlab::SnippetSearchResults do where(:scope, :count_method, :expected) do 'snippet_titles' | :snippet_titles_count | '1234' 'snippet_blobs' | :snippet_blobs_count | '1234' - 'projects' | :limited_projects_count | '1000+' + 'projects' | :limited_projects_count | max_limited_count 'unknown' | nil | nil end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 048d04cdefd..d98b9be726a 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -252,5 +252,43 @@ describe API::Settings, 'Settings' do expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost']) end end + + context 'domain_blacklist settings' do + it 'rejects domain_blacklist_enabled when domain_blacklist is empty' do + put api('/application/settings', admin), + params: { + domain_blacklist_enabled: true, + domain_blacklist: [] + } + + expect(response).to have_gitlab_http_status(400) + message = json_response["message"] + expect(message["domain_blacklist"]).to eq(["Domain blacklist cannot be empty if Blacklist is enabled."]) + end + + it 'allows array for domain_blacklist' do + put api('/application/settings', admin), + params: { + domain_blacklist_enabled: true, + domain_blacklist: ['domain1.com', 'domain2.com'] + } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['domain_blacklist_enabled']).to be(true) + expect(json_response['domain_blacklist']).to eq(['domain1.com', 'domain2.com']) + end + + it 'allows a string for domain_blacklist' do + put api('/application/settings', admin), + params: { + domain_blacklist_enabled: true, + domain_blacklist: 'domain3.com, *.domain4.com' + } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['domain_blacklist_enabled']).to be(true) + expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com']) + end + end end end diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 2cf3f4b83c4..d1d25fbabcd 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -19,4 +19,8 @@ module SearchHelpers click_link scope end end + + def max_limited_count + Gitlab::SearchResults::COUNT_LIMIT_MESSAGE + end end diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml index ed2a3243f0d..116bc8d0b9c 100644 --- a/spec/support/import_export/import_export.yml +++ b/spec/support/import_export/import_export.yml @@ -13,6 +13,10 @@ tree: group_members: - :user +preloads: + merge_request_diff: + source_project: + included_attributes: merge_requests: - :id |