diff options
Diffstat (limited to 'app')
71 files changed, 970 insertions, 422 deletions
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 9a0442e2afe..6637904d87d 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,6 +1,6 @@ <script> import Sortable from 'vendor/Sortable'; -import boardNewIssue from './board_new_issue'; +import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.vue index bc28f7f45f4..efface7143d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ -/* global ListIssue */ +<script> import eventHub from '../eventhub'; +import ListIssue from '../models/issue'; const Store = gl.issueBoards.BoardsStore; @@ -17,6 +18,9 @@ export default { error: false, }; }, + mounted() { + this.$refs.input.focus(); + }, methods: { submit(e) { e.preventDefault(); @@ -59,42 +63,51 @@ export default { eventHub.$emit(`hide-issue-form-${this.list.id}`); }, }, - mounted() { - this.$refs.input.focus(); - }, - template: ` - <div class="card board-new-issue-form"> - <form @submit="submit($event)"> - <div class="flash-container" - v-if="error"> - <div class="flash-alert"> - An error occurred. Please try again. - </div> - </div> - <label class="label-light" - :for="list.id + '-title'"> - Title - </label> - <input class="form-control" - type="text" - v-model="title" - ref="input" - autocomplete="off" - :id="list.id + '-title'" /> - <div class="clearfix prepend-top-10"> - <button class="btn btn-success pull-left" - type="submit" - :disabled="title === ''" - ref="submit-button"> - Submit issue - </button> - <button class="btn btn-default pull-right" - type="button" - @click="cancel"> - Cancel - </button> - </div> - </form> - </div> - `, }; +</script> + +<template> + <div class="card board-new-issue-form"> + <form @submit="submit($event)"> + <div + class="flash-container" + v-if="error" + > + <div class="flash-alert"> + An error occurred. Please try again. + </div> + </div> + <label + class="label-light" + :for="list.id + '-title'" + > + Title + </label> + <input + class="form-control" + type="text" + v-model="title" + ref="input" + autocomplete="off" + :id="list.id + '-title'" + /> + <div class="clearfix prepend-top-10"> + <button + class="btn btn-success pull-left" + type="submit" + :disabled="title === ''" + ref="submit-button" + > + Submit issue + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="cancel" + > + Cancel + </button> + </div> + </form> + </div> +</template> diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/index.js index 90166b3d3d1..06a8abea940 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/index.js @@ -24,7 +24,7 @@ import './components/new_list_dropdown'; import './components/modal/index'; import '../vue_shared/vue_resource_interceptor'; -$(() => { +export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; @@ -236,4 +236,4 @@ $(() => { </div> `, }); -}); +}; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 81edd95bf2b..3bfb6d39ad5 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -110,3 +110,5 @@ class ListIssue { } window.ListIssue = ListIssue; + +export default ListIssue; diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index ca8798facc9..b727261648c 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import deployKeysApp from './components/app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: document.getElementById('js-deploy-keys'), components: { deployKeysApp, @@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3f14b8340e4..95ba00bb49a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -42,31 +42,34 @@ var Dispatcher; }); }); - switch (page) { - case 'projects:merge_requests:index': - case 'projects:issues:index': - case 'projects:issues:show': - case 'projects:issues:new': - case 'projects:issues:edit': - case 'projects:merge_requests:creations:new': - case 'projects:merge_requests:creations:diffs': - case 'projects:merge_requests:edit': - case 'projects:merge_requests:show': - case 'projects:commit:show': - case 'projects:activity': - case 'projects:commits:show': - case 'projects:show': - case 'groups:show': - case 'projects:tree:show': - case 'projects:find_file:show': - case 'projects:blob:show': - case 'projects:blame:show': - case 'projects:network:show': - case 'projects:artifacts:browse': - case 'projects:artifacts:file': - shortcut_handler = true; - break; + const shortcutHandlerPages = [ + 'projects:activity', + 'projects:artifacts:browse', + 'projects:artifacts:file', + 'projects:blame:show', + 'projects:blob:show', + 'projects:commit:show', + 'projects:commits:show', + 'projects:find_file:show', + 'projects:issues:edit', + 'projects:issues:index', + 'projects:issues:new', + 'projects:issues:show', + 'projects:merge_requests:creations:diffs', + 'projects:merge_requests:creations:new', + 'projects:merge_requests:edit', + 'projects:merge_requests:index', + 'projects:merge_requests:show', + 'projects:network:show', + 'projects:show', + 'projects:tree:show', + 'groups:show', + ]; + + if (shortcutHandlerPages.indexOf(page) !== -1) { + shortcut_handler = true; } + switch (path[0]) { case 'admin': switch (path[1]) { diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js deleted file mode 100644 index b693084e434..00000000000 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ /dev/null @@ -1,102 +0,0 @@ -import eventHub from '../event_hub'; -import FilteredSearchTokenizer from '../filtered_search_tokenizer'; - -export default { - name: 'RecentSearchesDropdownContent', - - props: { - items: { - type: Array, - required: true, - }, - isLocalStorageAvailable: { - type: Boolean, - required: false, - default: true, - }, - allowedKeys: { - type: Array, - required: true, - }, - }, - - computed: { - processedItems() { - return this.items.map((item) => { - const { tokens, searchToken } - = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); - - const resultantTokens = tokens.map(token => ({ - prefix: `${token.key}:`, - suffix: `${token.symbol}${token.value}`, - })); - - return { - text: item, - tokens: resultantTokens, - searchToken, - }; - }); - }, - hasItems() { - return this.items.length > 0; - }, - }, - - methods: { - onItemActivated(text) { - eventHub.$emit('recentSearchesItemSelected', text); - }, - onRequestClearRecentSearches(e) { - // Stop the dropdown from closing - e.stopPropagation(); - - eventHub.$emit('requestClearRecentSearches'); - }, - }, - - template: ` - <div> - <div - v-if="!isLocalStorageAvailable" - class="dropdown-info-note"> - This feature requires local storage to be enabled - </div> - <ul v-else-if="hasItems"> - <li - v-for="(item, index) in processedItems" - :key="index"> - <button - type="button" - class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text)"> - <span> - <span - v-for="(token, tokenIndex) in item.tokens" - class="filtered-search-history-dropdown-token"> - <span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> - </span> - </span> - <span class="filtered-search-history-dropdown-search-token"> - {{ item.searchToken }} - </span> - </button> - </li> - <li class="divider"></li> - <li> - <button - type="button" - class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event)"> - Clear recent searches - </button> - </li> - </ul> - <div - v-else - class="dropdown-info-note"> - You don't have any recent searches - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue new file mode 100644 index 00000000000..26618af9515 --- /dev/null +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -0,0 +1,104 @@ +<script> +import eventHub from '../event_hub'; +import FilteredSearchTokenizer from '../filtered_search_tokenizer'; + +export default { + name: 'RecentSearchesDropdownContent', + props: { + items: { + type: Array, + required: true, + }, + isLocalStorageAvailable: { + type: Boolean, + required: false, + default: true, + }, + allowedKeys: { + type: Array, + required: true, + }, + }, + computed: { + processedItems() { + return this.items.map((item) => { + const { tokens, searchToken } + = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); + + const resultantTokens = tokens.map(token => ({ + prefix: `${token.key}:`, + suffix: `${token.symbol}${token.value}`, + })); + + return { + text: item, + tokens: resultantTokens, + searchToken, + }; + }); + }, + hasItems() { + return this.items.length > 0; + }, + }, + methods: { + onItemActivated(text) { + eventHub.$emit('recentSearchesItemSelected', text); + }, + onRequestClearRecentSearches(e) { + // Stop the dropdown from closing + e.stopPropagation(); + + eventHub.$emit('requestClearRecentSearches'); + }, + }, +}; +</script> +<template> + <div> + <div + v-if="!isLocalStorageAvailable" + class="dropdown-info-note"> + This feature requires local storage to be enabled + </div> + <ul v-else-if="hasItems"> + <li + v-for="(item, index) in processedItems" + :key="`processed-items-${index}`" + > + <button + type="button" + class="filtered-search-history-dropdown-item" + @click="onItemActivated(item.text)"> + <span> + <span + class="filtered-search-history-dropdown-token" + v-for="(token, index) in item.tokens" + :key="`dropdown-token-${index}`" + > + <span class="name">{{ token.prefix }}</span> + <span class="value">{{ token.suffix }}</span> + </span> + </span> + <span class="filtered-search-history-dropdown-search-token"> + {{ item.searchToken }} + </span> + </button> + </li> + <li class="divider"></li> + <li> + <button + type="button" + class="filtered-search-history-clear-button" + @click="onRequestClearRecentSearches($event)"> + Clear recent searches + </button> + </li> + </ul> + <div + v-else + class="dropdown-info-note"> + You don't have any recent searches + </div> + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index c99ed63c4af..f9338b82acf 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; +import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue'; import eventHub from './event_hub'; class RecentSearchesRoot { @@ -33,7 +33,7 @@ class RecentSearchesRoot { this.vm = new Vue({ el: this.wrapperElement, components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + RecentSearchesDropdownContent, }, data() { return state; }, template: ` diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 5de48aa49a9..7151ac05a09 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -213,7 +213,7 @@ export default class LabelsSelect { } } if (label.duplicate) { - color = gl.DropdownUtils.duplicateLabelColor(label.color); + color = DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 3aeeedbb45d..5cfe8723204 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,7 +1,9 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new + initBoards(); }); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index db064e3f801..1e56aa58da2 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -3,6 +3,7 @@ import Issue from '~/issue'; import ShortcutsIssuable from '~/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; +import '~/issue_show/index'; document.addEventListener('DOMContentLoaded', () => { new Issue(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index d88527351c1..5a6f4138b10 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,7 @@ import initSettingsPanels from '~/settings_panels'; +import initDeployKeys from '~/deploy_keys'; -document.addEventListener('DOMContentLoaded', initSettingsPanels); +document.addEventListener('DOMContentLoaded', () => { + initDeployKeys(); + initSettingsPanels(); +}); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 689befc742e..14545824e74 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { super(); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); - this.editBtn = document.querySelector('.js-issuable-edit'); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); - Mousetrap.bind('e', this.editIssue.bind(this)); + Mousetrap.bind('e', ShortcutsIssuable.editIssue); if (isMergeRequest) { this.enabledHelp.push('.hidden-shortcut.merge_requests'); @@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { return false; } - editIssue() { + static editIssue() { // Need to click the element as on issues, editing is inline // on merge request, editing is on a different page - this.editBtn.click(); + document.querySelector('.js-issuable-edit').click(); return false; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 7ba6c29006a..162f048aac7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -227,7 +227,8 @@ export default { @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" - type="button"> + type="button" + class="qa-merge-button"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index e9f23b0b113..143fd328d88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children" > <button type="button" - class="btn btn-sm btn-reopen btn-success" + class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" :disabled="isMakingRequest" @click="rebase" > diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6763af4e98b..b9390450477 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -13,10 +13,20 @@ display: inline-block; } + .issuable-meta { + .author_link { + display: inline-block; + } + + .issuable-comments { + height: 18px; + } + } + .icon-merge-request-unmerged { height: 13px; margin-bottom: 3px; - } + } } } diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 0f41af7d87b..6b0b22f8e73 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController def verify_billing case google_project_billing_status when nil - flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') + flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') when false - flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } + flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } when true return end diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index 15e77d854dc..b71f1e5fef4 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController before_action :require_pages_enabled! before_action :authorize_update_pages!, except: [:show] - before_action :domain, only: [:show, :destroy] + before_action :domain, only: [:show, :destroy, :verify] def show end @@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController @domain = @project.pages_domains.new 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 project_pages_domain_path(@project, @domain) + end + def create @domain = @project.pages_domains.create(pages_domain_params) if @domain.valid? - redirect_to project_pages_path(@project) + redirect_to project_pages_domain_path(@project, @domain) else render 'new' end @@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def domain - @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) + @domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s) end end diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb new file mode 100644 index 00000000000..b739d0f0f90 --- /dev/null +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -0,0 +1,27 @@ +module Projects + module Prometheus + class MetricsController < Projects::ApplicationController + before_action :authorize_admin_project! + + def active_common + respond_to do |format| + format.json do + matched_metrics = prometheus_service.matched_metrics || {} + + if matched_metrics.any? + render json: matched_metrics + else + head :no_content + end + end + end + end + + private + + def prometheus_service + @prometheus_service ||= project.find_or_initialize_service('prometheus') + end + end + end +end diff --git a/app/controllers/projects/prometheus_controller.rb b/app/controllers/projects/prometheus_controller.rb deleted file mode 100644 index 507468d7102..00000000000 --- a/app/controllers/projects/prometheus_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -class Projects::PrometheusController < Projects::ApplicationController - before_action :authorize_read_project! - before_action :require_prometheus_metrics! - - def active_metrics - respond_to do |format| - format.json do - matched_metrics = project.prometheus_service.matched_metrics || {} - - if matched_metrics.any? - render json: matched_metrics - else - head :no_content - end - end - end - end - - private - - def require_prometheus_metrics! - render_404 unless project.prometheus_service.present? - end -end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index f013e177c5b..5c9fce211ec 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder end end elsif only_group_labels? - label_ids << Label.where(group_id: group.id) + label_ids << Label.where(group_id: group_ids) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group - strong_memoize(:group) do + def group_ids + strong_memoize(:group_ids) do group = Group.find(params[:group_id]) - authorized_to_read_labels?(group) && group + groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group] + groups_user_can_read_labels(groups).map(&:id) end end @@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder Ability.allowed?(current_user, :read_label, label_parent) end + + def groups_user_can_read_labels(groups) + DeclarativePolicy.user_scope do + groups.select { |group| authorized_to_read_labels?(group) } + end + end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e293b3ef329..ab68ecad2ba 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -199,6 +199,7 @@ module ApplicationSettingsHelper :metrics_port, :metrics_sample_interval, :metrics_timeout, + :pages_domain_verification_enabled, :password_authentication_enabled_for_web, :password_authentication_enabled_for_git, :performance_bar_allowed_group_id, diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a6e1de6ffdc..0e806d16bc5 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -12,75 +12,42 @@ module BlobHelper def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) project_edit_blob_path(project, - tree_join(ref, path), - options[:link_opts]) - end - - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil - - return unless blob && blob.readable_text? - - common_classes = "btn js-edit-blob #{options[:extra_class]}" - - if !on_top_of_branch?(project, ref) - button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) - link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: edit_blob_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag 'Edit', - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: 'edit', fork_path: fork_path } - end + tree_join(ref, path), + options[:link_opts]) end def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" end - def ide_edit_text - "#{_('Web IDE')}" - end + def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) - def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) - return unless show_new_ide? + common_classes = "btn js-edit-blob #{options[:extra_class]}" - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil + edit_button_tag(blob, + common_classes, + _('Edit'), + edit_blob_path(project, ref, path, options), + project, + ref) + end - return unless blob && blob.readable_text? + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless show_new_ide? + return unless blob = readable_blob(options, path, project, ref) common_classes = "btn js-edit-ide #{options[:extra_class]}" - if !on_top_of_branch?(project, ref) - button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif current_user && can_modify_blob?(blob, project, ref) - link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: ide_edit_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag ide_edit_text, - class: common_classes, - data: { fork_path: fork_path } - end + edit_button_tag(blob, + common_classes, + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) end - def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user blob = project.repository.blob_at(ref, path) rescue nil @@ -96,21 +63,12 @@ module BlobHelper elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) - continue_params = { - to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to #{action} this file again.", - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag label, - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: action, fork_path: fork_path } + edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end def replace_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -122,7 +80,7 @@ module BlobHelper end def delete_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -332,4 +290,55 @@ module BlobHelper options end + + def readable_blob(options, path, project, ref) + blob = options.delete(:blob) + blob ||= project.repository.blob_at(ref, path) rescue nil + + blob if blob&.readable_text? + end + + def edit_blob_fork_params(path) + { + to: path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_modify_file_fork_params(action) + { + to: request.fullpath, + notice: edit_in_new_fork_notice_action(action), + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_fork_button_tag(common_classes, project, label, params, action = 'edit') + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params) + + button_tag label, + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: action, fork_path: fork_path } + end + + def edit_disabled_button_tag(button_text, common_classes) + button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) + end + + def edit_link_tag(link_text, edit_path, common_classes) + link_to link_text, edit_path, class: "#{common_classes} btn-sm" + end + + def edit_button_tag(blob, common_classes, text, edit_path, project, ref) + if !on_top_of_branch?(project, ref) + edit_disabled_button_tag(text, common_classes) + # This condition only applies to users who are logged in + # Web IDE (Beta) requires the user to have this feature enabled + elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) + edit_link_tag(text, edit_path, common_classes) + elsif current_user && can?(current_user, :fork_project, project) + edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7cd84fe69c9..44ecc2212f2 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -234,7 +234,7 @@ module IssuablesHelper data.merge!(updated_at_by(issuable)) - data.to_json + data end def updated_at_by(issuable) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f5733b4b57c..f6a6d9bebde 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -83,6 +83,10 @@ module TreeHelper " A fork of this project has been created that you can make changes in, so you can submit a merge request." end + def edit_in_new_fork_notice_action(action) + edit_in_new_fork_notice + " Try to #{action} this file again." + end + def commit_in_fork_help "A new branch will be created in your fork and a new merge request will be started." end diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb new file mode 100644 index 00000000000..0027dfdc36b --- /dev/null +++ b/app/mailers/emails/pages_domains.rb @@ -0,0 +1,43 @@ +module Emails + module PagesDomains + def pages_domain_enabled_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("GitLab Pages domain '#{domain.domain}' has been enabled") + ) + end + + def pages_domain_disabled_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("GitLab Pages domain '#{domain.domain}' has been disabled") + ) + end + + def pages_domain_verification_succeeded_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("Verification succeeded for GitLab Pages domain '#{domain.domain}'") + ) + end + + def pages_domain_verification_failed_email(domain, recipient) + @domain = domain + @project = domain.project + + mail( + to: recipient.notification_email, + subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'") + ) + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index eade0fe278f..45d4fb451d8 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -5,6 +5,7 @@ class Notify < BaseMailer include Emails::Issues include Emails::MergeRequests include Emails::Notes + include Emails::PagesDomains include Emails::Projects include Emails::Profile include Emails::Pipelines diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index d8bf54e0c40..588bd50ed77 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -1,10 +1,14 @@ class PagesDomain < ActiveRecord::Base + VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze + VERIFICATION_THRESHOLD = 3.days.freeze + belongs_to :project validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, uniqueness: { case_sensitive: false } validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true + validates :verification_code, presence: true, allow_blank: false validate :validate_pages_domain validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } @@ -16,10 +20,32 @@ class PagesDomain < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' + after_initialize :set_verification_code after_create :update_daemon - after_save :update_daemon + after_update :update_daemon, if: :pages_config_changed? after_destroy :update_daemon + scope :enabled, -> { where('enabled_until >= ?', Time.now ) } + scope :needs_verification, -> do + verified_at = arel_table[:verified_at] + enabled_until = arel_table[:enabled_until] + threshold = Time.now + VERIFICATION_THRESHOLD + + where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) + end + + def verified? + !!verified_at + end + + def unverified? + !verified? + end + + def enabled? + !Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present? + end + def to_param domain end @@ -84,12 +110,49 @@ class PagesDomain < ActiveRecord::Base @certificate_text ||= x509.try(:to_text) end + # Verification codes may be TXT records for domain or verification_domain, to + # support the use of CNAME records on domain. + def verification_domain + return unless domain.present? + + "_#{VERIFICATION_KEY}.#{domain}" + end + + def keyed_verification_code + return unless verification_code.present? + + "#{VERIFICATION_KEY}=#{verification_code}" + end + private + def set_verification_code + return if self.verification_code.present? + + self.verification_code = SecureRandom.hex(16) + end + def update_daemon ::Projects::UpdatePagesConfigurationService.new(project).execute end + def pages_config_changed? + project_id_changed? || + domain_changed? || + certificate_changed? || + key_changed? || + became_enabled? || + became_disabled? + end + + def became_enabled? + enabled_until.present? && !enabled_until_was.present? + end + + def became_disabled? + !enabled_until.present? && enabled_until_was.present? + end + def validate_matching_key unless has_matching_key? self.errors.add(:key, "doesn't match the certificate") diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 1bb576ff971..58731451429 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -69,16 +69,16 @@ class PrometheusService < MonitoringService client.ping { success: true, result: 'Checked API endpoint' } - rescue Gitlab::PrometheusError => err + rescue Gitlab::PrometheusClient::Error => err { success: false, result: err } end def environment_metrics(environment) - with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics)) + with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &rename_field(:data, :metrics)) end def deployment_metrics(deployment) - metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics)) + metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &rename_field(:data, :metrics)) metrics&.merge(deployment_time: deployment.created_at.to_i) || {} end @@ -107,7 +107,7 @@ class PrometheusService < MonitoringService data: data, last_update: Time.now.utc } - rescue Gitlab::PrometheusError => err + rescue Gitlab::PrometheusClient::Error => err { success: false, result: err.message } end @@ -116,10 +116,10 @@ class PrometheusService < MonitoringService Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url)) else cluster = cluster_with_prometheus(environment_id) - raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster + raise Gitlab::PrometheusClient::Error, "couldn't find cluster with Prometheus installed" unless cluster rest_client = client_from_cluster(cluster) - raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client + raise Gitlab::PrometheusClient::Error, "couldn't create proxy Prometheus client" unless rest_client Gitlab::PrometheusClient.new(rest_client) end @@ -152,9 +152,11 @@ class PrometheusService < MonitoringService cluster.application_prometheus.proxy_client end - def rename_data_to_metrics(metrics) - metrics[:metrics] = metrics.delete :data - metrics + def rename_field(old_field, new_field) + -> (metrics) do + metrics[new_field] = metrics.delete(old_field) + metrics + end end def synchronize_service_state! diff --git a/app/models/tree.rb b/app/models/tree.rb index c89b8eca9be..4c1856b67a8 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -9,10 +9,9 @@ class Tree @repository = repository @sha = sha @path = path - @recursive = recursive git_repo = @repository.raw_repository - @entries = get_entries(git_repo, @sha, @path, recursive: @recursive) + @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) end def readme @@ -58,21 +57,4 @@ class Tree def sorted_entries trees + blobs + submodules end - - private - - def get_entries(git_repo, sha, path, recursive: false) - current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path) - ordered_entries = [] - - current_path_entries.each do |entry| - ordered_entries << entry - - if recursive && entry.dir? - ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true)) - end - end - - ordered_entries - end end diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb index 280a2c3afa4..ffde824972c 100644 --- a/app/services/ci/create_trace_artifact_service.rb +++ b/app/services/ci/create_trace_artifact_service.rb @@ -4,13 +4,33 @@ module Ci return if job.job_artifacts_trace job.trace.read do |stream| - if stream.file? - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) + break unless stream.file? + + clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| + create_job_trace!(job, clone_path) + FileUtils.rm(stream.path) end end end + + private + + def create_job_trace!(job, path) + File.open(path) do |stream| + job.create_job_artifacts_trace!( + project: job.project, + file_type: :trace, + file: stream) + end + end + + def clone_file!(src_path, temp_dir) + FileUtils.mkdir_p(temp_dir) + Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| + temp_path = File.join(dir_path, "job.log") + FileUtils.copy(src_path, temp_path) + yield(temp_path) + end + end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 66a9b1f82e0..e87fd49d193 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -77,8 +77,12 @@ class IssuableBaseService < BaseService return unless labels params[:label_ids] = labels.split(",").map do |label_name| - service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) - label = service.execute + label = Labels::FindOrCreateService.new( + current_user, + parent, + title: label_name.strip, + available_labels: available_labels + ).execute label.try(:id) end.compact @@ -102,7 +106,7 @@ class IssuableBaseService < BaseService end def available_labels - LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def merge_quick_actions_into_params!(issuable) @@ -303,4 +307,8 @@ class IssuableBaseService < BaseService def update_project_counter_caches?(issuable) issuable.state_changed? end + + def parent + project + end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 940c8b333d3..079f611b3f3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -1,8 +1,9 @@ module Labels class FindOrCreateService - def initialize(current_user, project, params = {}) + def initialize(current_user, parent, params = {}) @current_user = current_user - @project = project + @parent = parent + @available_labels = params.delete(:available_labels) @params = params.dup.with_indifferent_access end @@ -13,12 +14,13 @@ module Labels private - attr_reader :current_user, :project, :params, :skip_authorization + attr_reader :current_user, :parent, :params, :skip_authorization def available_labels @available_labels ||= LabelsFinder.new( current_user, - project_id: project.id + "#{parent_type}_id".to_sym => parent.id, + only_group_labels: parent_is_group? ).execute(skip_authorization: skip_authorization) end @@ -27,8 +29,8 @@ module Labels def find_or_create_label new_label = available_labels.find_by(title: title) - if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = Labels::CreateService.new(params).execute(project: project) + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) + new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent) end new_label @@ -37,5 +39,13 @@ module Labels def title params[:title] || params[:name] end + + def parent_type + parent.model_name.param_key + end + + def parent_is_group? + parent_type == "group" + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 56e941d90ff..e07ecda27b5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -339,6 +339,30 @@ class NotificationService end end + def pages_domain_verification_succeeded(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later + end + end + + def pages_domain_verification_failed(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_failed_email(domain, user).deliver_later + end + end + + def pages_domain_enabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_enabled_email(domain, user).deliver_later + end + end + + def pages_domain_disabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_disabled_email(domain, user).deliver_later + end + end + protected def new_resource_email(target, method) @@ -433,6 +457,14 @@ class NotificationService private + def recipients_for_pages_domain(domain) + project = domain.project + + return [] unless project + + notifiable_users(project.team.masters, :watch, target: project) + end + def notifiable?(*args) NotificationRecipientService.notifiable?(*args) end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index cacb74b1205..52ff64cc938 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -23,7 +23,7 @@ module Projects end def pages_domains_config - project.pages_domains.map do |domain| + enabled_pages_domains.map do |domain| { domain: domain.domain, certificate: domain.certificate, @@ -32,6 +32,14 @@ module Projects end end + def enabled_pages_domains + if Gitlab::CurrentSettings.pages_domain_verification_enabled? + project.pages_domains.enabled + else + project.pages_domains + end + end + def reload_daemon # GitLab Pages daemon constantly watches for modification time of `pages.path` # It reloads configuration when `pages.path` is modified diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb new file mode 100644 index 00000000000..86166047302 --- /dev/null +++ b/app/services/verify_pages_domain_service.rb @@ -0,0 +1,107 @@ +require 'resolv' + +class VerifyPagesDomainService < BaseService + # The maximum number of seconds to be spent on each DNS lookup + RESOLVER_TIMEOUT_SECONDS = 15 + + # How long verification lasts for + VERIFICATION_PERIOD = 7.days + + attr_reader :domain + + def initialize(domain) + @domain = domain + end + + def execute + return error("No verification code set for #{domain.domain}") unless domain.verification_code.present? + + if !verification_enabled? || dns_record_present? + verify_domain! + elsif expired? + disable_domain! + else + unverify_domain! + end + end + + private + + def verify_domain! + was_disabled = !domain.enabled? + was_unverified = domain.unverified? + + # Prevent any pre-existing grace period from being truncated + reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max + + domain.update!(verified_at: Time.now, enabled_until: reverify) + + if was_disabled + notify(:enabled) + elsif was_unverified + notify(:verification_succeeded) + end + + success + end + + def unverify_domain! + if domain.verified? + domain.update!(verified_at: nil) + notify(:verification_failed) + end + + error("Couldn't verify #{domain.domain}") + end + + def disable_domain! + domain.update!(verified_at: nil, enabled_until: nil) + + notify(:disabled) + + error("Couldn't verify #{domain.domain}. It is now disabled.") + end + + # A domain is only expired until `disable!` has been called + def expired? + domain.enabled_until && domain.enabled_until < Time.now + end + + def dns_record_present? + Resolv::DNS.open do |resolver| + resolver.timeouts = RESOLVER_TIMEOUT_SECONDS + + check(domain.domain, resolver) || check(domain.verification_domain, resolver) + end + end + + def check(domain_name, resolver) + records = parse(txt_records(domain_name, resolver)) + + records.any? do |record| + record == domain.keyed_verification_code || record == domain.verification_code + end + rescue => err + log_error("Failed to check TXT records on #{domain_name} for #{domain.domain}: #{err}") + false + end + + def txt_records(domain_name, resolver) + resolver.getresources(domain_name, Resolv::DNS::Resource::IN::TXT) + end + + def parse(records) + records.flat_map(&:strings).flat_map(&:split) + end + + def verification_enabled? + Gitlab::CurrentSettings.pages_domain_verification_enabled? + end + + def notify(type) + return unless verification_enabled? + + Gitlab::AppLogger.info("Pages domain '#{domain.domain}' changed state to '#{type}'") + notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 60f12030f98..20527d31870 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -237,6 +237,17 @@ .col-sm-10 = f.number_field :max_pages_size, class: 'form-control' .help-block 0 for unlimited + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :pages_domain_verification_enabled do + = f.check_box :pages_domain_verification_enabled + Require users to prove ownership of custom domains + .help-block + Domain verification is an essential security measure for public GitLab + sites. Users are required to demonstrate they control a domain before + it is enabled + = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') %fieldset %legend Continuous Integration and Deployment diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 1e52646b1cc..abec3607cab 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -35,9 +35,8 @@ method: :put, class: 'btn btn-default', data: { confirm: _("Are you sure you want to reset registration token?") } - = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token, - type: 'shared' } + = render partial: 'ci/runner/how_to_setup_shared_runner', + locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } .append-bottom-20.clearfix .pull-left diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 8db7727b80c..37fb8fbab26 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,16 +1,16 @@ - link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank' -.bs-callout.help-callout - %h4= _("How to setup a #{type} Runner for a new project") +.append-bottom-10 + %h4= _("Setup a #{type} Runner manually") - %ol - %li - = _("Install a Runner compatible with GitLab CI") - = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe - %li - = _("Specify the following URL during the Runner setup:") - %code#coordinator_address= root_url(only_path: false) - %li - = _("Use the following registration token during setup:") - %code#registration_token= registration_token - %li - = _("Start the Runner!") +%ol + %li + = _("Install a Runner compatible with GitLab CI") + = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe + %li + = _("Specify the following URL during the Runner setup:") + %code#coordinator_address= root_url(only_path: false) + %li + = _("Use the following registration token during setup:") + %code#registration_token= registration_token + %li + = _("Start the Runner!") diff --git a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml new file mode 100644 index 00000000000..2a190cb9250 --- /dev/null +++ b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml @@ -0,0 +1,3 @@ +.bs-callout.help-callout + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: registration_token, type: 'shared' } diff --git a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml new file mode 100644 index 00000000000..e765a353fe4 --- /dev/null +++ b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml @@ -0,0 +1,26 @@ +.bs-callout.help-callout + .append-bottom-10 + %h4= _('Setup a specific Runner automatically') + + %p + - link_to_help_page = link_to(_('Learn more about Kubernetes'), + help_page_path('user/project/clusters/index'), + target: '_blank', + rel: 'noopener noreferrer') + + = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } + + %ol + %li + = _('Click the button below to begin the install process by navigating to the Kubernetes page') + %li + = _('Select an existing Kubernetes cluster or create a new one') + %li + = _('From the Kubernetes cluster details view, install Runner from the applications list') + + = link_to _('Install Runner on Kubernetes'), + project_clusters_path(@project), + class: 'btn btn-info' + %hr + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: registration_token, type: 'specific' } diff --git a/app/views/notify/pages_domain_disabled_email.html.haml b/app/views/notify/pages_domain_disabled_email.html.haml new file mode 100644 index 00000000000..34ce4238a12 --- /dev/null +++ b/app/views/notify/pages_domain_disabled_email.html.haml @@ -0,0 +1,15 @@ +%p + Following a verification check, your GitLab Pages custom domain has been + %strong disabled. + This means that your content is no longer visible at #{link_to @domain.url, @domain.url} +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + If this domain has been disabled in error, please follow + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + to verify and re-enable your domain. +%p + If you no longer wish to use this domain with GitLab Pages, please remove it + from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_disabled_email.text.haml b/app/views/notify/pages_domain_disabled_email.text.haml new file mode 100644 index 00000000000..4e81b054b1f --- /dev/null +++ b/app/views/notify/pages_domain_disabled_email.text.haml @@ -0,0 +1,13 @@ +Following a verification check, your GitLab Pages custom domain has been +**disabled**. This means that your content is no longer visible at #{@domain.url} + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +If this domain has been disabled in error, please follow these instructions +to verify and re-enable your domain: + += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + +If you no longer wish to use this domain with GitLab Pages, please remove it +from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_enabled_email.html.haml b/app/views/notify/pages_domain_enabled_email.html.haml new file mode 100644 index 00000000000..db09e503f65 --- /dev/null +++ b/app/views/notify/pages_domain_enabled_email.html.haml @@ -0,0 +1,11 @@ +%p + Following a verification check, your GitLab Pages custom domain has been + enabled. You should now be able to view your content at #{link_to @domain.url, @domain.url} +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_enabled_email.text.haml b/app/views/notify/pages_domain_enabled_email.text.haml new file mode 100644 index 00000000000..1ed1dbb8315 --- /dev/null +++ b/app/views/notify/pages_domain_enabled_email.text.haml @@ -0,0 +1,9 @@ +Following a verification check, your GitLab Pages custom domain has been +enabled. You should now be able to view your content at #{@domain.url} + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_verification_failed_email.html.haml b/app/views/notify/pages_domain_verification_failed_email.html.haml new file mode 100644 index 00000000000..0bb0eb09fd5 --- /dev/null +++ b/app/views/notify/pages_domain_verification_failed_email.html.haml @@ -0,0 +1,17 @@ +%p + Verification has failed for one of your GitLab Pages custom domains! +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + Unless you take action, it will be disabled on + %strong= @domain.enabled_until.strftime('%F %T.') + Until then, you can view your content at #{link_to @domain.url, @domain.url} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. +%p + If you no longer wish to use this domain with GitLab Pages, please remove it + from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_verification_failed_email.text.haml b/app/views/notify/pages_domain_verification_failed_email.text.haml new file mode 100644 index 00000000000..c14e0e0c24d --- /dev/null +++ b/app/views/notify/pages_domain_verification_failed_email.text.haml @@ -0,0 +1,14 @@ +Verification has failed for one of your GitLab Pages custom domains! + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +Unless you take action, it will be disabled on *#{@domain.enabled_until.strftime('%F %T')}*. +Until then, you can view your content at #{@domain.url} + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. + +If you no longer wish to use this domain with GitLab Pages, please remove it +from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/pages_domain_verification_succeeded_email.html.haml b/app/views/notify/pages_domain_verification_succeeded_email.html.haml new file mode 100644 index 00000000000..2ead3187b10 --- /dev/null +++ b/app/views/notify/pages_domain_verification_succeeded_email.html.haml @@ -0,0 +1,13 @@ +%p + One of your GitLab Pages custom domains has been successfully verified! +%p + Project: #{link_to @project.human_name, project_url(@project)} +%p + Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)} +%p + This is a notification. No action is required on your part. You can view your + content at #{link_to @domain.url, @domain.url} +%p + Please visit + = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + for more information about custom domain verification. diff --git a/app/views/notify/pages_domain_verification_succeeded_email.text.haml b/app/views/notify/pages_domain_verification_succeeded_email.text.haml new file mode 100644 index 00000000000..e7cdbdee420 --- /dev/null +++ b/app/views/notify/pages_domain_verification_succeeded_email.text.haml @@ -0,0 +1,10 @@ +One of your GitLab Pages custom domains has been successfully verified! + +Project: #{@project.human_name} (#{project_url(@project)}) +Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)}) + +No action is required on your part. You can view your content at #{@domain.url} + +Please visit += help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') +for more information about custom domain verification. diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index 8129c72feb2..f455522d17c 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -3,7 +3,7 @@ .radio = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio" + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" %strong Fast-forward merge %br %span.descr diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 2a77dedd9a2..1b150ec3e5c 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -11,8 +11,8 @@ = view_on_environment_button(@commit.sha, @path, @environment) if @environment .btn-group{ role: "group" }< - = edit_blob_link - = ide_blob_link + = edit_blob_button + = ide_edit_button - if current_user = replace_blob_link = delete_blob_link diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index b3afd16f900..f1324c61500 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -27,6 +27,3 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 = commit_in_fork_help - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('blob') diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 600d679b60c..112dde66ff7 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -4,7 +4,7 @@ .col-xs-12 .text-content %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation') - - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} .text-center diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0b01e38d23d..47bfcb21cf4 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -17,7 +17,7 @@ \ - if editable_diff?(diff_file) - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} - = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, + = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) - if image_diff && image_replaced diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0931ceb1512..b947b91322d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -85,7 +85,7 @@ .settings-content = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = render 'merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" = render 'export', project: @project diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 91f68d8c419..d63443c9da5 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -55,7 +55,7 @@ .issue-details.issuable-details .detail-page-description.content-block - %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue) + %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json #js-issuable-app %h2.title= markdown_field(@issue, :title) - if @issue.description.present? @@ -82,6 +82,3 @@ = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable: @issue - -= webpack_bundle_tag('common_vue') -= webpack_bundle_tag('issue_show') diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index a85cda407af..75df92b05a7 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -3,15 +3,26 @@ .panel-heading Domains (#{@domains.count}) %ul.well-list + - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? - @domains.each do |domain| %li .pull-right = link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped" = link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" .clearfix - %span= link_to domain.domain, domain.url + - if verification_enabled + - tooltip, status = domain.unverified? ? ['Unverified', 'failed'] : ['Verified', 'success'] + = link_to domain.url, title: tooltip, class: 'has-tooltip' do + = sprite_icon("status_#{status}", size: 16, css_class: "has-tooltip ci-status-icon ci-status-icon-#{status}") + = domain.domain + - else + = link_to domain.domain, domain.url %p - if domain.subject %span.label.label-gray Certificate: #{domain.subject} - if domain.expired? %span.label.label-danger Expired + - if verification_enabled && domain.unverified? + %li.warning-row + #{domain.domain} is not verified. To learn how to verify ownership, visit your + = link_to 'domain details', project_pages_domain_path(@project, domain) diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index 876cac0dacb..72e9203bdb0 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,4 +1,10 @@ - page_title "#{@domain.domain}", 'Pages Domains' +- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? +- if verification_enabled && @domain.unverified? + %p.alert.alert-warning + %strong + This domain is not verified. You will need to verify ownership before + access is enabled. %h3.page-title Pages Domain @@ -15,9 +21,26 @@ DNS %td %p - To access the domain create a new DNS record: + To access this domain create a new DNS record: %pre #{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}. + - if verification_enabled + %tr + %td + Verification status + %td + %p + - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + To #{link_to 'verify ownership', help_link} of your domain, create + this DNS record: + %pre + #{@domain.verification_domain} TXT #{@domain.keyed_verification_code} + %p + - if @domain.verified? + #{@domain.domain} has been successfully verified. + - else + = button_to 'Verify ownership', verify_project_pages_domain_path(@project, @domain), class: 'btn btn-save btn-sm' + %tr %td Certificate diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index b037b57e78a..4fd4ca355a8 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,6 +1,6 @@ %h3 Shared Runners -.bs-callout.bs-callout-warning.shared-runners-description +.bs-callout.shared-runners-description - if Gitlab::CurrentSettings.shared_runners_text.present? = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text) - else @@ -9,7 +9,7 @@ on GitLab.com). %hr - if @project.shared_runners_enabled? - = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do + = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do Disable shared Runners - else = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 28ccbf7eb15..f0813e56b71 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -1,8 +1,7 @@ %h3 Specific Runners -= render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: @project.runners_token, - type: 'specific' } += render partial: 'ci/runner/how_to_setup_specific_runner', + locals: { registration_token: @project.runners_token } - if @project_runners.any? %h4.underlined-title Runners activated for this project diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 5f38ecd6820..6dc2b85fd32 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -7,7 +7,7 @@ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') .col-lg-9 - .panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } } + .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json) } } .panel-heading %h3.panel-title = s_('PrometheusService|Monitored') diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 3077203c2a6..235d532bf98 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -4,7 +4,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('deploy_keys') -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 05539dfed7c..39511435508 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -75,7 +75,7 @@ - if show_new_ide? = succeed " " do = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do - = ide_edit_text + = _('Web IDE') = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index a10fc42b82d..3312254f5fb 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -6,7 +6,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'boards' %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f2c20114534..28a5e5da037 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3,6 +3,7 @@ - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping - cronjob:import_export_project_cleanup +- cronjob:pages_domain_verification_cron - cronjob:pipeline_schedule - cronjob:prune_old_events - cronjob:remove_expired_group_links @@ -82,6 +83,7 @@ - new_merge_request - new_note - pages +- pages_domain_verification - post_receive - process_commit - project_cache diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 09559e3b696..d7e24491516 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -1,42 +1,10 @@ class AuthorizedProjectsWorker include ApplicationWorker + prepend WaitableWorker - # Schedules multiple jobs and waits for them to be completed. - def self.bulk_perform_and_wait(args_list) - # Short-circuit: it's more efficient to do small numbers of jobs inline - return bulk_perform_inline(args_list) if args_list.size <= 3 - - waiter = Gitlab::JobWaiter.new(args_list.size) - - # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] - # into [[1, "key"], [2, "key"], [3, "key"]] - waiting_args_list = args_list.map { |args| [*args, waiter.key] } - bulk_perform_async(waiting_args_list) - - waiter.wait - end - - # Performs multiple jobs directly. Failed jobs will be put into sidekiq so - # they can benefit from retries - def self.bulk_perform_inline(args_list) - failed = [] - - args_list.each do |args| - begin - new.perform(*args) - rescue - failed << args - end - end - - bulk_perform_async(failed) if failed.present? - end - - def perform(user_id, notify_key = nil) + def perform(user_id) user = User.find_by(id: user_id) user&.refresh_authorized_projects - ensure - Gitlab::JobWaiter.notify(notify_key, jid) if notify_key end end diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb new file mode 100644 index 00000000000..48ebe862248 --- /dev/null +++ b/app/workers/concerns/waitable_worker.rb @@ -0,0 +1,44 @@ +module WaitableWorker + extend ActiveSupport::Concern + + module ClassMethods + # Schedules multiple jobs and waits for them to be completed. + def bulk_perform_and_wait(args_list, timeout: 10) + # Short-circuit: it's more efficient to do small numbers of jobs inline + return bulk_perform_inline(args_list) if args_list.size <= 3 + + waiter = Gitlab::JobWaiter.new(args_list.size) + + # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] + # into [[1, "key"], [2, "key"], [3, "key"]] + waiting_args_list = args_list.map { |args| [*args, waiter.key] } + bulk_perform_async(waiting_args_list) + + waiter.wait(timeout) + end + + # Performs multiple jobs directly. Failed jobs will be put into sidekiq so + # they can benefit from retries + def bulk_perform_inline(args_list) + failed = [] + + args_list.each do |args| + begin + new.perform(*args) + rescue + failed << args + end + end + + bulk_perform_async(failed) if failed.present? + end + end + + def perform(*args) + notify_key = args.pop if Gitlab::JobWaiter.key?(args.last) + + super(*args) + ensure + Gitlab::JobWaiter.notify(notify_key, jid) if notify_key + end +end diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb new file mode 100644 index 00000000000..a3ff4bd2101 --- /dev/null +++ b/app/workers/pages_domain_verification_cron_worker.rb @@ -0,0 +1,10 @@ +class PagesDomainVerificationCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + PagesDomain.needs_verification.find_each do |domain| + PagesDomainVerificationWorker.perform_async(domain.id) + end + end +end diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb new file mode 100644 index 00000000000..2e93489113c --- /dev/null +++ b/app/workers/pages_domain_verification_worker.rb @@ -0,0 +1,11 @@ +class PagesDomainVerificationWorker + include ApplicationWorker + + def perform(domain_id) + domain = PagesDomain.find_by(id: domain_id) + + return unless domain + + VerifyPagesDomainService.new(domain).execute + end +end diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index e0e6d1418de..fbb14efc525 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -16,43 +16,41 @@ class StuckImportJobsWorker private def mark_projects_without_jid_as_failed! - started_projects_without_jid.each do |project| + enqueued_projects_without_jid.each do |project| project.mark_import_as_failed(error_message) end.count end def mark_projects_with_jid_as_failed! - completed_jids_count = 0 + jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h - started_projects_with_jid.find_in_batches(batch_size: 500) do |group| - jids = group.map(&:import_jid) + # Find the jobs that aren't currently running or that exceeded the threshold. + completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys) + return unless completed_jids.any? - # Find the jobs that aren't currently running or that exceeded the threshold. - completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set + completed_project_ids = jids_and_ids.values_at(*completed_jids) - if completed_jids.any? - completed_jids_count += completed_jids.count - group.each do |project| - project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid) - end + # We select the projects again, because they may have transitioned from + # scheduled/started to finished/failed while we were looking up their Sidekiq status. + completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids) - Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}") - end - end + Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}") - completed_jids_count + completed_projects.each do |project| + project.mark_import_as_failed(error_message) + end.count end - def started_projects - Project.with_import_status(:started) + def enqueued_projects + Project.with_import_status(:scheduled, :started) end - def started_projects_with_jid - started_projects.where.not(import_jid: nil) + def enqueued_projects_with_jid + enqueued_projects.where.not(import_jid: nil) end - def started_projects_without_jid - started_projects.where(import_jid: nil) + def enqueued_projects_without_jid + enqueued_projects.where(import_jid: nil) end def error_message |