diff options
Diffstat (limited to 'app')
20 files changed, 293 insertions, 42 deletions
diff --git a/app/assets/javascripts/maintenance_mode_settings/components/app.vue b/app/assets/javascripts/maintenance_mode_settings/components/app.vue new file mode 100644 index 00000000000..47150c9dc95 --- /dev/null +++ b/app/assets/javascripts/maintenance_mode_settings/components/app.vue @@ -0,0 +1,44 @@ +<script> +import { GlToggle, GlFormGroup, GlFormTextarea, GlButton } from '@gitlab/ui'; + +export default { + name: 'MaintenanceModeSettingsApp', + components: { + GlToggle, + GlFormGroup, + GlFormTextarea, + GlButton, + }, + data() { + return { + inMaintenanceMode: false, + bannerMessage: '', + }; + }, +}; +</script> +<template> + <article> + <div class="d-flex align-items-center mb-3"> + <gl-toggle v-model="inMaintenanceMode" class="mb-0" /> + <div class="ml-2"> + <p class="mb-0">{{ __('Enable maintenance mode') }}</p> + <p class="mb-0 text-secondary-500"> + {{ + __('Non-admin users can sign in with read-only access and make read-only API requests.') + }} + </p> + </div> + </div> + <gl-form-group label="Banner Message" label-for="maintenanceBannerMessage"> + <gl-form-textarea + id="maintenanceBannerMessage" + v-model="bannerMessage" + :placeholder="__(`GitLab is undergoing maintenance and is operating in a read-only mode.`)" + /> + </gl-form-group> + <div class="mt-4"> + <gl-button variant="success">{{ __('Save changes') }}</gl-button> + </div> + </article> +</template> diff --git a/app/assets/javascripts/maintenance_mode_settings/index.js b/app/assets/javascripts/maintenance_mode_settings/index.js new file mode 100644 index 00000000000..7a80233faf0 --- /dev/null +++ b/app/assets/javascripts/maintenance_mode_settings/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import MaintenanceModeSettingsApp from './components/app.vue'; + +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-maintenance-mode-settings'); + + return new Vue({ + el, + components: { + MaintenanceModeSettingsApp, + }, + + render(createElement) { + return createElement('maintenance-mode-settings-app'); + }, + }); +}; diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 78b7e29ae53..493c216cc6e 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,9 +1,11 @@ import initSettingsPanels from '~/settings_panels'; import projectSelect from '~/project_select'; import selfMonitor from '~/self_monitor'; +import maintenanceModeSettings from '~/maintenance_mode_settings'; document.addEventListener('DOMContentLoaded', () => { selfMonitor(); + maintenanceModeSettings(); // Initialize expandable settings panels initSettingsPanels(); projectSelect(); diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index dadb20e511b..12e16b79d37 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -37,7 +37,8 @@ export default { text() { return sprintf( s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. - Existing project labels with the same title will be merged. This action cannot be reversed.`), + Existing project labels with the same title will be merged. If a group label with the same title exists, + it will also be merged. This action cannot be reversed.`), { labelTitle: this.labelTitle, groupName: this.groupName, diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 4d8ae8a5652..0aae2f20671 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -483,6 +483,34 @@ img.emoji { } /** COMMON SPACING CLASSES **/ +/** + 🚨 Do not use these classes — they are deprecated and being removed. 🚨 + See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. + + Instead, if you need a spacing class, add it below using the following values. + $gl-spacing-scale-0: 0; + $gl-spacing-scale-1: 2px; + $gl-spacing-scale-2: 4px; + $gl-spacing-scale-3: 8px; + $gl-spacing-scale-4: 12px; + $gl-spacing-scale-5: 16px; + $gl-spacing-scale-6: 24px; + $gl-spacing-scale-7: 32px; + $gl-spacing-scale-8: 40px; + $gl-spacing-scale-9: 48px; + $gl-spacing-scale-10: 56px; + $gl-spacing-scale-11: 64px; + $gl-spacing-scale-12: 80px; + $gl-spacing-scale-13: 96px; + + E.g., a padding top of 96px can be added using: + .gl-shim-pt-13 { + padding-top: 96px; + } + + Please use -shim- so it can be differentiated from the old scale classes. + These will be replaced when the Gitlab UI utilities are included. +**/ @each $index, $padding in $spacing-scale { #{'.gl-p-#{$index}'} { padding: $padding; } #{'.gl-pl-#{$index}'} { padding-left: $padding; } @@ -583,13 +611,11 @@ img.emoji { .gl-font-size-large { font-size: $gl-font-size-large; } .gl-line-height-24 { line-height: $gl-line-height-24; } -.gl-line-height-14 { line-height: $gl-line-height-14; } .gl-font-size-0 { font-size: 0; } .gl-font-size-12 { font-size: $gl-font-size-12; } .gl-font-size-14 { font-size: $gl-font-size-14; } .gl-font-size-16 { font-size: $gl-font-size-16; } -.gl-font-size-20 { font-size: $gl-font-size-20; } .gl-font-size-28 { font-size: $gl-font-size-28; } .gl-font-size-42 { font-size: $gl-font-size-42; } @@ -599,3 +625,10 @@ img.emoji { border-top: 1px solid $border-color; } + +/** +🚨 Do not use these classes — they clash with the Gitlab UI design system and will be removed. 🚨 +See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. +**/ +.gl-line-height-14 { line-height: $gl-line-height-14; } +.gl-font-size-20 { font-size: $gl-font-size-20; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4d858f88921..86b65ea34f3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -11,6 +11,38 @@ $default-transition-duration: 0.15s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; $toggle-sidebar-height: 48px; + +/** + 🚨 Do not use this spacing scale — it is deprecated and being removed. 🚨 + See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. + + Instead, if you need a spacing class, add it to app/assets/stylesheets/framework/common.scss, + using the following values. + + $gl-spacing-scale-0: 0; + $gl-spacing-scale-1: 2px; + $gl-spacing-scale-2: 4px; + $gl-spacing-scale-3: 8px; + $gl-spacing-scale-4: 12px; + $gl-spacing-scale-5: 16px; + $gl-spacing-scale-6: 24px; + $gl-spacing-scale-7: 32px; + $gl-spacing-scale-8: 40px; + $gl-spacing-scale-9: 48px; + $gl-spacing-scale-10: 56px; + $gl-spacing-scale-11: 64px; + $gl-spacing-scale-12: 80px; + $gl-spacing-scale-13: 96px; + + E.g., a padding top of 96px can be added using: + .gl-shim-pt-13 { + padding-top: 96px; + } + + Please use -shim- so it can be differentiated from the old scale classes. + + These will be replaced when the Gitlab UI utilities are included. +**/ $spacing-scale: ( 0: 0, 1: #{0.5 * $grid-size}, diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index f161d76c623..925c15262e9 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -67,7 +67,8 @@ .gl-bg-purple-light { background-color: $purple-light; } // Classes using mixins coming from @gitlab-ui -// can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged +// can be removed once the mixins are added. +// See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. .gl-bg-blue-50 { @include gl-bg-blue-50; } .gl-bg-red-100 { @include gl-bg-red-100; } .gl-bg-orange-100 { @include gl-bg-orange-100; } diff --git a/app/controllers/concerns/sends_blob.rb b/app/controllers/concerns/sends_blob.rb index 8ecdaced9f5..9bba61fda84 100644 --- a/app/controllers/concerns/sends_blob.rb +++ b/app/controllers/concerns/sends_blob.rb @@ -8,16 +8,16 @@ module SendsBlob include SendFileUpload end - def send_blob(repository, blob, params = {}) + def send_blob(repository, blob, inline: true, allow_caching: false) if blob headers['X-Content-Type-Options'] = 'nosniff' - return if cached_blob?(blob) + return if cached_blob?(blob, allow_caching: allow_caching) if blob.stored_externally? - send_lfs_object(blob) + send_lfs_object(blob, repository.project) else - send_git_blob(repository, blob, params) + send_git_blob(repository, blob, inline: inline) end else render_404 @@ -26,11 +26,11 @@ module SendsBlob private - def cached_blob?(blob) + def cached_blob?(blob, allow_caching: false) stale = stale?(etag: blob.id) # The #stale? method sets cache headers. # Because we are opinionated we set the cache headers ourselves. - response.cache_control[:public] = project.public? + response.cache_control[:public] = allow_caching response.cache_control[:max_age] = if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -48,7 +48,7 @@ module SendsBlob !stale end - def send_lfs_object(blob) + def send_lfs_object(blob, project) lfs_object = find_lfs_object(blob) if lfs_object && lfs_object.project_allowed_access?(project) diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 749afb71923..b12aee346ed 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -2,6 +2,7 @@ module SnippetsActions extend ActiveSupport::Concern + include SendsBlob def edit # We need to load some info from the existing blob @@ -12,16 +13,26 @@ module SnippetsActions end def raw - disposition = params[:inline] == 'false' ? 'attachment' : 'inline' - workhorse_set_content_type! - send_data( - convert_line_endings(blob.data), - type: 'text/plain; charset=utf-8', - disposition: disposition, - filename: Snippet.sanitized_file_name(blob.name) - ) + # Until we don't migrate all snippets to version + # snippets we need to support old `SnippetBlob` + # blobs + if defined?(blob.snippet) + send_data( + convert_line_endings(blob.data), + type: 'text/plain; charset=utf-8', + disposition: content_disposition, + filename: Snippet.sanitized_file_name(blob.name) + ) + else + send_blob( + snippet.repository, + blob, + inline: content_disposition == 'inline', + allow_caching: snippet.public? + ) + end end def js_request? @@ -30,6 +41,10 @@ module SnippetsActions private + def content_disposition + @disposition ||= params[:inline] == 'false' ? 'attachment' : 'inline' + end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def blob return unless snippet diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 1f4a25f82e9..6e6bf09a32a 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController def show @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) - send_blob(@repository, @blob) + send_blob(@repository, @blob, allow_caching: @project.public?) end def destroy diff --git a/app/controllers/projects/import/jira_controller.rb b/app/controllers/projects/import/jira_controller.rb index d38d9e27347..e88d28e9db4 100644 --- a/app/controllers/projects/import/jira_controller.rb +++ b/app/controllers/projects/import/jira_controller.rb @@ -16,9 +16,8 @@ module Projects end def import - import_state = @project.import_state || @project.create_import_state - - schedule_import(jira_import_params) unless import_state.in_progress? + response = ::JiraImport::StartImportService.new(current_user, @project, jira_import_params[:jira_project_key]).execute + flash[:notice] = response.message if response.message.present? redirect_to project_import_jira_path(@project) end @@ -39,21 +38,6 @@ module Projects redirect_to project_issues_path(@project) end - def schedule_import(params) - import_data = @project.create_or_update_import_data(data: {}).becomes(JiraImportData) - - jira_project_details = JiraImportData::JiraProjectDetails.new( - params[:jira_project_key], - Time.now.strftime('%Y-%m-%d %H:%M:%S'), - { user_id: current_user.id, name: current_user.name } - ) - import_data << jira_project_details - import_data.force_import! - - @project.import_type = 'jira' - @project.import_state.schedule if @project.save - end - def jira_import_params params.permit(:jira_project_key) end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index f7bc6898112..69a3898af55 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -17,7 +17,7 @@ class Projects::RawController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - send_blob(@repository, @blob, inline: (params[:inline] != 'false')) + send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: @project.public?) end private diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index cfc0925d9e1..90ff798077a 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController render 'show' elsif file_blob - send_blob(@project_wiki.repository, file_blob) + send_blob(@project_wiki.repository, file_blob, allow_caching: @project.public?) elsif show_create_form? # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new title = params[:id] unless params[:random_title].present? diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index b60fc96bd03..d48cc868434 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -51,6 +51,9 @@ module Types description: "Timestamp of the note's resolution" field :position, Types::Notes::DiffPositionType, null: true, description: 'The position of this note on a diff' + field :confidential, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if this note is confidential', + method: :confidential? end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 3f409b8bb22..690aa978716 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -70,6 +70,16 @@ module Ci joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) } + scope :belonging_to_group, -> (group_id, include_ancestors: false) { + groups = ::Group.where(id: group_id) + + if include_ancestors + groups = Gitlab::ObjectHierarchy.new(groups).base_and_ancestors + end + + joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) + } + scope :belonging_to_parent_group_of_project, -> (project_id) { project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index b2d9d52bd22..e302672042e 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -48,6 +48,7 @@ class DiscussionEntity < Grape::Entity expose :for_commit?, as: :for_commit expose :commit_id + expose :confidential?, as: :confidential private diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb new file mode 100644 index 00000000000..91a7956e585 --- /dev/null +++ b/app/services/jira_import/start_import_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module JiraImport + class StartImportService + attr_reader :user, :project, :jira_project_key + + def initialize(user, project, jira_project_key) + @user = user + @project = project + @jira_project_key = jira_project_key + end + + def execute + validation_response = validate + return validation_response if validation_response&.error? + + create_and_schedule_import + end + + private + + def create_and_schedule_import + import_data = project.create_or_update_import_data(data: {}).becomes(JiraImportData) + jira_project_details = JiraImportData::JiraProjectDetails.new( + jira_project_key, + Time.now.strftime('%Y-%m-%d %H:%M:%S'), + { user_id: user.id, name: user.name } + ) + import_data << jira_project_details + import_data.force_import! + + project.import_type = 'jira' + project.import_state.schedule if project.save! + + ServiceResponse.success(payload: { import_data: import_data } ) + rescue => ex + # in case project.save! raises an erorr + Gitlab::ErrorTracking.track_exception(ex, project_id: project.id) + build_error_response(ex.message) + end + + def validate + return build_error_response(_('Jira import feature is disabled.')) unless Feature.enabled?(:jira_issue_import, project) + return build_error_response(_('You do not have permissions to run the import.')) unless user.can?(:admin_project, project) + return build_error_response(_('Jira integration not configured.')) unless project.jira_service&.active? + return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank? + return build_error_response(_('Jira import is already running.')) if import_in_progress? + end + + def build_error_response(message) + import_data = JiraImportData.new(project: project) + import_data.errors.add(:base, message) + ServiceResponse.error( + message: import_data.errors.full_messages.to_sentence, + http_status: 400, + payload: { import_data: import_data } + ) + end + + def import_in_progress? + import_state = project.import_state || project.create_import_state + import_state.in_progress? + end + end +end diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb index 58f795e639e..93445dd4ddd 100644 --- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb +++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb @@ -36,8 +36,8 @@ module PagesDomains when 'valid' save_certificate(acme_order.private_key, api_order) acme_order.destroy! - # when 'invalid' - # TODO: implement error handling + when 'invalid' + save_order_error(acme_order, api_order) end end @@ -47,5 +47,28 @@ module PagesDomains certificate = api_order.certificate pages_domain.update!(gitlab_provided_key: private_key, gitlab_provided_certificate: certificate) end + + def save_order_error(acme_order, api_order) + log_error(api_order) + + return unless Feature.enabled?(:pages_letsencrypt_errors, pages_domain.project) + + pages_domain.assign_attributes(auto_ssl_failed: true) + pages_domain.save!(validate: false) + + acme_order.destroy! + end + + def log_error(api_order) + Gitlab::AppLogger.error( + message: "Failed to obtain Let's Encrypt certificate", + acme_error: api_order.challenge_error, + project_id: pages_domain.project_id, + pages_domain: pages_domain.domain + ) + rescue => e + # getting authorizations is an additional network request which can raise errors + Gitlab::ErrorTracking.track_exception(e) + end end end diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 1a029996aaf..bebda385886 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -104,6 +104,18 @@ = f.submit _('Save changes'), class: "btn btn-success" +- if Feature.enabled?(:maintenance_mode) + %section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Maintenance mode') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Prevent users from performing write operations on GitLab while performing maintenance.') + .settings-content + #js-maintenance-mode-settings + - if Feature.enabled?(:instance_level_integrations) = render_if_exists 'admin/application_settings/elasticsearch_form' = render 'admin/application_settings/plantuml' diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb index fe6d516d3cf..43fb35c5298 100644 --- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb +++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb @@ -10,6 +10,11 @@ class PagesDomainSslRenewalCronWorker # rubocop:disable Scalability/IdempotentWo return unless ::Gitlab::LetsEncrypt.enabled? PagesDomain.need_auto_ssl_renewal.with_logging_info.find_each do |domain| + # Ideally that should be handled in PagesDomain.need_auto_ssl_renewal scope + # but it's hard to make scope work with feature flags + # once we remove feature flag we can modify scope to implement this behaviour + next if Feature.enabled?(:pages_letsencrypt_errors, domain.project) && domain.auto_ssl_failed + with_context(project: domain.project) do PagesDomainSslRenewalWorker.perform_async(domain.id) end |