Commit e7c8f8fb authored by Constance Okoghenun's avatar Constance Okoghenun

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into profile-bundle-tag-refactor

parents de0aff84 1041f5da
...@@ -2,6 +2,27 @@ ...@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.5.2 (2018-02-25)
### Fixed (7 changes)
- Fix single digit value clipping for stacked progress bar. !17217
- Fix issue with cache key being empty when variable used as the key. !17260
- Enable Legacy Authorization by default on Cluster creations. !17302
- Allow branch names to be named the same as the sha it points to.
- Fix 500 error when loading an invalid upload URL.
- Don't attempt to update user tracked fields if database is in read-only.
- Prevent MR Widget error when no CI configured.
### Performance (5 changes)
- Improve query performance for snippets dashboard. !17088
- Only check LFS integrity for first ref in a push to avoid timeout. !17098
- Improve query performance of MembersFinder. !17190
- Increase feature flag cache TTL to one hour.
- Improve performance of searching for and autocompleting of users.
## 10.5.1 (2018-02-22) ## 10.5.1 (2018-02-22)
- No changes. - No changes.
......
...@@ -411,7 +411,7 @@ group :ed25519 do ...@@ -411,7 +411,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.85.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -285,7 +285,7 @@ GEM ...@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.84.0) gitaly-proto (0.85.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1057,7 +1057,7 @@ DEPENDENCIES ...@@ -1057,7 +1057,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.84.0) gitaly-proto (~> 0.85.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
<script> <script>
import Sortable from 'vendor/Sortable'; 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 boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
/* global ListIssue */ <script>
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -17,6 +18,9 @@ export default { ...@@ -17,6 +18,9 @@ export default {
error: false, error: false,
}; };
}, },
mounted() {
this.$refs.input.focus();
},
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
...@@ -59,42 +63,51 @@ export default { ...@@ -59,42 +63,51 @@ export default {
eventHub.$emit(`hide-issue-form-${this.list.id}`); 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>
...@@ -24,7 +24,7 @@ import './components/new_list_dropdown'; ...@@ -24,7 +24,7 @@ import './components/new_list_dropdown';
import './components/modal/index'; import './components/modal/index';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
$(() => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -236,4 +236,4 @@ $(() => { ...@@ -236,4 +236,4 @@ $(() => {
</div> </div>
`, `,
}); });
}); };
...@@ -110,3 +110,5 @@ class ListIssue { ...@@ -110,3 +110,5 @@ class ListIssue {
} }
window.ListIssue = ListIssue; window.ListIssue = ListIssue;
export default ListIssue;
import Vue from 'vue'; import Vue from 'vue';
import deployKeysApp from './components/app.vue'; import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: document.getElementById('js-deploy-keys'), el: document.getElementById('js-deploy-keys'),
components: { components: {
deployKeysApp, deployKeysApp,
...@@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
...@@ -42,31 +42,34 @@ var Dispatcher; ...@@ -42,31 +42,34 @@ var Dispatcher;
}); });
}); });
switch (page) { const shortcutHandlerPages = [
case 'projects:merge_requests:index': 'projects:activity',
case 'projects:issues:index': 'projects:artifacts:browse',
case 'projects:issues:show': 'projects:artifacts:file',
case 'projects:issues:new': 'projects:blame:show',
case 'projects:issues:edit': 'projects:blob:show',
case 'projects:merge_requests:creations:new': 'projects:commit:show',
case 'projects:merge_requests:creations:diffs': 'projects:commits:show',
case 'projects:merge_requests:edit': 'projects:find_file:show',
case 'projects:merge_requests:show': 'projects:issues:edit',
case 'projects:commit:show': 'projects:issues:index',
case 'projects:activity': 'projects:issues:new',
case 'projects:commits:show': 'projects:issues:show',
case 'projects:show': 'projects:merge_requests:creations:diffs',
case 'groups:show': 'projects:merge_requests:creations:new',
case 'projects:tree:show': 'projects:merge_requests:edit',
case 'projects:find_file:show': 'projects:merge_requests:index',
case 'projects:blob:show': 'projects:merge_requests:show',
case 'projects:blame:show': 'projects:network:show',
case 'projects:network:show': 'projects:show',
case 'projects:artifacts:browse': 'projects:tree:show',
case 'projects:artifacts:file': 'groups:show',
shortcut_handler = true; ];
break;
if (shortcutHandlerPages.indexOf(page) !== -1) {
shortcut_handler = true;
} }
switch (path[0]) { switch (path[0]) {
case 'admin': case 'admin':
switch (path[1]) { switch (path[1]) {
......
<script>
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import FilteredSearchTokenizer from '../filtered_search_tokenizer'; import FilteredSearchTokenizer from '../filtered_search_tokenizer';
export default { export default {
name: 'RecentSearchesDropdownContent', name: 'RecentSearchesDropdownContent',
props: { props: {
items: { items: {
type: Array, type: Array,
...@@ -19,7 +19,6 @@ export default { ...@@ -19,7 +19,6 @@ export default {
required: true, required: true,
}, },
}, },
computed: { computed: {
processedItems() { processedItems() {
return this.items.map((item) => { return this.items.map((item) => {
...@@ -42,7 +41,6 @@ export default { ...@@ -42,7 +41,6 @@ export default {
return this.items.length > 0; return this.items.length > 0;
}, },
}, },
methods: { methods: {
onItemActivated(text) { onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text); eventHub.$emit('recentSearchesItemSelected', text);
...@@ -54,49 +52,53 @@ export default { ...@@ -54,49 +52,53 @@ export default {
eventHub.$emit('requestClearRecentSearches'); eventHub.$emit('requestClearRecentSearches');
}, },
}, },
};
template: ` </script>
<div> <template>
<div <div>
v-if="!isLocalStorageAvailable" <div
class="dropdown-info-note"> v-if="!isLocalStorageAvailable"
This feature requires local storage to be enabled class="dropdown-info-note">
</div> This feature requires local storage to be enabled
<ul v-else-if="hasItems"> </div>
<li <ul v-else-if="hasItems">
v-for="(item, index) in processedItems" <li
:key="index"> v-for="(item, index) in processedItems"
<button :key="`processed-items-${index}`"
type="button" >
class="filtered-search-history-dropdown-item" <button
@click="onItemActivated(item.text)"> type="button"
<span> class="filtered-search-history-dropdown-item"
<span @click="onItemActivated(item.text)">
v-for="(token, tokenIndex) in item.tokens" <span>
class="filtered-search-history-dropdown-token"> <span
<span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> class="filtered-search-history-dropdown-token"
</span> v-for="(token, index) in item.tokens"
</span> :key="`dropdown-token-${index}`"
<span class="filtered-search-history-dropdown-search-token"> >
{{ item.searchToken }} <span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
</span> </span>
</button> </span>
</li> <span class="filtered-search-history-dropdown-search-token">
<li class="divider"></li> {{ item.searchToken }}
<li> </span>
<button </button>
type="button" </li>
class="filtered-search-history-clear-button" <li class="divider"></li>
@click="onRequestClearRecentSearches($event)"> <li>
Clear recent searches <button
</button> type="button"
</li> class="filtered-search-history-clear-button"
</ul> @click="onRequestClearRecentSearches($event)">
<div Clear recent searches
v-else </button>
class="dropdown-info-note"> </li>
You don't have any recent searches </ul>
</div> <div
v-else
class="dropdown-info-note">
You don't have any recent searches
</div> </div>
`, </div>
}; </template>
import Vue from 'vue'; 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'; import eventHub from './event_hub';
class RecentSearchesRoot { class RecentSearchesRoot {
...@@ -33,7 +33,7 @@ class RecentSearchesRoot { ...@@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({ this.vm = new Vue({
el: this.wrapperElement, el: this.wrapperElement,
components: { components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent, RecentSearchesDropdownContent,
}, },
data() { return state; }, data() { return state; },
template: ` template: `
......
...@@ -213,7 +213,7 @@ export default class LabelsSelect { ...@@ -213,7 +213,7 @@ export default class LabelsSelect {
} }
} }
if (label.duplicate) { if (label.duplicate) {
color = gl.DropdownUtils.duplicateLabelColor(label.color); color = DropdownUtils.duplicateLabelColor(label.color);
} }
else { else {
if (label.color != null) { if (label.color != null) {
......
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
}); });
...@@ -3,6 +3,7 @@ import Issue from '~/issue'; ...@@ -3,6 +3,7 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import '~/notes/index'; import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
......
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
document.addEventListener('DOMContentLoaded', initSettingsPanels); document.addEventListener('DOMContentLoaded', () => {
initDeployKeys();
initSettingsPanels();
});
...@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts {
super(); super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); 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('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
Mousetrap.bind('e', this.editIssue.bind(this)); Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) { if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests'); this.enabledHelp.push('.hidden-shortcut.merge_requests');
...@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts {
return false; return false;
} }
editIssue() { static editIssue() {
// Need to click the element as on issues, editing is inline // Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page // on merge request, editing is on a different page
this.editBtn.click(); document.querySelector('.js-issuable-edit').click();
return false; return false;
} }
......
...@@ -227,7 +227,8 @@ export default { ...@@ -227,7 +227,8 @@ export default {
@click="handleMergeButtonClick()" @click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
:class="mergeButtonClass" :class="mergeButtonClass"
type="button"> type="button"
class="qa-merge-button">
<i <i
v-if="isMakingRequest" v-if="isMakingRequest"
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
......
...@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children" ...@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children"
> >
<button <button
type="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" :disabled="isMakingRequest"
@click="rebase" @click="rebase"
> >
......
...@@ -13,10 +13,20 @@ ...@@ -13,10 +13,20 @@
display: inline-block; display: inline-block;
} }
.issuable-meta {
.author_link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.icon-merge-request-unmerged { .icon-merge-request-unmerged {
height: 13px; height: 13px;
margin-bottom: 3px; margin-bottom: 3px;
} }
} }
} }
......
...@@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -40,9 +40,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
def verify_billing def verify_billing
case google_project_billing_status case google_project_billing_status
when nil 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 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 when true
return return
end end
......
...@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :require_pages_enabled! before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy] before_action :domain, only: [:show, :destroy, :verify]
def show def show
end end
...@@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController
@domain = @project.pages_domains.new @domain = @project.pages_domains.new
end end
def verify
result = VerifyPagesDomainService.new(@domain).execute
if result[:status] == :success
flash[:notice] = 'Successfully verified domain ownership'
else
flash[:alert] = 'Failed to verify domain ownership'
end
redirect_to project_pages_domain_path(@project, @domain)
end
def create def create
@domain = @project.pages_domains.create(pages_domain_params) @domain = @project.pages_domains.create(pages_domain_params)
if @domain.valid? if @domain.valid?
redirect_to project_pages_path(@project) redirect_to project_pages_domain_path(@project, @domain)
else else
render 'new' render 'new'
end end
...@@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController ...@@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end end
def domain 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
end end
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
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
...@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder ...@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end end
end end
elsif only_group_labels? elsif only_group_labels?
label_ids << Label.where(group_id: group.id) label_ids << Label.where(group_id: group_ids)
else else
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id))
...@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder ...@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group def group_ids
strong_memoize(:group) do strong_memoize(:group_ids) do
group = Group.find(params[:group_id]) 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
end end
...@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder ...@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent) Ability.allowed?(current_user, :read_label, label_parent)
end end
def groups_user_can_read_labels(groups)
DeclarativePolicy.user_scope do
groups.select { |group| authorized_to_read_labels?(group) }
end
end
end end
...@@ -199,6 +199,7 @@ module ApplicationSettingsHelper ...@@ -199,6 +199,7 @@ module ApplicationSettingsHelper
:metrics_port, :metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout, :metrics_timeout,
:pages_domain_verification_enabled,
:password_authentication_enabled_for_web, :password_authentication_enabled_for_web,
:password_authentication_enabled_for_git, :password_authentication_enabled_for_git,
:performance_bar_allowed_group_id, :performance_bar_allowed_group_id,
......
...@@ -12,75 +12,42 @@ module BlobHelper ...@@ -12,75 +12,42 @@ module BlobHelper
def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
project_edit_blob_path(project, project_edit_blob_path(project,
tree_join(ref, path), tree_join(ref, path),
options[:link_opts]) 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
end end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
end end
def ide_edit_text def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
"#{_('Web IDE')}" return unless blob = readable_blob(options, path, project, ref)
end
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) common_classes = "btn js-edit-blob #{options[:extra_class]}"
return unless show_new_ide?
blob = options.delete(:blob) edit_button_tag(blob,
blob ||= project.repository.blob_at(ref, path) rescue nil 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]}" common_classes = "btn js-edit-ide #{options[:extra_class]}"
if !on_top_of_branch?(project, ref) edit_button_tag(blob,
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' } common_classes,
# This condition applies to anonymous or users who can edit directly _('Web IDE'),
elsif current_user && can_modify_blob?(blob, project, ref) ide_edit_path(project, ref, path, options),
link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" project,
elsif current_user && can?(current_user, :fork_project, project) ref)
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
end 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 return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil blob = project.repository.blob_at(ref, path) rescue nil
...@@ -96,21 +63,12 @@ module BlobHelper ...@@ -96,21 +63,12 @@ module BlobHelper
elsif can_modify_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project) elsif can?(current_user, :fork_project, project)
continue_params = { edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
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 }
end end
end end
def replace_blob_link(project = @project, ref = @ref, path = @path) def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link( modify_file_button(
project, project,
ref, ref,
path, path,
...@@ -122,7 +80,7 @@ module BlobHelper ...@@ -122,7 +80,7 @@ module BlobHelper
end end
def delete_blob_link(project = @project, ref = @ref, path = @path) def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link( modify_file_button(
project, project,
ref, ref,
path, path,
...@@ -332,4 +290,55 @@ module BlobHelper ...@@ -332,4 +290,55 @@ module BlobHelper
options options
end 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 end
...@@ -234,7 +234,7 @@ module IssuablesHelper ...@@ -234,7 +234,7 @@ module IssuablesHelper
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
data.to_json data
end end
def updated_at_by(issuable) def updated_at_by(issuable)
......
...@@ -83,6 +83,10 @@ module TreeHelper ...@@ -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." " A fork of this project has been created that you can make changes in, so you can submit a merge request."
end 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 def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started." "A new branch will be created in your fork and a new merge request will be started."
end end
......
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
...@@ -5,6 +5,7 @@ class Notify < BaseMailer ...@@ -5,6 +5,7 @@ class Notify < BaseMailer
include Emails::Issues include Emails::Issues
include Emails::MergeRequests include Emails::MergeRequests
include Emails::Notes include Emails::Notes
include Emails::PagesDomains
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Pipelines include Emails::Pipelines
......
class PagesDomain < ActiveRecord::Base class PagesDomain < ActiveRecord::Base
VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze
VERIFICATION_THRESHOLD = 3.days.freeze
belongs_to :project belongs_to :project
validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false } validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: 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_pages_domain
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
...@@ -16,10 +20,32 @@ class PagesDomain < ActiveRecord::Base ...@@ -16,10 +20,32 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base, key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
after_initialize :set_verification_code
after_create :update_daemon after_create :update_daemon
after_save :update_daemon after_update :update_daemon, if: :pages_config_changed?
after_destroy :update_daemon 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 def to_param
domain domain
end end
...@@ -84,12 +110,49 @@ class PagesDomain < ActiveRecord::Base ...@@ -84,12 +110,49 @@ class PagesDomain < ActiveRecord::Base
@certificate_text ||= x509.try(:to_text) @certificate_text ||= x509.try(:to_text)
end 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 private
def set_verification_code
return if self.verification_code.present?
self.verification_code = SecureRandom.hex(16)
end
def update_daemon def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute ::Projects::UpdatePagesConfigurationService.new(project).execute
end 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 def validate_matching_key
unless has_matching_key? unless has_matching_key?
self.errors.add(:key, "doesn't match the certificate") self.errors.add(:key, "doesn't match the certificate")
......
...@@ -69,16 +69,16 @@ class PrometheusService < MonitoringService ...@@ -69,16 +69,16 @@ class PrometheusService < MonitoringService
client.ping client.ping
{ success: true, result: 'Checked API endpoint' } { success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusError => err rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err } { success: false, result: err }
end end
def environment_metrics(environment) 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 end
def deployment_metrics(deployment) 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) || {} metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
end end
...@@ -107,7 +107,7 @@ class PrometheusService < MonitoringService ...@@ -107,7 +107,7 @@ class PrometheusService < MonitoringService
data: data, data: data,
last_update: Time.now.utc last_update: Time.now.utc
} }
rescue Gitlab::PrometheusError => err rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err.message } { success: false, result: err.message }
end end
...@@ -116,10 +116,10 @@ class PrometheusService < MonitoringService ...@@ -116,10 +116,10 @@ class PrometheusService < MonitoringService
Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url)) Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url))
else else
cluster = cluster_with_prometheus(environment_id) 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) 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) Gitlab::PrometheusClient.new(rest_client)
end end
...@@ -152,9 +152,11 @@ class PrometheusService < MonitoringService ...@@ -152,9 +152,11 @@ class PrometheusService < MonitoringService
cluster.application_prometheus.proxy_client cluster.application_prometheus.proxy_client
end end
def rename_data_to_metrics(metrics) def rename_field(old_field, new_field)
metrics[:metrics] = metrics.delete :data -> (metrics) do
metrics metrics[new_field] = metrics.delete(old_field)
metrics
end
end end
def synchronize_service_state! def synchronize_service_state!
......
...@@ -9,10 +9,9 @@ class Tree ...@@ -9,10 +9,9 @@ class Tree
@repository = repository @repository = repository
@sha = sha @sha = sha
@path = path @path = path
@recursive = recursive
git_repo = @repository.raw_repository 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 end
def readme def readme
...@@ -58,21 +57,4 @@ class Tree ...@@ -58,21 +57,4 @@ class Tree
def sorted_entries def sorted_entries
trees + blobs + submodules trees + blobs + submodules
end 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 end
...@@ -4,13 +4,33 @@ module Ci ...@@ -4,13 +4,33 @@ module Ci
return if job.job_artifacts_trace return if job.job_artifacts_trace
job.trace.read do |stream| job.trace.read do |stream|
if stream.file? break unless stream.file?
job.create_job_artifacts_trace!(
project: job.project, clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
file_type: :trace, create_job_trace!(job, clone_path)
file: stream) FileUtils.rm(stream.path)
end end
end 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
end end
...@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService ...@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService
return unless labels return unless labels
params[:label_ids] = labels.split(",").map do |label_name| params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = Labels::FindOrCreateService.new(
label = service.execute current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id) label.try(:id)
end.compact end.compact
...@@ -102,7 +106,7 @@ class IssuableBaseService < BaseService ...@@ -102,7 +106,7 @@ class IssuableBaseService < BaseService
end end
def available_labels def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
def merge_quick_actions_into_params!(issuable) def merge_quick_actions_into_params!(issuable)
...@@ -303,4 +307,8 @@ class IssuableBaseService < BaseService ...@@ -303,4 +307,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable) def update_project_counter_caches?(issuable)
issuable.state_changed? issuable.state_changed?
end end
def parent
project
end
end end
module Labels module Labels
class FindOrCreateService class FindOrCreateService
def initialize(current_user, project, params = {}) def initialize(current_user, parent, params = {})
@current_user = current_user @current_user = current_user
@project = project @parent = parent
@available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access @params = params.dup.with_indifferent_access
end end
...@@ -13,12 +14,13 @@ module Labels ...@@ -13,12 +14,13 @@ module Labels
private private
attr_reader :current_user, :project, :params, :skip_authorization attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels def available_labels
@available_labels ||= LabelsFinder.new( @available_labels ||= LabelsFinder.new(
current_user, current_user,
project_id: project.id "#{parent_type}_id".to_sym => parent.id,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization) ).execute(skip_authorization: skip_authorization)
end end
...@@ -27,8 +29,8 @@ module Labels ...@@ -27,8 +29,8 @@ module Labels
def find_or_create_label def find_or_create_label
new_label = available_labels.find_by(title: title) new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(project: project) new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end end
new_label new_label
...@@ -37,5 +39,13 @@ module Labels ...@@ -37,5 +39,13 @@ module Labels
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
def parent_type
parent.model_name.param_key
end
def parent_is_group?
parent_type == "group"
end
end end
end end
...@@ -339,6 +339,30 @@ class NotificationService ...@@ -339,6 +339,30 @@ class NotificationService
end end
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 protected
def new_resource_email(target, method) def new_resource_email(target, method)
...@@ -433,6 +457,14 @@ class NotificationService ...@@ -433,6 +457,14 @@ class NotificationService
private 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) def notifiable?(*args)
NotificationRecipientService.notifiable?(*args) NotificationRecipientService.notifiable?(*args)
end end
......
...@@ -23,7 +23,7 @@ module Projects ...@@ -23,7 +23,7 @@ module Projects
end end
def pages_domains_config def pages_domains_config
project.pages_domains.map do |domain| enabled_pages_domains.map do |domain|
{ {
domain: domain.domain, domain: domain.domain,
certificate: domain.certificate, certificate: domain.certificate,
...@@ -32,6 +32,14 @@ module Projects ...@@ -32,6 +32,14 @@ module Projects
end end
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 def reload_daemon
# GitLab Pages daemon constantly watches for modification time of `pages.path` # GitLab Pages daemon constantly watches for modification time of `pages.path`
# It reloads configuration when `pages.path` is modified # It reloads configuration when `pages.path` is modified
......
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
...@@ -237,6 +237,17 @@ ...@@ -237,6 +237,17 @@
.col-sm-10 .col-sm-10
= f.number_field :max_pages_size, class: 'form-control' = f.number_field :max_pages_size, class: 'form-control'
.help-block 0 for unlimited .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 %fieldset
%legend Continuous Integration and Deployment %legend Continuous Integration and Deployment
......
...@@ -35,9 +35,8 @@ ...@@ -35,9 +35,8 @@
method: :put, class: 'btn btn-default', method: :put, class: 'btn btn-default',
data: { confirm: _("Are you sure you want to reset registration token?") } data: { confirm: _("Are you sure you want to reset registration token?") }
= render partial: 'ci/runner/how_to_setup_runner', = render partial: 'ci/runner/how_to_setup_shared_runner',
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token, locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
type: 'shared' }
.append-bottom-20.clearfix .append-bottom-20.clearfix
.pull-left .pull-left
......
- link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank' - link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'
.bs-callout.help-callout .append-bottom-10
%h4= _("How to setup a #{type} Runner for a new project") %h4= _("Setup a #{type} Runner manually")
%ol %ol
%li %li
= _("Install a Runner compatible with GitLab CI") = _("Install a Runner compatible with GitLab CI")
= (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe
%li %li
= _("Specify the following URL during the Runner setup:") = _("Specify the following URL during the Runner setup:")
%code#coordinator_address= root_url(only_path: false) %code#coordinator_address= root_url(only_path: false)
%li %li
= _("Use the following registration token during setup:") = _("Use the following registration token during setup:")
%code#registration_token= registration_token %code#registration_token= registration_token
%li %li
= _("Start the Runner!") = _("Start the Runner!")
.bs-callout.help-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'shared' }
.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' }
%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.
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.
%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.
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.
%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.
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.
%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.
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.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.radio .radio
= label_tag :project_merge_method_ff do = 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 %strong Fast-forward merge
%br %br
%span.descr %span.descr
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment = view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= edit_blob_link = edit_blob_button
= ide_blob_link = ide_edit_button
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
......
...@@ -27,6 +27,3 @@ ...@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project) - unless can?(current_user, :push_code, @project)
.inline.prepend-left-10 .inline.prepend-left-10
= commit_in_fork_help = commit_in_fork_help
- content_for :page_specific_javascripts do
= webpack_bundle_tag('blob')
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.col-xs-12 .col-xs-12
.text-content .text-content
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation') %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} %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 .text-center
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} - 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) blob: blob, link_opts: link_opts)
- if image_diff && image_replaced - if image_diff && image_replaced
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
.settings-content .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = 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 = 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 = render 'export', project: @project
......
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .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 #js-issuable-app
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
...@@ -82,6 +82,3 @@ ...@@ -82,6 +82,3 @@
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue = render 'shared/issuable/sidebar', issuable: @issue
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issue_show')
...@@ -3,15 +3,26 @@ ...@@ -3,15 +3,26 @@
.panel-heading .panel-heading
Domains (#{@domains.count}) Domains (#{@domains.count})
%ul.well-list %ul.well-list
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- @domains.each do |domain| - @domains.each do |domain|
%li %li
.pull-right .pull-right
= link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped" = 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" = 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 .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 %p
- if domain.subject - if domain.subject
%span.label.label-gray Certificate: #{domain.subject} %span.label.label-gray Certificate: #{domain.subject}
- if domain.expired? - if domain.expired?
%span.label.label-danger 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)
- page_title "#{@domain.domain}", 'Pages Domains' - 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 %h3.page-title
Pages Domain Pages Domain
...@@ -15,9 +21,26 @@ ...@@ -15,9 +21,26 @@
DNS DNS
%td %td
%p %p
To access the domain create a new DNS record: To access this domain create a new DNS record:
%pre %pre
#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}. #{@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 %tr
%td %td
Certificate Certificate
......
%h3 Shared Runners %h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description .bs-callout.shared-runners-description
- if Gitlab::CurrentSettings.shared_runners_text.present? - if Gitlab::CurrentSettings.shared_runners_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text) = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text)
- else - else
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
on GitLab.com). on GitLab.com).
%hr %hr
- if @project.shared_runners_enabled? - 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 Disable shared Runners
- else - else
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
......
%h3 Specific Runners %h3 Specific Runners
= render partial: 'ci/runner/how_to_setup_runner', = render partial: 'ci/runner/how_to_setup_specific_runner',
locals: { registration_token: @project.runners_token, locals: { registration_token: @project.runners_token }
type: 'specific' }
- if @project_runners.any? - if @project_runners.any?
%h4.underlined-title Runners activated for this project %h4.underlined-title Runners activated for this project
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9 .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 .panel-heading
%h3.panel-title %h3.panel-title
= s_('PrometheusService|Monitored') = s_('PrometheusService|Monitored')
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('deploy_keys')
-# Protected branches & tags use a lot of nested partials. -# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory. -# The shared parts of the views can be found in the `shared` directory.
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
- if show_new_ide? - if show_new_ide?
= succeed " " do = succeed " " do
= link_to ide_edit_path(@project, @id), class: 'btn btn-default' 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' = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = 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-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 %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- cronjob:expire_build_artifacts - cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping - cronjob:gitlab_usage_ping
- cronjob:import_export_project_cleanup - cronjob:import_export_project_cleanup
- cronjob:pages_domain_verification_cron
- cronjob:pipeline_schedule - cronjob:pipeline_schedule
- cronjob:prune_old_events - cronjob:prune_old_events
- cronjob:remove_expired_group_links - cronjob:remove_expired_group_links
...@@ -82,6 +83,7 @@ ...@@ -82,6 +83,7 @@
- new_merge_request - new_merge_request
- new_note - new_note
- pages - pages
- pages_domain_verification
- post_receive - post_receive
- process_commit - process_commit
- project_cache - project_cache
......
class AuthorizedProjectsWorker class AuthorizedProjectsWorker
include ApplicationWorker include ApplicationWorker
prepend WaitableWorker
# Schedules multiple jobs and waits for them to be completed. def perform(user_id)
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)
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
user&.refresh_authorized_projects user&.refresh_authorized_projects
ensure
Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
end end
end end
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
class PagesDomainVerificationCronWorker
include ApplicationWorker
include CronjobQueue
def perform
PagesDomain.needs_verification.find_each do |domain|
PagesDomainVerificationWorker.perform_async(domain.id)
end
end
end
class PagesDomainVerificationWorker
include ApplicationWorker
def perform(domain_id)
domain = PagesDomain.find_by(id: domain_id)
return unless domain
VerifyPagesDomainService.new(domain).execute
end
end
...@@ -16,43 +16,41 @@ class StuckImportJobsWorker ...@@ -16,43 +16,41 @@ class StuckImportJobsWorker
private private
def mark_projects_without_jid_as_failed! 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) project.mark_import_as_failed(error_message)
end.count end.count
end end
def mark_projects_with_jid_as_failed! 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| # Find the jobs that aren't currently running or that exceeded the threshold.
jids = group.map(&:import_jid) 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_project_ids = jids_and_ids.values_at(*completed_jids)
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set
if completed_jids.any? # We select the projects again, because they may have transitioned from
completed_jids_count += completed_jids.count # scheduled/started to finished/failed while we were looking up their Sidekiq status.
group.each do |project| completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids)
project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid)
end
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}") Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}")
end
end
completed_jids_count completed_projects.each do |project|
project.mark_import_as_failed(error_message)
end.count
end end
def started_projects def enqueued_projects
Project.with_import_status(:started) Project.with_import_status(:scheduled, :started)
end end
def started_projects_with_jid def enqueued_projects_with_jid
started_projects.where.not(import_jid: nil) enqueued_projects.where.not(import_jid: nil)
end end
def started_projects_without_jid def enqueued_projects_without_jid
started_projects.where(import_jid: nil) enqueued_projects.where(import_jid: nil)
end end
def error_message def error_message
......
---
title: Add verification for GitLab Pages custom domains
merge_request:
author:
type: security
---
title: Improve query performance of MembersFinder.
merge_request: 17190
author:
type: performance
---
title: Enable Legacy Authorization by default on Cluster creations
merge_request: 17302
author:
type: fixed
---
title: Add a button to deploy a runner to a Kubernetes cluster in the settings page
merge_request: 17278
author:
type: changed
---
title: Improve query performance for snippets dashboard.
merge_request: 17088
author:
type: performance
---
title: Fix issue with cache key being empty when variable used as the key
merge_request: 17260
author:
type: fixed
---
title: Do not persist Google Project verification flash errors after a page reload
merge_request: 17299
author:
type: fixed
---
title: Fix Group labels load failure when there are duplicate labels present
merge_request: 17353
author:
type: fixed
--- ---
title: Fix 500 error when loading an invalid upload URL title: Verify project import status again before marking as failed
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Increase feature flag cache TTL to one hour
merge_request:
author:
type: performance
---
title: Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed
merge_request: 17293
author:
type: fixed
--- ---
title: Prevent MR Widget error when no CI configured title: Fixed issue edit shortcut not opening edit form
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Only check LFS integrity for first ref in a push to avoid timeout
merge_request: 17098
author:
type: performance
---
title: Fix single digit value clipping for stacked progress bar
merge_request: 17217
author:
type: fixed
---
title: Prevent trace artifact migration to incur data loss
merge_request: 17313
author:
type: fixed
---
title: Return a 404 instead of 403 if the repository does not exist on disk
merge_request: 17341
author:
type: fixed
---
title: Move BoardNewIssue vue component
merge_request: 16947
author: George Tsiolis
type: performance
---
title: Move RecentSearchesDropdownContent vue component
merge_request: 16951
author: George Tsiolis
type: performance
---
title: Don't attempt to update user tracked fields if database is in read-only
merge_request:
author:
type: fixed
---
title: Improve performance of searching for and autocompleting of users
merge_request:
author:
type: performance
---
title: Allow branch names to be named the same as the sha it points to
merge_request:
author:
type: fixed
...@@ -214,6 +214,10 @@ production: &base ...@@ -214,6 +214,10 @@ production: &base
repository_archive_cache_worker: repository_archive_cache_worker:
cron: "0 * * * *" cron: "0 * * * *"
# Verify custom GitLab Pages domains
pages_domain_verification_cron_worker:
cron: "*/15 * * * *"
registry: registry:
# enabled: true # enabled: true
# host: registry.example.com # host: registry.example.com
......
...@@ -427,6 +427,10 @@ Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({}) ...@@ -427,6 +427,10 @@ Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *' Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *'
Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker' Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker'
Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
# #
# GitLab Shell # GitLab Shell
# #
......
...@@ -10,7 +10,7 @@ Sidekiq.configure_server do |config| ...@@ -10,7 +10,7 @@ Sidekiq.configure_server do |config|
config.server_middleware do |chain| config.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] chain.add Gitlab::SidekiqMiddleware::Shutdown
chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0' chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
chain.add Gitlab::SidekiqStatus::ServerMiddleware chain.add Gitlab::SidekiqStatus::ServerMiddleware
end end
......
...@@ -55,7 +55,11 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -55,7 +55,11 @@ constraints(ProjectUrlConstrainer.new) do
end end
resource :pages, only: [:show, :destroy] do resource :pages, only: [:show, :destroy] do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
member do
post :verify
end
end
end end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
...@@ -74,7 +78,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -74,7 +78,9 @@ constraints(ProjectUrlConstrainer.new) do
resource :mattermost, only: [:new, :create] resource :mattermost, only: [:new, :create]
namespace :prometheus do namespace :prometheus do
get :active_metrics resources :metrics, constraints: { id: %r{[^\/]+} }, only: [] do
get :active_common, on: :collection
end
end end
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do
......
...@@ -67,3 +67,4 @@ ...@@ -67,3 +67,4 @@
- [gcp_cluster, 1] - [gcp_cluster, 1]
- [project_migrate_hashed_storage, 1] - [project_migrate_hashed_storage, 1]
- [storage_migrator, 1] - [storage_migrator, 1]
- [pages_domain_verification, 1]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment