From 72721699f11187199e89631ce0b5e3d2f7c167e9 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 18 Feb 2020 00:09:20 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../pages/admin/serverless/domains/index.js | 19 ++ app/assets/stylesheets/pages/pages.scss | 11 + .../admin/serverless/domains_controller.rb | 62 ++++ .../projects/environments_controller.rb | 2 +- .../merge_requests/diffs_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/pages_domain.rb | 3 + .../environments/auto_stop_service.rb | 2 +- .../dashboard/system_dashboard_service.rb | 1 + app/services/snippets/create_service.rb | 4 +- .../admin/serverless/domains/_form.html.haml | 68 ++++ .../admin/serverless/domains/index.html.haml | 25 ++ .../layouts/nav/sidebar/_admin.html.haml | 5 + .../environments/auto_stop_cron_worker.rb | 2 +- .../35968-add_path_to_edit_custom_metrics.yml | 5 + .../ali-serverless-domains-be-1.yml | 5 + config/initializers/0_license.rb | 2 +- config/initializers/load_balancing.rb | 2 +- config/routes/admin.rb | 8 + ..._add_temporary_index_to_promotion_notes.rb | 23 ++ ...149_schedule_fix_orphan_promoted_issues.rb | 35 ++ db/schema.rb | 1 + doc/administration/audit_events.md | 4 - doc/administration/gitaly/index.md | 1 + doc/api/commits.md | 2 +- doc/ci/environments.md | 67 ++++ doc/ci/img/environment_auto_stop_v12_8.png | Bin 0 -> 43534 bytes doc/ci/review_apps/index.md | 5 + doc/ci/yaml/README.md | 25 +- .../understanding_explain_plans.md | 16 +- doc/user/clusters/applications.md | 28 ++ lib/api/commits.rb | 2 +- .../fix_orphan_promoted_issues.rb | 13 + lib/gitlab/database.rb | 2 +- lib/gitlab/gitaly_client/commit_service.rb | 2 +- lib/gitlab/marginalia.rb | 2 +- .../project_metrics_details_inserter.rb | 40 +++ locale/gitlab.pot | 24 ++ .../serverless/domains_controller_spec.rb | 298 ++++++++++++++++++ .../admin/admin_serverless_domains_spec.rb | 59 ++++ .../metrics/dashboard/schemas/metrics.json | 3 +- .../dashboard/schemas/panel_groups.json | 3 +- spec/lib/gitlab/database_spec.rb | 6 + .../gitaly_client/commit_service_spec.rb | 13 + .../metrics/dashboard/processor_spec.rb | 15 +- spec/models/pages_domain_spec.rb | 30 ++ spec/requests/api/commits_spec.rb | 26 +- .../domains_controller_routing_spec.rb | 22 ++ spec/services/snippets/create_service_spec.rb | 4 +- 49 files changed, 975 insertions(+), 26 deletions(-) create mode 100644 app/assets/javascripts/pages/admin/serverless/domains/index.js create mode 100644 app/controllers/admin/serverless/domains_controller.rb create mode 100644 app/views/admin/serverless/domains/_form.html.haml create mode 100644 app/views/admin/serverless/domains/index.html.haml create mode 100644 changelogs/unreleased/35968-add_path_to_edit_custom_metrics.yml create mode 100644 changelogs/unreleased/ali-serverless-domains-be-1.yml create mode 100644 db/post_migrate/20200207184023_add_temporary_index_to_promotion_notes.rb create mode 100644 db/post_migrate/20200207185149_schedule_fix_orphan_promoted_issues.rb create mode 100644 doc/ci/img/environment_auto_stop_v12_8.png create mode 100644 lib/gitlab/background_migration/fix_orphan_promoted_issues.rb create mode 100644 lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb create mode 100644 spec/controllers/admin/serverless/domains_controller_spec.rb create mode 100644 spec/features/admin/admin_serverless_domains_spec.rb create mode 100644 spec/routing/admin/serverless/domains_controller_routing_spec.rb diff --git a/app/assets/javascripts/pages/admin/serverless/domains/index.js b/app/assets/javascripts/pages/admin/serverless/domains/index.js new file mode 100644 index 00000000000..5be466886a5 --- /dev/null +++ b/app/assets/javascripts/pages/admin/serverless/domains/index.js @@ -0,0 +1,19 @@ +import initSettingsPanels from '~/settings_panels'; + +document.addEventListener('DOMContentLoaded', () => { + // Initialize expandable settings panels + initSettingsPanels(); + + const domainCard = document.querySelector('.js-domain-cert-show'); + const domainForm = document.querySelector('.js-domain-cert-inputs'); + const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn'); + const domainSubmitButton = document.querySelector('.js-serverless-domain-submit'); + + if (domainReplaceButton && domainCard && domainForm) { + domainReplaceButton.addEventListener('click', () => { + domainCard.classList.add('hidden'); + domainForm.classList.remove('hidden'); + domainSubmitButton.removeAttribute('disabled'); + }); + } +}); diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss index 374227fe16a..93caa345f8a 100644 --- a/app/assets/stylesheets/pages/pages.scss +++ b/app/assets/stylesheets/pages/pages.scss @@ -56,4 +56,15 @@ border-top-right-radius: $border-radius-default; } + &.floating-status-badge { + position: absolute; + right: $gl-padding-24; + bottom: $gl-padding-4; + margin-bottom: 0; + } +} + +.form-control.has-floating-status-badge { + position: relative; + padding-right: 120px; } diff --git a/app/controllers/admin/serverless/domains_controller.rb b/app/controllers/admin/serverless/domains_controller.rb new file mode 100644 index 00000000000..c37aec13105 --- /dev/null +++ b/app/controllers/admin/serverless/domains_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class Admin::Serverless::DomainsController < Admin::ApplicationController + before_action :check_feature_flag + before_action :domain, only: [:update, :verify] + + def index + @domain = PagesDomain.instance_serverless.first_or_initialize + end + + def create + if PagesDomain.instance_serverless.count > 0 + return redirect_to admin_serverless_domains_path, notice: _('An instance-level serverless domain already exists.') + end + + @domain = PagesDomain.instance_serverless.create(create_params) + + if @domain.persisted? + redirect_to admin_serverless_domains_path, notice: _('Domain was successfully created.') + else + render 'index' + end + end + + def update + if domain.update(update_params) + redirect_to admin_serverless_domains_path, notice: _('Domain was successfully updated.') + else + render 'index' + end + end + + def verify + result = VerifyPagesDomainService.new(domain).execute + + if result[:status] == :success + flash[:notice] = _('Successfully verified domain ownership') + else + flash[:alert] = _('Failed to verify domain ownership') + end + + redirect_to admin_serverless_domains_path + end + + private + + def domain + @domain = PagesDomain.instance_serverless.find(params[:id]) + end + + def check_feature_flag + render_404 unless Feature.enabled?(:serverless_domain) + end + + def update_params + params.require(:pages_domain).permit(:user_provided_certificate, :user_provided_key) + end + + def create_params + params.require(:pages_domain).permit(:domain, :user_provided_certificate, :user_provided_key) + end +end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 70c4b536854..5c49fa842a4 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -16,7 +16,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:prometheus_computed_alerts) end before_action do - push_frontend_feature_flag(:auto_stop_environments) + push_frontend_feature_flag(:auto_stop_environments, default_enabled: true) end after_action :expire_etag_cache, only: [:cancel_auto_stop] diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 872497992e1..953b2ffeb0b 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -18,7 +18,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end def diffs_batch - return render_404 unless Feature.enabled?(:diffs_batch_load, @merge_request.project) + return render_404 unless Feature.enabled?(:diffs_batch_load, @merge_request.project, default_enabled: true) diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options) positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 8c0188e1783..c5f017efe8d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -19,7 +19,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action only: [:show] do - push_frontend_feature_flag(:diffs_batch_load, @project) + push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true) push_frontend_feature_flag(:single_mr_diff_view, @project) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 91767c53f81..05cf427184c 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -14,6 +14,7 @@ class PagesDomain < ApplicationRecord validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, uniqueness: { case_sensitive: false } + validates :certificate, :key, presence: true, if: :usage_serverless? validates :certificate, presence: { message: 'must be present if HTTPS-only is enabled' }, if: :certificate_should_be_present? validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? } @@ -64,6 +65,8 @@ class PagesDomain < ApplicationRecord scope :with_logging_info, -> { includes(project: [:namespace, :route]) } + scope :instance_serverless, -> { where(wildcard: true, scope: :instance, usage: :serverless) } + def verified? !!verified_at end diff --git a/app/services/environments/auto_stop_service.rb b/app/services/environments/auto_stop_service.rb index 6eef8138493..ee7f25a4d76 100644 --- a/app/services/environments/auto_stop_service.rb +++ b/app/services/environments/auto_stop_service.rb @@ -30,7 +30,7 @@ module Environments def stop_in_batch environments = Environment.auto_stoppable(BATCH_SIZE) - return false unless environments.exists? && Feature.enabled?(:auto_stop_environments) + return false unless environments.exists? && Feature.enabled?(:auto_stop_environments, default_enabled: true) Ci::StopEnvironmentsService.execute_in_batch(environments) end diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb index bef65dbe1c2..aa8421e10d5 100644 --- a/app/services/metrics/dashboard/system_dashboard_service.rb +++ b/app/services/metrics/dashboard/system_dashboard_service.rb @@ -11,6 +11,7 @@ module Metrics SEQUENCE = [ STAGES::CommonMetricsInserter, STAGES::ProjectMetricsInserter, + STAGES::ProjectMetricsDetailsInserter, STAGES::EndpointInserter, STAGES::Sorter ].freeze diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 9e87bebbe4e..7ded185a6f9 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -24,8 +24,8 @@ module Snippets spam_check(snippet, current_user) snippet_saved = snippet.with_transaction_returning_status do - if snippet.save && snippet.store_mentions! - create_repository_for(snippet, current_user) + (snippet.save && snippet.store_mentions!).tap do |saved| + create_repository_for(snippet, current_user) if saved end end diff --git a/app/views/admin/serverless/domains/_form.html.haml b/app/views/admin/serverless/domains/_form.html.haml new file mode 100644 index 00000000000..8c1c1d41caa --- /dev/null +++ b/app/views/admin/serverless/domains/_form.html.haml @@ -0,0 +1,68 @@ +- form_name = 'js-serverless-domain-settings' +- form_url = @domain.persisted? ? admin_serverless_domain_path(@domain.id, anchor: form_name) : admin_serverless_domains_path(anchor: form_name) +- show_certificate_card = @domain.persisted? && @domain.errors.blank? += form_for @domain, url: form_url, html: { class: 'fieldset-form' } do |f| + = form_errors(@domain) + + %fieldset + - if @domain.persisted? + - dns_record = "*.#{@domain.domain} CNAME #{Settings.pages.host}." + - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}" + .form-group.row + .col-sm-6.position-relative + = f.label :domain, _('Domain'), class: 'label-bold' + = f.text_field :domain, class: 'form-control has-floating-status-badge', readonly: true + .status-badge.floating-status-badge + - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success'] + .badge{ class: status } + = text + = link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "btn has-tooltip", title: _("Retry verification") + + .col-sm-6 + = f.label :serverless_domain_dns, _('DNS'), class: 'label-bold' + .input-group + = text_field_tag :serverless_domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true + .input-group-append + = clipboard_button(target: '#serverless_domain_dns', class: 'btn-default input-group-text d-none d-sm-block') + + .col-sm-12.form-text.text-muted + = _("To access this domain create a new DNS record") + + .form-group + = f.label :serverless_domain_verification, _('Verification status'), class: 'label-bold' + .input-group + = text_field_tag :serverless_domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true + .input-group-append + = clipboard_button(target: '#serverless_domain_verification', class: 'btn-default d-none d-sm-block') + %p.form-text.text-muted + - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')) + = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help } + + - else + .form-group + = f.label :domain, _('Domain'), class: 'label-bold' + = f.text_field :domain, class: 'form-control' + + - if show_certificate_card + .card.js-domain-cert-show + .card-header + = _('Certificate') + .d-flex.justify-content-between.align-items-center.p-3 + %span + = @domain.subject || _('missing') + %button.btn.btn-remove.btn-sm.js-domain-cert-replace-btn{ type: 'button' } + = _('Replace') + + .js-domain-cert-inputs{ class: ('hidden' if show_certificate_card) } + .form-group + = f.label :user_provided_certificate, _('Certificate (PEM)'), class: 'label-bold' + = f.text_area :user_provided_certificate, rows: 5, class: 'form-control', value: '' + %span.form-text.text-muted + = _("Upload a certificate for your domain with all intermediates") + .form-group + = f.label :user_provided_key, _('Key (PEM)'), class: 'label-bold' + = f.text_area :user_provided_key, rows: 5, class: 'form-control', value: '' + %span.form-text.text-muted + = _("Upload a private key for your certificate") + + = f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "btn btn-success js-serverless-domain-submit", disabled: @domain.persisted? diff --git a/app/views/admin/serverless/domains/index.html.haml b/app/views/admin/serverless/domains/index.html.haml new file mode 100644 index 00000000000..bd3c6bc6e04 --- /dev/null +++ b/app/views/admin/serverless/domains/index.html.haml @@ -0,0 +1,25 @@ +- breadcrumb_title _("Operations") +- page_title _("Operations") +- @content_class = "limit-container-width" unless fluid_layout + +-# normally expanded_by_default? is used here, but since this is the only panel +-# in this settings page, let's leave it always open by default +- expanded = true + +%section.settings.as-serverless-domain.no-animate#js-serverless-domain-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Serverless domain') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Set an instance-wide domain that will be available to all clusters when installing Knative.') + .settings-content + - if Gitlab.config.pages.enabled + = render 'form' + - else + .card + .card-header + = s_('GitLabPages|Domains') + .nothing-here-block + = s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.") diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index e8e1da720cd..9f70124ba0d 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -254,6 +254,11 @@ = link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do %span = _('CI/CD') + - if Feature.enabled?(:serverless_domain) + = nav_link(path: 'application_settings#operations') do + = link_to admin_serverless_domains_path, title: _('Operations') do + %span + = _('Operations') = nav_link(path: 'application_settings#reporting') do = link_to reporting_admin_application_settings_path, title: _('Reporting') do %span diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb index 8fcda35b414..fdc9490453c 100644 --- a/app/workers/environments/auto_stop_cron_worker.rb +++ b/app/workers/environments/auto_stop_cron_worker.rb @@ -8,7 +8,7 @@ module Environments feature_category :continuous_delivery def perform - return unless Feature.enabled?(:auto_stop_environments) + return unless Feature.enabled?(:auto_stop_environments, default_enabled: true) AutoStopService.new.execute end diff --git a/changelogs/unreleased/35968-add_path_to_edit_custom_metrics.yml b/changelogs/unreleased/35968-add_path_to_edit_custom_metrics.yml new file mode 100644 index 00000000000..5820296828f --- /dev/null +++ b/changelogs/unreleased/35968-add_path_to_edit_custom_metrics.yml @@ -0,0 +1,5 @@ +--- +title: Adds path to edit custom metrics in dashboard response +merge_request: 24645 +author: +type: added diff --git a/changelogs/unreleased/ali-serverless-domains-be-1.yml b/changelogs/unreleased/ali-serverless-domains-be-1.yml new file mode 100644 index 00000000000..9d0095f27c7 --- /dev/null +++ b/changelogs/unreleased/ali-serverless-domains-be-1.yml @@ -0,0 +1,5 @@ +--- +title: Add admin settings panel for instance-level serverless domain (behind feature flag) +merge_request: 21222 +author: +type: added diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb index 19c71c34904..5c4546f499f 100644 --- a/config/initializers/0_license.rb +++ b/config/initializers/0_license.rb @@ -10,7 +10,7 @@ Gitlab.ee do end # Needed to run migration - if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses') + if Gitlab::Database.cached_table_exists?('licenses') message = LicenseHelper.license_message(signed_in: true, is_admin: true, in_html: false) if ::License.block_changes? && message.present? warn "WARNING: #{message}" diff --git a/config/initializers/load_balancing.rb b/config/initializers/load_balancing.rb index a49bcbe1f96..7502a6299ae 100644 --- a/config/initializers/load_balancing.rb +++ b/config/initializers/load_balancing.rb @@ -3,7 +3,7 @@ # We need to run this initializer after migrations are done so it doesn't fail on CI Gitlab.ee do - if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses') + if Gitlab::Database.cached_table_exists?('licenses') if Gitlab::Database::LoadBalancing.enable? Gitlab::Database.disable_prepared_statements diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5493e229f7e..5210b84c8ba 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -32,6 +32,14 @@ namespace :admin do resources :abuse_reports, only: [:index, :destroy] resources :gitaly_servers, only: [:index] + namespace :serverless do + resources :domains, only: [:index, :create, :update] do + member do + post '/verify', to: 'domains#verify' + end + end + end + resources :spam_logs, only: [:index, :destroy] do member do post :mark_as_ham diff --git a/db/post_migrate/20200207184023_add_temporary_index_to_promotion_notes.rb b/db/post_migrate/20200207184023_add_temporary_index_to_promotion_notes.rb new file mode 100644 index 00000000000..44a32938483 --- /dev/null +++ b/db/post_migrate/20200207184023_add_temporary_index_to_promotion_notes.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTemporaryIndexToPromotionNotes < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :notes, + :note, + where: "noteable_type = 'Issue' AND system IS TRUE AND note LIKE 'promoted to epic%'", + name: 'tmp_idx_on_promoted_notes' + end + + def down + # NO OP + end +end diff --git a/db/post_migrate/20200207185149_schedule_fix_orphan_promoted_issues.rb b/db/post_migrate/20200207185149_schedule_fix_orphan_promoted_issues.rb new file mode 100644 index 00000000000..83ba56501dd --- /dev/null +++ b/db/post_migrate/20200207185149_schedule_fix_orphan_promoted_issues.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class ScheduleFixOrphanPromotedIssues < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 100 + BACKGROUND_MIGRATION = 'FixOrphanPromotedIssues'.freeze + + disable_ddl_transaction! + + class Note < ActiveRecord::Base + include EachBatch + + self.table_name = 'notes' + + scope :of_promotion, -> do + where(noteable_type: 'Issue') + .where('notes.system IS TRUE') + .where("notes.note LIKE 'promoted to epic%'") + end + end + + def up + Note.of_promotion.each_batch(of: BATCH_SIZE) do |notes, index| + jobs = notes.map { |note| [BACKGROUND_MIGRATION, [note.id]] } + + BackgroundMigrationWorker.bulk_perform_async(jobs) + end + end + + def down + # NO OP + end +end diff --git a/db/schema.rb b/db/schema.rb index 0259c63d3e5..435d994e201 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2811,6 +2811,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_220211) do t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))" t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin + t.index ["note"], name: "tmp_idx_on_promoted_notes", where: "(((noteable_type)::text = 'Issue'::text) AND (system IS TRUE) AND (note ~~ 'promoted to epic%'::text))" t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" t.index ["project_id", "id"], name: "index_notes_on_project_id_and_id_and_system_false", where: "(NOT system)" t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type" diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index fafd078c487..cf017c0167b 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -1,7 +1,3 @@ ---- -last_updated: 2019-09-16 ---- - # Audit Events **(STARTER)** GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan][ee]. diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 030ed3bc96b..390e0ae05af 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -584,6 +584,7 @@ a few things that you need to do: 1. Configure [object storage for merge request diffs](../merge_request_diffs.md#using-object-storage). 1. Configure [object storage for packages](../packages/index.md#using-object-storage) (optional feature). 1. Configure [object storage for dependency proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature). +1. Configure [object storage for Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage) (optional feature). NOTE: **Note:** One current feature of GitLab that still requires a shared directory (NFS) is diff --git a/doc/api/commits.md b/doc/api/commits.md index eb3fb7b2195..ee635a009bf 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -18,7 +18,7 @@ GET /projects/:id/repository/commits | `all` | boolean | no | Retrieve every commit from the repository | | `with_stats` | boolean | no | Stats about each commit will be added to the response | | `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit | -| `order` | string | no | List commits in order. Possible value: [`topo`](https://git-scm.com/docs/git-log#Documentation/git-log.txt---topo-order). | +| `order` | string | no | List commits in order. Possible values: `default`, [`topo`](https://git-scm.com/docs/git-log#Documentation/git-log.txt---topo-order). Defaults to `default`, the commits are shown in reverse chronological order. | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/repository/commits" diff --git a/doc/ci/environments.md b/doc/ci/environments.md index eddda2031b0..65dc65f23f5 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -624,6 +624,73 @@ to automatically stop. You can read more in the [`.gitlab-ci.yml` reference](yaml/README.md#environmenton_stop). +#### Environments auto-stop + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/20956) in GitLab 12.8. + +You can set a expiry time to environments and stop them automatically after a certain period. + +For example, consider the use of this feature with Review Apps environments. +When you set up Review Apps, sometimes they keep running for a long time +because some merge requests are left as open. An example for this situation is when the author of the merge +request is not actively working on it, due to priority changes or a different approach was decided on, and the merge requests was simply forgotten. +Idle environments waste resources, therefore they +should be terminated as soon as possible. + +To address this problem, you can specify an optional expiration date for +Review Apps environments. When the expiry time is reached, GitLab will automatically trigger a job +to stop the environment, eliminating the need of manually doing so. In case an environment is updated, the expiration is renewed +ensuring that only active merge requests keep running Review Apps. + +To enable this feature, you need to specify the [`environment:auto_stop_in`](yaml/README.md#environmentauto_stop_in) keyword in `.gitlab-ci.yml`. +You can specify a human-friendly date as the value, such as `1 hour and 30 minutes` or `1 day`. +`auto_stop_in` uses the same format of [`artifacts:expire_in` docs](yaml/README.md#artifactsexpire_in). + +##### Auto-stop example + +In the following example, there is a basic review app setup that creates a new environment +per merge request. The `review_app` job is triggered by every push and +creates or updates an environment named `review/your-branch-name`. +The environment keeps running until `stop_review_app` is executed: + +```yaml +review_app: + script: deploy-review-app + environment: + name: review/$CI_COMMIT_REF_NAME + on_stop: stop_review_app + auto_stop_in: 1 week + +stop_review_app: + script: stop-review-app + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + when: manual +``` + +As long as a merge request is active and keeps getting new commits, +the review app will not stop, so developers don't need to worry about +re-initiating review app. + +On the other hand, since `stop_review_app` is set to `auto_stop_in: 1 week`, +if a merge request becomes inactive for more than a week, +GitLab automatically triggers the `stop_review_app` job to stop the environment. + +You can also check the expiration date of environments through the GitLab UI. To do so, +go to **Operations > Environments > Environment**. You can see the auto-stop period +at the left-top section and a pin-mark button at the right-top section. This pin-mark +button can be used to prevent auto-stopping the environment. By clicking this button, the `auto_stop_in` setting is over-written +and the environment will be active until it's stopped manually. + +![Environment auto stop](img/environment_auto_stop_v12_8.png) + +NOTE: **NOTE** +Due to the resource limitation, a background worker for stopping environments only +runs once every hour. This means environments will not be stopped at the exact +timestamp as the specified period, but will be stopped when the hourly cron worker +detects expired environments. + ### Grouping similar environments > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14. diff --git a/doc/ci/img/environment_auto_stop_v12_8.png b/doc/ci/img/environment_auto_stop_v12_8.png new file mode 100644 index 0000000000000000000000000000000000000000..3a3c54ab62da5b564dc8ffb50d25c1b84d7f2b5c GIT binary patch literal 43534 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 bG~4#yHrG0J)vzJreKVSAtbbl_*|z@!>Kv{r literal 0 HcmV?d00001 diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index cc081f2543a..bdef2eca1f2 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -80,6 +80,11 @@ you can copy and paste into `.gitlab-ci.yml` as a starting point. To do so: 1. Feel free to tune this template to your own needs. +## Review Apps auto-stop + +See how to [configure Review Apps environments to expire and auto-stop](../environments.md#environments-auto-stop) +after a given period of time. + ## Review Apps examples The following are example projects that demonstrate Review App configuration: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3367fac5137..1a301481f05 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -105,7 +105,7 @@ The following table lists available parameters for jobs: | [`tags`](#tags) | List of tags which are used to select Runner. | | [`allow_failure`](#allow_failure) | Allow job to fail. Failed job doesn't contribute to commit status. | | [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. | -| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, and `environment:action`. | +| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. | | [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. | | [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, and `artifacts:reports:junit`.

In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_management`, `artifacts:reports:performance` and `artifacts:reports:metrics`. | | [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. | @@ -1453,6 +1453,29 @@ The `stop_review_app` job is **required** to have the following keywords defined - `stage` should be the same as the `review_app` in order for the environment to stop automatically when the branch is deleted +#### `environment:auto_stop_in` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/20956) in GitLab 12.8. + +The `auto_stop_in` keyword is for specifying life period of the environment, +that when expired, GitLab GitLab automatically stops them. + +For example, + +```yaml +review_app: + script: deploy-review-app + environment: + name: review/$CI_COMMIT_REF_NAME + auto_stop_in: 1 day +``` + +When `review_app` job is executed and a review app is created, a life period of +the environment is set to `1 day`. + +For more information, see +[the environments auto-stop documentation](../environments.md#environments-auto-stop) + #### `environment:kubernetes` > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6. diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md index 8f4caf286fe..8b1a6df72c4 100644 --- a/doc/development/understanding_explain_plans.md +++ b/doc/development/understanding_explain_plans.md @@ -707,7 +707,7 @@ For more information about the available options, run: ### `#database-lab` -Another tool GitLab employees can use is a chatbot powered by [Joe](https://gitlab.com/postgres-ai/joe), available in the [`#database-lab`](https://gitlab.slack.com/archives/CLJMDRD8C) channel on Slack. +Another tool GitLab employees can use is a chatbot powered by [Joe](https://gitlab.com/postgres-ai/joe) which uses [Database Lab](https://gitlab.com/postgres-ai/database-lab) to instantly provide developers with their own clone of the production database. Joe is available in the [`#database-lab`](https://gitlab.slack.com/archives/CLJMDRD8C) channel on Slack. Unlike chatops, it gives you a way to execute DDL statements (like creating indexes and tables) and get query plan not only for `SELECT` but also `UPDATE` and `DELETE`. For example, in order to test new index you can do the following: @@ -742,6 +742,20 @@ For more information about the available options, run: help ``` +#### Tips & Tricks + +The database connection is now maintained during your whole session, so you can use `exec set ...` for any session variables (such as `enable_seqscan` or `work_mem`). These settings will be applied to all subsequent commands until you reset them. + +It is also possible to use transactions. This may be useful when you are working on statements that modify the data, for example INSERT, UPDATE, and DELETE. The `explain` command will perform `EXPLAIN ANALYZE`, which executes the statement. In order to run each `explain` starting from a clean state you can wrap it in a transaction, for example: + +```sql +exec BEGIN + +explain UPDATE some_table SET some_column = TRUE + +exec ROLLBACK +``` + ## Further reading A more extensive guide on understanding query plans can be found in diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 55c51ef5fb6..c526f7339d5 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -488,6 +488,7 @@ Supported applications: - [GitLab Runner](#install-gitlab-runner-using-gitlab-ci) - [Cilium](#install-cilium-using-gitlab-ci) - [JupyterHub](#install-jupyterhub-using-gitlab-ci) +- [Elastic Stack](#install-elastic-stack-using-gitlab-ci) ### Usage @@ -791,6 +792,33 @@ project. Refer to the [chart reference](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html) for the available configuration options. +### Install Elastic Stack using GitLab CI + +> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/cluster-applications/-/merge_requests/45) in GitLab 12.8. + +Elastic Stack is installed using GitLab CI by defining configuration in +`.gitlab/managed-apps/config.yaml`. + +The following configuration is required to install Elastic Stack using GitLab CI: + +```yaml +elasticStack: + installed: true +``` + +Elastic Stack is installed into the `gitlab-managed-apps` namespace of your cluster. + +You can check the default [values.yaml](https://gitlab.com/gitlab-org/gitlab/-/blob/master/vendor/elastic_stack/values.yaml) we set for this chart. + +You can customize the installation of Elastic Stack by defining +`.gitlab/managed-apps/elastic-stack/values.yaml` file in your cluster +management project. Refer to the +[chart](https://github.com/helm/charts/blob/master/stable/elastic-stack/values.yaml) for the +available configuration options. + +NOTE: **Note:** +In this alpha implementation of installing Elastic Stack through CI, reading the environment pod logs through Elasticsearch is unsupported. This is supported if [installed via the UI](#elastic-stack). + ## Upgrading applications > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24789) in GitLab 11.8. diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4e04a99e97c..dfb0066ceb0 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -38,7 +38,7 @@ module API optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' - optional :order, type: String, desc: 'List commits in order', values: %w[topo] + optional :order, type: String, desc: 'List commits in order', default: 'default', values: %w[default topo] use :pagination end get ':id/repository/commits' do diff --git a/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb new file mode 100644 index 00000000000..d6ec56ae19e --- /dev/null +++ b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # No OP for CE + class FixOrphanPromotedIssues + def perform(note_id) + end + end + end +end + +Gitlab::BackgroundMigration::FixOrphanPromotedIssues.prepend_if_ee('EE::Gitlab::BackgroundMigration::FixOrphanPromotedIssues') diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index a614e68703c..02005be1f6a 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -233,7 +233,7 @@ module Gitlab end def self.cached_table_exists?(table_name) - connection.schema_cache.data_source_exists?(table_name) + exists? && connection.schema_cache.data_source_exists?(table_name) end def self.database_version diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index b981b9cf1bc..ac22f5bf419 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -324,7 +324,7 @@ module Gitlab request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.revision = encode_binary(options[:ref]) if options[:ref] - request.order = options[:order].upcase if options[:order].present? + request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present? request.paths = encode_repeated(Array(options[:path])) if options[:path].present? diff --git a/lib/gitlab/marginalia.rb b/lib/gitlab/marginalia.rb index b4b9896e6b9..24e21a1d512 100644 --- a/lib/gitlab/marginalia.rb +++ b/lib/gitlab/marginalia.rb @@ -22,7 +22,7 @@ module Gitlab def self.set_feature_cache # During db:create and db:bootstrap skip feature query as DB is not available yet. - return false unless ActiveRecord::Base.connected? && Gitlab::Database.cached_table_exists?('features') + return false unless Gitlab::Database.cached_table_exists?('features') self.enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG) end diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb new file mode 100644 index 00000000000..a9d11f58255 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + module Stages + class ProjectMetricsDetailsInserter < BaseStage + def transform! + dashboard[:panel_groups].each do |panel_group| + next unless panel_group + + has_custom_metrics = custom_group_titles.include?(panel_group[:group]) + panel_group[:has_custom_metrics] = has_custom_metrics + + panel_group[:panels].each do |panel| + next unless panel + + panel[:metrics].each do |metric| + next unless metric + + metric[:edit_path] = has_custom_metrics ? edit_path(metric) : nil + end + end + end + end + + private + + def custom_group_titles + @custom_group_titles ||= PrometheusMetricEnums.custom_group_details.values.map { |group_details| group_details[:group_title] } + end + + def edit_path(metric) + Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path(project, metric[:metric_id]) + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ea0a773a525..2514bf89c8a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1107,6 +1107,9 @@ msgstr "" msgid "Add comment now" msgstr "" +msgid "Add domain" +msgstr "" + msgid "Add email address" msgstr "" @@ -1919,6 +1922,9 @@ msgstr "" msgid "An error occurred. Please try again." msgstr "" +msgid "An instance-level serverless domain already exists." +msgstr "" + msgid "An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable." msgstr "" @@ -6807,6 +6813,12 @@ msgstr "" msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled" msgstr "" +msgid "Domain was successfully created." +msgstr "" + +msgid "Domain was successfully updated." +msgstr "" + msgid "Don't have an account yet?" msgstr "" @@ -8184,6 +8196,9 @@ msgstr "" msgid "Failed to upload object map file" msgstr "" +msgid "Failed to verify domain ownership" +msgstr "" + msgid "Failure" msgstr "" @@ -17229,6 +17244,9 @@ msgstr "" msgid "Serverless" msgstr "" +msgid "Serverless domain" +msgstr "" + msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first." msgstr "" @@ -17334,6 +17352,9 @@ msgstr "" msgid "Set a template repository for projects in this group" msgstr "" +msgid "Set an instance-wide domain that will be available to all clusters when installing Knative." +msgstr "" + msgid "Set default and restrict visibility levels. Configure import sources and git access protocol." msgstr "" @@ -18548,6 +18569,9 @@ msgstr "" msgid "Successfully unlocked" msgstr "" +msgid "Successfully verified domain ownership" +msgstr "" + msgid "Suggest code changes which can be immediately applied in one click. Try it out!" msgstr "" diff --git a/spec/controllers/admin/serverless/domains_controller_spec.rb b/spec/controllers/admin/serverless/domains_controller_spec.rb new file mode 100644 index 00000000000..aed83e190be --- /dev/null +++ b/spec/controllers/admin/serverless/domains_controller_spec.rb @@ -0,0 +1,298 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::Serverless::DomainsController do + let(:admin) { create(:admin) } + let(:user) { create(:user) } + + describe '#index' do + context 'non-admin user' do + before do + sign_in(user) + end + + it 'responds with 404' do + get :index + + expect(response.status).to eq(404) + end + end + + context 'admin user' do + before do + create(:pages_domain) + sign_in(admin) + end + + context 'with serverless_domain feature disabled' do + before do + stub_feature_flags(serverless_domain: false) + end + + it 'responds with 404' do + get :index + + expect(response.status).to eq(404) + end + end + + context 'when instance-level serverless domain exists' do + let!(:serverless_domain) { create(:pages_domain, :instance_serverless) } + + it 'loads the instance serverless domain' do + get :index + + expect(assigns(:domain).id).to eq(serverless_domain.id) + end + end + + context 'when domain does not exist' do + it 'initializes an instance serverless domain' do + get :index + + domain = assigns(:domain) + + expect(domain.persisted?).to eq(false) + expect(domain.wildcard).to eq(true) + expect(domain.scope).to eq('instance') + expect(domain.usage).to eq('serverless') + end + end + end + end + + describe '#create' do + let(:create_params) do + sample_domain = build(:pages_domain) + + { + domain: 'serverless.gitlab.io', + user_provided_certificate: sample_domain.certificate, + user_provided_key: sample_domain.key + } + end + + context 'non-admin user' do + before do + sign_in(user) + end + + it 'responds with 404' do + post :create, params: { pages_domain: create_params } + + expect(response.status).to eq(404) + end + end + + context 'admin user' do + before do + sign_in(admin) + end + + context 'with serverless_domain feature disabled' do + before do + stub_feature_flags(serverless_domain: false) + end + + it 'responds with 404' do + post :create, params: { pages_domain: create_params } + + expect(response.status).to eq(404) + end + end + + context 'when an instance-level serverless domain exists' do + let!(:serverless_domain) { create(:pages_domain, :instance_serverless) } + + it 'does not create a new domain' do + expect { post :create, params: { pages_domain: create_params } }.not_to change { PagesDomain.instance_serverless.count } + end + + it 'redirects to index' do + post :create, params: { pages_domain: create_params } + + expect(response).to redirect_to admin_serverless_domains_path + expect(flash[:notice]).to include('An instance-level serverless domain already exists.') + end + end + + context 'when an instance-level serverless domain does not exist' do + it 'creates an instance serverless domain with the provided attributes' do + expect { post :create, params: { pages_domain: create_params } }.to change { PagesDomain.instance_serverless.count }.by(1) + + domain = PagesDomain.instance_serverless.first + expect(domain.domain).to eq(create_params[:domain]) + expect(domain.certificate).to eq(create_params[:user_provided_certificate]) + expect(domain.key).to eq(create_params[:user_provided_key]) + expect(domain.wildcard).to eq(true) + expect(domain.scope).to eq('instance') + expect(domain.usage).to eq('serverless') + end + + it 'redirects to index' do + post :create, params: { pages_domain: create_params } + + expect(response).to redirect_to admin_serverless_domains_path + expect(flash[:notice]).to include('Domain was successfully created.') + end + end + + context 'when there are errors' do + it 'renders index view' do + post :create, params: { pages_domain: { foo: 'bar' } } + + expect(assigns(:domain).errors.size).to be > 0 + expect(response).to render_template('index') + end + end + end + end + + describe '#update' do + let(:domain) { create(:pages_domain, :instance_serverless) } + + let(:update_params) do + sample_domain = build(:pages_domain) + + { + user_provided_certificate: sample_domain.certificate, + user_provided_key: sample_domain.key + } + end + + context 'non-admin user' do + before do + sign_in(user) + end + + it 'responds with 404' do + put :update, params: { id: domain.id, pages_domain: update_params } + + expect(response.status).to eq(404) + end + end + + context 'admin user' do + before do + sign_in(admin) + end + + context 'with serverless_domain feature disabled' do + before do + stub_feature_flags(serverless_domain: false) + end + + it 'responds with 404' do + put :update, params: { id: domain.id, pages_domain: update_params } + + expect(response.status).to eq(404) + end + end + + context 'when domain exists' do + it 'updates the domain with the provided attributes' do + new_certificate = build(:pages_domain, :ecdsa).certificate + new_key = build(:pages_domain, :ecdsa).key + + put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: new_certificate, user_provided_key: new_key } } + + domain.reload + + expect(domain.certificate).to eq(new_certificate) + expect(domain.key).to eq(new_key) + end + + it 'does not update the domain name' do + put :update, params: { id: domain.id, pages_domain: { domain: 'new.com' } } + + expect(domain.reload.domain).not_to eq('new.com') + end + + it 'redirects to index' do + put :update, params: { id: domain.id, pages_domain: update_params } + + expect(response).to redirect_to admin_serverless_domains_path + expect(flash[:notice]).to include('Domain was successfully updated.') + end + end + + context 'when domain does not exist' do + it 'returns 404' do + put :update, params: { id: 0, pages_domain: update_params } + + expect(response.status).to eq(404) + end + end + + context 'when there are errors' do + it 'renders index view' do + put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: 'bad certificate' } } + + expect(assigns(:domain).errors.size).to be > 0 + expect(response).to render_template('index') + end + end + end + end + + describe '#verify' do + let(:domain) { create(:pages_domain, :instance_serverless) } + + context 'non-admin user' do + before do + sign_in(user) + end + + it 'responds with 404' do + post :verify, params: { id: domain.id } + + expect(response.status).to eq(404) + end + end + + context 'admin user' do + before do + sign_in(admin) + end + + def stub_service + service = double(:service) + + expect(VerifyPagesDomainService).to receive(:new).with(domain).and_return(service) + + service + end + + context 'with serverless_domain feature disabled' do + before do + stub_feature_flags(serverless_domain: false) + end + + it 'responds with 404' do + post :verify, params: { id: domain.id } + + expect(response.status).to eq(404) + end + end + + it 'handles verification success' do + expect(stub_service).to receive(:execute).and_return(status: :success) + + post :verify, params: { id: domain.id } + + expect(response).to redirect_to admin_serverless_domains_path + expect(flash[:notice]).to eq('Successfully verified domain ownership') + end + + it 'handles verification failure' do + expect(stub_service).to receive(:execute).and_return(status: :failed) + + post :verify, params: { id: domain.id } + + expect(response).to redirect_to admin_serverless_domains_path + expect(flash[:alert]).to eq('Failed to verify domain ownership') + end + end + end +end diff --git a/spec/features/admin/admin_serverless_domains_spec.rb b/spec/features/admin/admin_serverless_domains_spec.rb new file mode 100644 index 00000000000..85fe67004da --- /dev/null +++ b/spec/features/admin/admin_serverless_domains_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Admin Serverless Domains', :js do + let(:sample_domain) { build(:pages_domain) } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + sign_in(create(:admin)) + end + + it 'Add domain with certificate' do + visit admin_serverless_domains_path + + fill_in 'pages_domain[domain]', with: 'foo.com' + fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate + fill_in 'pages_domain[user_provided_key]', with: sample_domain.key + click_button 'Add domain' + + expect(current_path).to eq admin_serverless_domains_path + + expect(page).to have_field('pages_domain[domain]', with: 'foo.com') + expect(page).to have_field('serverless_domain_dns', with: /^\*\.foo\.com CNAME /) + expect(page).to have_field('serverless_domain_verification', with: /^_gitlab-pages-verification-code.foo.com TXT /) + expect(page).not_to have_field('pages_domain[user_provided_certificate]') + expect(page).not_to have_field('pages_domain[user_provided_key]') + + expect(page).to have_content 'Unverified' + expect(page).to have_content '/CN=test-certificate' + end + + it 'Update domain certificate' do + visit admin_serverless_domains_path + + fill_in 'pages_domain[domain]', with: 'foo.com' + fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate + fill_in 'pages_domain[user_provided_key]', with: sample_domain.key + click_button 'Add domain' + + expect(current_path).to eq admin_serverless_domains_path + + expect(page).not_to have_field('pages_domain[user_provided_certificate]') + expect(page).not_to have_field('pages_domain[user_provided_key]') + + click_button 'Replace' + + expect(page).to have_field('pages_domain[user_provided_certificate]') + expect(page).to have_field('pages_domain[user_provided_key]') + + fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate + fill_in 'pages_domain[user_provided_key]', with: sample_domain.key + + click_button 'Save changes' + + expect(page).to have_content 'Domain was successfully updated' + expect(page).to have_content '/CN=test-certificate' + end +end diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json index ac40f2dcd13..038f5ac5d4e 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json @@ -16,7 +16,8 @@ "label": { "type": "string" }, "track": { "type": "string" }, "prometheus_endpoint_path": { "type": "string" }, - "metric_id": { "type": "number" } + "metric_id": { "type": "number" }, + "edit_path": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json index f4afb4cbffc..d16fcd40359 100644 --- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json @@ -10,7 +10,8 @@ "panels": { "type": "array", "items": { "$ref": "panels.json" } - } + }, + "has_custom_metrics": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 4a0eab3ea27..b99f311de29 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -394,6 +394,12 @@ describe Gitlab::Database do expect(described_class.cached_table_exists?(:bogus_table_name)).to be_falsey end end + + it 'returns false when database does not exist' do + expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, 'broken' } + + expect(described_class.cached_table_exists?(:projects)).to be(false) + end end describe '.exists?' do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 5e1d6199c3c..5c36d6d35af 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -281,6 +281,19 @@ describe Gitlab::GitalyClient::CommitService do end describe '#find_commits' do + it 'sends an RPC request with NONE when default' do + request = Gitaly::FindCommitsRequest.new( + repository: repository_message, + disable_walk: true, + order: 'NONE' + ) + + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commits) + .with(request, kind_of(Hash)).and_return([]) + + client.find_commits(order: 'default') + end + it 'sends an RPC request' do request = Gitaly::FindCommitsRequest.new( repository: repository_message, diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb index 8c3942c7f7d..e8860d50437 100644 --- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -12,6 +12,7 @@ describe Gitlab::Metrics::Dashboard::Processor do [ Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, Gitlab::Metrics::Dashboard::Stages::ProjectMetricsInserter, + Gitlab::Metrics::Dashboard::Stages::ProjectMetricsDetailsInserter, Gitlab::Metrics::Dashboard::Stages::EndpointInserter, Gitlab::Metrics::Dashboard::Stages::Sorter ] @@ -25,6 +26,10 @@ describe Gitlab::Metrics::Dashboard::Processor do end end + it 'includes boolean to indicate if panel group has custom metrics' do + expect(dashboard[:panel_groups]).to all(include( { has_custom_metrics: boolean } )) + end + context 'when the dashboard is not present' do let(:dashboard_yml) { nil } @@ -145,7 +150,8 @@ describe Gitlab::Metrics::Dashboard::Processor do unit: metric.unit, label: metric.legend, metric_id: metric.id, - prometheus_endpoint_path: prometheus_path(metric.query) + prometheus_endpoint_path: prometheus_path(metric.query), + edit_path: edit_metric_path(metric) } end @@ -165,4 +171,11 @@ describe Gitlab::Metrics::Dashboard::Processor do identifier: metric ) end + + def edit_metric_path(metric) + Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path( + project, + metric.id + ) + end end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 99b7c4f148a..d2a54c3eea7 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -104,6 +104,14 @@ describe PagesDomain do describe 'validate certificate' do subject { domain } + context 'serverless domain' do + it 'requires certificate and key to be present' do + expect(build(:pages_domain, :without_certificate, :without_key, usage: :serverless)).not_to be_valid + expect(build(:pages_domain, :without_certificate, usage: :serverless)).not_to be_valid + expect(build(:pages_domain, :without_key, usage: :serverless)).not_to be_valid + end + end + context 'with matching key' do let(:domain) { build(:pages_domain) } @@ -555,6 +563,28 @@ describe PagesDomain do end end + describe '.instance_serverless' do + subject { described_class.instance_serverless } + + before do + create(:pages_domain, wildcard: true) + create(:pages_domain, :instance_serverless) + create(:pages_domain, scope: :instance) + create(:pages_domain, :instance_serverless) + create(:pages_domain, usage: :serverless) + end + + it 'returns domains that are wildcard, instance-level, and serverless' do + expect(subject.length).to eq(2) + + subject.each do |domain| + expect(domain.wildcard).to eq(true) + expect(domain.usage).to eq('serverless') + expect(domain.scope).to eq('instance') + end + end + end + describe '.need_auto_ssl_renewal' do subject { described_class.need_auto_ssl_renewal } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 170b9ccccf8..c179de249d5 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -266,8 +266,30 @@ describe API::Commits do end end - context 'set to blank' do - let(:order) { '' } + context 'set to default' do + let(:order) { 'default' } + + # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876 + # * 0031876 + # |\ + # * | bf6e164 + # | * 48ca272 + # * | 9d526f8 + # | * 335bc94 + # |/ + # * 1039376 + it 'returns project commits ordered by default order' do + commits = project.repository.commits("0031876", limit: 6, order: 'default') + + get api(route, current_user) + + expect(json_response.size).to eq(6) + expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) + end + end + + context 'set to an invalid parameter' do + let(:order) { 'invalid' } it_behaves_like '400 response' do let(:request) { get api(route, current_user) } diff --git a/spec/routing/admin/serverless/domains_controller_routing_spec.rb b/spec/routing/admin/serverless/domains_controller_routing_spec.rb new file mode 100644 index 00000000000..18c0db6add1 --- /dev/null +++ b/spec/routing/admin/serverless/domains_controller_routing_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::Serverless::DomainsController do + it 'routes to #index' do + expect(get: '/admin/serverless/domains').to route_to('admin/serverless/domains#index') + end + + it 'routes to #create' do + expect(post: '/admin/serverless/domains/').to route_to('admin/serverless/domains#create') + end + + it 'routes to #update' do + expect(put: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1') + expect(patch: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1') + end + + it 'routes #verify' do + expect(post: '/admin/serverless/domains/1/verify').to route_to(controller: 'admin/serverless/domains', action: 'verify', id: '1') + end +end diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb index 1cf1cff51ed..a1cbec6748a 100644 --- a/spec/services/snippets/create_service_spec.rb +++ b/spec/services/snippets/create_service_spec.rb @@ -164,7 +164,9 @@ describe Snippets::CreateService do it 'does not create snippet repository' do stub_feature_flags(version_snippets: false) - subject + expect do + subject + end.to change(Snippet, :count).by(1) expect(snippet.repository_exists?).to be_falsey end -- GitLab