Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-25 09:08:11 +00:00
parent 9c83aadd26
commit 5064bf8c56
61 changed files with 1044 additions and 154 deletions

View File

@ -28,7 +28,9 @@ PreCommit:
EsLint:
enabled: true
# https://github.com/sds/overcommit/issues/338
command: './node_modules/eslint/bin/eslint.js'
required_executable: 'yarn'
command: ['yarn', 'eslint']
flags: []
HamlLint:
enabled: true
MergeConflicts:

View File

@ -1,7 +1,6 @@
<script>
import { GlPopover, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { glEmojiTag } from '~/emoji';
import Tracking from '~/tracking';
@ -51,7 +50,7 @@ export default {
},
data() {
return {
popoverDismissed: parseBoolean(Cookies.get(this.dismissKey)),
popoverDismissed: parseBoolean(getCookie(`${this.trackLabel}_${this.dismissKey}`)),
tracking: {
label: this.trackLabel,
property: this.humanAccess,
@ -68,17 +67,27 @@ export default {
emoji() {
return popoverStates[this.trackLabel].emoji || '';
},
dismissCookieName() {
return `${this.trackLabel}_${this.dismissKey}`;
},
commitCookieName() {
return `suggest_gitlab_ci_yml_commit_${this.dismissKey}`;
},
},
mounted() {
if (this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' && !this.popoverDismissed)
if (
this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' &&
!this.popoverDismissed
) {
scrollToElement(document.querySelector(this.target));
}
this.trackOnShow();
},
methods: {
onDismiss() {
this.popoverDismissed = true;
Cookies.set(this.dismissKey, this.popoverDismissed, { expires: 365 });
setCookie(this.dismissCookieName, this.popoverDismissed);
},
trackOnShow() {
if (!this.popoverDismissed) this.track();

View File

@ -5,6 +5,7 @@ import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { setCookie } from '~/lib/utils/common_utils';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
@ -60,6 +61,16 @@ export default () => {
}
if (suggestEl) {
const commitButton = document.querySelector('#commit-changes');
initPopover(suggestEl);
if (commitButton) {
const commitCookieName = `suggest_gitlab_ci_yml_commit_${suggestEl.dataset.dismissKey}`;
commitButton.addEventListener('click', () => {
setCookie(commitCookieName, true);
});
}
}
};

View File

@ -0,0 +1,66 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
id: 'delete-environment-modal',
name: 'DeleteEnvironmentModal',
components: {
GlModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
confirmDeleteMessage() {
return sprintf(
s__(
`Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?`,
),
{
environmentName: this.environment.name,
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('deleteEnvironment', this.environment);
},
},
};
</script>
<template>
<gl-modal
:id="$options.id"
:footer-primary-button-text="s__('Environments|Delete environment')"
footer-primary-button-variant="danger"
@submit="onSubmit"
>
<template slot="header">
<h4 class="modal-title d-flex mw-100">
{{ __('Delete') }}
<span v-gl-tooltip :title="environment.name" class="text-truncate mx-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>
<p>{{ confirmDeleteMessage }}</p>
</gl-modal>
</template>

View File

@ -0,0 +1,70 @@
<script>
/**
* Renders the delete button that allows deleting a stopped environment.
* Used in the environments table and the environment detail view.
*/
import $ from 'jquery';
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
Icon,
LoadingButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
environment: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return s__('Environments|Delete environment');
},
},
mounted() {
eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
},
beforeDestroy() {
eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
},
methods: {
onClick() {
$(this.$el).tooltip('dispose');
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
if (this.environment.id === environment.id) {
this.isLoading = true;
}
},
},
};
</script>
<template>
<loading-button
v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
container-class="btn btn-danger d-none d-sm-none d-md-block"
data-toggle="modal"
data-target="#delete-environment-modal"
@click="onClick"
>
<icon name="remove" />
</loading-button>
</template>

View File

@ -15,8 +15,9 @@ import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue';
import DeleteComponent from './environment_delete.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
/**
@ -33,6 +34,7 @@ export default {
Icon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
@ -112,6 +114,15 @@ export default {
return this.model && this.model.can_stop;
},
/**
* Returns whether the environment can be deleted.
*
* @returns {Boolean}
*/
canDeleteEnvironment() {
return Boolean(this.model && this.model.can_delete && this.model.delete_path);
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
@ -485,6 +496,7 @@ export default {
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canDeleteEnvironment ||
this.canRetry
);
},
@ -680,6 +692,8 @@ export default {
/>
<stop-component v-if="canStopEnvironment" :environment="model" />
<delete-component v-if="canDeleteEnvironment" :environment="model" />
</div>
</div>
</div>

View File

@ -9,6 +9,7 @@ import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import EnableReviewAppButton from './enable_review_app_button.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
@ -18,6 +19,7 @@ export default {
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
DeleteEnvironmentModal,
},
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
@ -95,6 +97,7 @@ export default {
<template>
<div>
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">

View File

@ -63,10 +63,9 @@ export default {
<template slot="header">
<h4 class="modal-title d-flex mw-100">
Stopping
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">{{
environment.name
}}</span>
?
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>

View File

@ -3,10 +3,12 @@ import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
export default {
components: {
StopEnvironmentModal,
DeleteEnvironmentModal,
},
mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
@ -39,6 +41,7 @@ export default {
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<h4 class="js-folder-name environments-folder-name">
{{ s__('Environments|Environments') }} /

View File

@ -27,6 +27,10 @@ export default {
data() {
const store = new EnvironmentsStore();
const isDetailView = document.body.contains(
document.getElementById('environments-detail-view'),
);
return {
store,
state: store.state,
@ -36,7 +40,9 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
environmentInDeleteModal: {},
environmentInRollbackModal: {},
isDetailView,
};
},
@ -121,6 +127,10 @@ export default {
this.environmentInStopModal = environment;
},
updateDeleteModal(environment) {
this.environmentInDeleteModal = environment;
},
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
@ -133,6 +143,30 @@ export default {
this.postAction({ endpoint, errorMessage });
},
deleteEnvironment(environment) {
const endpoint = environment.delete_path;
const mountedToShow = environment.mounted_to_show;
const errorMessage = s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
);
this.service
.deleteAction(endpoint)
.then(() => {
if (!mountedToShow) {
// Reload as a first solution to bust the ETag cache
window.location.reload();
return;
}
const url = window.location.href.split('/');
url.pop();
window.location.href = url.join('/');
})
.catch(() => {
Flash(errorMessage);
});
},
rollbackEnvironment(environment) {
const { retryUrl, isLastDeployment } = environment;
const errorMessage = isLastDeployment
@ -178,36 +212,42 @@ export default {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
if (!this.isDetailView) {
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
eventHub.$on('requestDeleteEnvironment', this.updateDeleteModal);
eventHub.$on('deleteEnvironment', this.deleteEnvironment);
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
@ -216,9 +256,13 @@ export default {
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
eventHub.$off('requestDeleteEnvironment', this.updateDeleteModal);
eventHub.$off('deleteEnvironment', this.deleteEnvironment);
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);

View File

@ -0,0 +1,32 @@
import Vue from 'vue';
import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
import environmentsMixin from './mixins/environments_mixin';
export default () => {
const el = document.getElementById('delete-environment-modal');
const container = document.getElementById('environments-detail-view');
return new Vue({
el,
components: {
DeleteEnvironmentModal,
},
mixins: [environmentsMixin],
data() {
const environment = JSON.parse(JSON.stringify(container.dataset));
environment.delete_path = environment.deletePath;
environment.mounted_to_show = true;
return {
environment,
};
},
render(createElement) {
return createElement('delete-environment-modal', {
props: {
environment: this.environment,
},
});
},
});
};

View File

@ -16,6 +16,11 @@ export default class EnvironmentsService {
return axios.post(endpoint, {});
}
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(endpoint, {});
}
getFolderContent(folderUrl) {
return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}

View File

@ -9,6 +9,7 @@ import { getLocationHash } from './url_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { isFunction } from 'lodash';
import Cookies from 'js-cookie';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
@ -902,3 +903,10 @@ window.gl.utils = {
spriteIcon,
imagePath,
};
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(name);

View File

@ -0,0 +1,3 @@
import initShowEnvironment from '~/environments/mount_show';
document.addEventListener('DOMContentLoaded', () => initShowEnvironment());

View File

@ -4,6 +4,9 @@ module Projects
module Settings
class OperationsController < Projects::ApplicationController
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
respond_to :json, only: [:reset_alerting_token]
helper_method :error_tracking_setting
@ -27,8 +30,24 @@ module Projects
end
end
def reset_alerting_token
result = ::Projects::Operations::UpdateService
.new(project, current_user, alerting_params)
.execute
if result[:status] == :success
render json: { token: project.alerting_setting.token }
else
render json: {}, status: :unprocessable_entity
end
end
private
def alerting_params
{ alerting_setting_attributes: { regenerate_token: true } }
end
def prometheus_service
project.find_or_initialize_service(::PrometheusService.to_param)
end

View File

@ -50,4 +50,8 @@ module EnvironmentsHelper
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
}
end
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
end

View File

@ -4,6 +4,7 @@
module GitlabRoutingHelper
extend ActiveSupport::Concern
include API::Helpers::RelatedResourcesHelpers
included do
Gitlab::Routing.includes_helpers(self)
end
@ -29,6 +30,10 @@ module GitlabRoutingHelper
metrics_project_environment_path(environment.project, environment, *args)
end
def environment_delete_path(environment, *args)
expose_path(api_v4_projects_environments_path(id: environment.project.id, environment_id: environment.id))
end
def issue_path(entity, *args)
project_issue_path(entity.project, entity, *args)
end

View File

@ -62,13 +62,16 @@ class CommitStatus < ApplicationRecord
preload(project: :namespace)
end
scope :match_id_and_lock_version, -> (slice) do
scope :match_id_and_lock_version, -> (items) do
# it expects that items are an array of attributes to match
# each hash needs to have `id` and `lock_version`
slice.inject(self) do |relation, item|
match = CommitStatus.where(item.slice(:id, :lock_version))
or_conditions = items.inject(none) do |relation, item|
match = CommitStatus.default_scoped.where(item.slice(:id, :lock_version))
relation.or(match)
end
merge(or_conditions)
end
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily

View File

@ -79,6 +79,12 @@ module Noteable
.discussions(self)
end
def discussion_ids_relation
notes.select(:discussion_id)
.group(:discussion_id)
.order('MIN(created_at), MIN(id)')
end
def capped_notes_count(max)
notes.limit(max).count
end

View File

@ -81,7 +81,7 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
Gitlab::PrometheusClient.new(api_url)
Gitlab::PrometheusClient.new(api_url, allow_local_requests: allow_local_api_url?)
end
def prometheus_available?
@ -94,7 +94,8 @@ class PrometheusService < MonitoringService
end
def allow_local_api_url?
self_monitoring_project? && internal_prometheus_url?
allow_local_requests_from_web_hooks_and_services? ||
(self_monitoring_project? && internal_prometheus_url?)
end
def configured?
@ -111,6 +112,10 @@ class PrometheusService < MonitoringService
api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
end
def allow_local_requests_from_web_hooks_and_services?
current_settings.allow_local_requests_from_web_hooks_and_services?
end
def should_return_client?
api_url.present? && manual_configuration? && active? && valid?
end

View File

@ -19,8 +19,6 @@ class Snippet < ApplicationRecord
MAX_FILE_COUNT = 1
ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-03-22'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content

View File

@ -12,7 +12,13 @@ class EnvironmentPolicy < BasePolicy
!@subject.stop_action_available? && can?(:update_environment, @subject)
end
condition(:stopped) do
@subject.stopped?
end
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
rule { ~stopped }.prevent(:destroy_environment)
end
EnvironmentPolicy.prepend_if_ee('EE::EnvironmentPolicy')

View File

@ -271,6 +271,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :update_environment
enable :destroy_environment
enable :create_deployment
enable :update_deployment
enable :create_release
@ -316,6 +317,7 @@ class ProjectPolicy < BasePolicy
enable :create_deploy_token
enable :read_pod_logs
enable :destroy_deploy_token
enable :read_prometheus_alerts
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror

View File

@ -28,6 +28,10 @@ class EnvironmentEntity < Grape::Entity
cancel_auto_stop_project_environment_path(environment.project, environment)
end
expose :delete_path do |environment|
environment_delete_path(environment)
end
expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
cluster.cluster_type
end
@ -63,6 +67,10 @@ class EnvironmentEntity < Grape::Entity
environment.elastic_stack_available?
end
expose :can_delete do |environment|
can?(current_user, :destroy_environment, environment)
end
private
alias_method :environment, :object

View File

@ -13,12 +13,30 @@ module Projects
def project_update_params
error_tracking_params
.merge(alerting_setting_params)
.merge(metrics_setting_params)
.merge(grafana_integration_params)
.merge(prometheus_integration_params)
.merge(incident_management_setting_params)
end
def alerting_setting_params
return {} unless can?(current_user, :read_prometheus_alerts, project)
attr = params[:alerting_setting_attributes]
return {} unless attr
regenerate_token = attr.delete(:regenerate_token)
if regenerate_token
attr[:token] = nil
else
attr = attr.except(:token)
end
{ alerting_setting_attributes: attr }
end
def metrics_setting_params
attribs = params[:metrics_setting_attributes]
return {} unless attribs

View File

@ -23,7 +23,7 @@
.js-suggest-gitlab-ci-yml{ data: { toggle: 'popover',
target: '#gitlab-ci-yml-selector',
track_label: 'suggest_gitlab_ci_yml',
dismiss_key: "suggest_gitlab_ci_yml_#{@project.id}",
dismiss_key: @project.id,
human_access: human_access } }
.file-buttons

View File

@ -17,5 +17,5 @@
.js-suggest-gitlab-ci-yml-commit-changes{ data: { toggle: 'popover',
target: '#commit-changes',
track_label: 'suggest_commit_first_project_gitlab_ci_yml',
dismiss_key: "suggest_commit_first_project_gitlab_ci_yml_#{@project.id}",
dismiss_key: @project.id,
human_access: human_access } }

View File

@ -5,74 +5,81 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
= s_("Environments|Stopping")
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
= @environment.name
?
.modal-body
%p= s_('Environments|Are you sure you want to stop this environment?')
- unless @environment.stop_action_available?
.warning_message
%p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
emphasis_end: '</strong>'.html_safe,
ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
ci_config_link_end: '</a>'.html_safe }
%a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
target: '_blank',
rel: 'noopener noreferrer' }
= s_('Environments|Learn more about stopping environments')
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
#environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} }
- if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
= s_("Environments|Stopping")
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
#{@environment.name}?
.modal-body
%p= s_('Environments|Are you sure you want to stop this environment?')
- unless @environment.stop_action_available?
.warning_message
%p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
emphasis_end: '</strong>'.html_safe,
ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
ci_config_link_end: '</a>'.html_safe }
%a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
target: '_blank',
rel: 'noopener noreferrer' }
= s_('Environments|Learn more about stopping environments')
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
.top-area.justify-content-between
.d-flex
%h3.page-title= @environment.name
- if @environment.auto_stop_at?
%p.align-self-end.prepend-left-8
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
.nav-controls.my-2
= render 'projects/environments/pin_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
= s_('Environments|Stop')
- if can_destroy_environment?(@environment)
#delete-environment-modal
.environments-container
- if @deployments.blank?
.empty-state
.text-content
%h4.state-title
= _("You don't have any deployments right now.")
%p.blank-state-text
= _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
.top-area.justify-content-between
.d-flex
%h3.page-title= @environment.name
- if @environment.auto_stop_at?
%p.align-self-end.prepend-left-8
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
.nav-controls.my-2
= render 'projects/environments/pin_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
= s_('Environments|Stop')
- if can_destroy_environment?(@environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#delete-environment-modal' } do
= s_('Environments|Delete')
= render @deployments
.environments-container
- if @deployments.blank?
.empty-state
.text-content
%h4.state-title
= _("You don't have any deployments right now.")
%p.blank-state-text
= _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= paginate @deployments, theme: 'gitlab'
= render @deployments
= paginate @deployments, theme: 'gitlab'

View File

@ -34,7 +34,6 @@
<rect width="38" height="4" y="12" fill="#FB722E" rx="2"/>
</g>
<path fill="#EEE" d="M4 14h106v4H4z"/>
<path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -17,7 +17,6 @@
<rect width="38" height="4" y="12" fill="#FB722E" rx="2"/>
</g>
<path fill="#EEE" d="M2 12h106v4H2z"/>
<path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
<g transform="translate(122)">
<rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/>
@ -39,7 +38,6 @@
<rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/>
<path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
<g transform="translate(243)">
<rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/>
@ -61,7 +59,6 @@
<rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/>
<rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/>
<path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,5 @@
---
title: Improve pagination in discussions API
merge_request: 27697
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Adds features to delete stopped environments
merge_request: 22629
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Support custom graceful timeout for Sidekiq Cluster processes
merge_request: 27710
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Allow self monitoring project to query internal Prometheus even when "Allow local requests in webhooks and services" setting is false
merge_request: 27865
author:
type: fixed

View File

@ -75,7 +75,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
put :reset_registration_token
end
resource :operations, only: [:show, :update]
resource :operations, only: [:show, :update] do
member do
post :reset_alerting_token
end
end
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository do

View File

@ -30,7 +30,7 @@ class Gitlab::Seeder::CycleAnalytics
REVIEW_STAGE_MAX_DURATION_IN_HOURS = 72
DEPLOYMENT_MAX_DURATION_IN_HOURS = 48
def self.seeder_base_on_env(project)
def self.seeder_based_on_env(project)
if ENV[FLAG]
self.new(project: project)
elsif ENV[PERF_TEST]
@ -194,7 +194,7 @@ Gitlab::Seeder.quiet do
project_id = ENV['CYCLE_ANALYTICS_SEED_PROJECT_ID']
project = Project.find(project_id) if project_id
seeder = Gitlab::Seeder::CycleAnalytics.seeder_base_on_env(project)
seeder = Gitlab::Seeder::CycleAnalytics.seeder_based_on_env(project)
if seeder
seeder.seed!

View File

@ -761,6 +761,33 @@ runs once every hour. This means environments will not be stopped at the exact
timestamp as the specified period, but will be stopped when the hourly cron worker
detects expired environments.
#### Delete a stopped environment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22629) in GitLab 12.9.
You can delete [stopped environments](#stopping-an-environment) in one of two
ways: through the GitLab UI or through the API.
##### Delete environments through the UI
To view the list of **Stopped** environments, navigate to **Operations > Environments**
and click the **Stopped** tab.
From there, you can click the **Delete** button directly, or you can click the
environment name to see its details and **Delete** it from there.
You can also delete environments by viewing the details for a
stopped environment:
1. Navigate to **Operations > Environments**.
1. Click on the name of an environment within the **Stopped** environments list.
1. Click on the **Delete** button that appears at the top for all stopped environments.
1. Finally, confirm your chosen environment in the modal that appears to delete it.
##### Delete environments through the API
Environments can also be deleted by using the [Environments API](../api/environments.md#delete-an-environment).
### Grouping similar environments
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14.

View File

@ -28,10 +28,10 @@ module API
get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable)
discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
discussion_ids = paginate(noteable.discussion_ids_relation)
notes = readable_discussion_notes(noteable, discussion_ids)
present paginate(discussions), with: Entities::Discussion
present Discussion.build_collection(notes, noteable), with: Entities::Discussion
end
desc "Get a single #{noteable_type.to_s.downcase} discussion" do
@ -221,10 +221,9 @@ module API
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def readable_discussion_notes(noteable, discussion_id = nil)
def readable_discussion_notes(noteable, discussion_ids)
notes = noteable.notes
notes = notes.where(discussion_id: discussion_id) if discussion_id
notes = notes
.where(discussion_id: discussion_ids)
.inc_relations_for_view
.includes(:noteable)
.fresh

View File

@ -82,9 +82,10 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
end
delete ':id/environments/:environment_id' do
authorize! :update_environment, user_project
authorize! :read_environment, user_project
environment = user_project.environments.find(params[:environment_id])
authorize! :destroy_environment, environment
destroy_conditionally!(environment)
end

View File

@ -40,7 +40,7 @@ module Gitlab
end
def lfs_oids_from_repository
project.repository.gitaly_blob_client.get_all_lfs_pointers(nil).map(&:lfs_oid)
project.repository.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
end
def orphan_oids

View File

@ -13,7 +13,7 @@ module Gitlab
end
def all_pointers
@repository.gitaly_blob_client.get_all_lfs_pointers(@newrev)
@repository.gitaly_blob_client.get_all_lfs_pointers
end
end
end

View File

@ -131,10 +131,9 @@ module Gitlab
map_lfs_pointers(response)
end
def get_all_lfs_pointers(revision)
request = Gitaly::GetNewLFSPointersRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision)
def get_all_lfs_pointers
request = Gitaly::GetAllLFSPointersRequest.new(
repository: @gitaly_repo
)
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout)

View File

@ -62,21 +62,28 @@ module Gitlab
# directory - The directory of the Rails application.
#
# Returns an Array containing the PIDs of the started processes.
def self.start(queues, env: :development, directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, dryrun: false)
def self.start(queues, env: :development, directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, timeout: CLI::DEFAULT_SOFT_TIMEOUT_SECONDS, dryrun: false)
queues.map.with_index do |pair, index|
start_sidekiq(pair, env: env, directory: directory, max_concurrency: max_concurrency, min_concurrency: min_concurrency, worker_id: index, dryrun: dryrun)
start_sidekiq(pair, env: env,
directory: directory,
max_concurrency: max_concurrency,
min_concurrency: min_concurrency,
worker_id: index,
timeout: timeout,
dryrun: dryrun)
end
end
# Starts a Sidekiq process that processes _only_ the given queues.
#
# Returns the PID of the started process.
def self.start_sidekiq(queues, env:, directory:, max_concurrency:, min_concurrency:, worker_id:, dryrun:)
def self.start_sidekiq(queues, env:, directory:, max_concurrency:, min_concurrency:, worker_id:, timeout:, dryrun:)
counts = count_by_queue(queues)
cmd = %w[bundle exec sidekiq]
cmd << "-c#{self.concurrency(queues, min_concurrency, max_concurrency)}"
cmd << "-e#{env}"
cmd << "-t#{timeout}"
cmd << "-gqueues:#{proc_details(counts)}"
cmd << "-r#{directory}"

View File

@ -8,9 +8,17 @@ module Gitlab
module SidekiqCluster
class CLI
CHECK_TERMINATE_INTERVAL_SECONDS = 1
# How long to wait in total when asking for a clean termination
# Sidekiq default to self-terminate is 25s
TERMINATE_TIMEOUT_SECONDS = 30
# How long to wait when asking for a clean termination.
# It maps the Sidekiq default timeout:
# https://github.com/mperham/sidekiq/wiki/Signals#term
#
# This value is passed to Sidekiq's `-t` if none
# is given through arguments.
DEFAULT_SOFT_TIMEOUT_SECONDS = 25
# After surpassing the soft timeout.
DEFAULT_HARD_TIMEOUT_SECONDS = 5
CommandError = Class.new(StandardError)
@ -74,7 +82,8 @@ module Gitlab
directory: @rails_path,
max_concurrency: @max_concurrency,
min_concurrency: @min_concurrency,
dryrun: @dryrun
dryrun: @dryrun,
timeout: soft_timeout_seconds
)
return if @dryrun
@ -88,6 +97,15 @@ module Gitlab
SidekiqCluster.write_pid(@pid) if @pid
end
def soft_timeout_seconds
@soft_timeout_seconds || DEFAULT_SOFT_TIMEOUT_SECONDS
end
# The amount of time it'll wait for killing the alive Sidekiq processes.
def hard_timeout_seconds
soft_timeout_seconds + DEFAULT_HARD_TIMEOUT_SECONDS
end
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
end
@ -101,7 +119,7 @@ module Gitlab
end
def wait_for_termination
deadline = monotonic_time + TERMINATE_TIMEOUT_SECONDS
deadline = monotonic_time + hard_timeout_seconds
sleep(CHECK_TERMINATE_INTERVAL_SECONDS) while continue_waiting?(deadline)
hard_stop_stuck_pids
@ -176,6 +194,10 @@ module Gitlab
@interval = int.to_i
end
opt.on('-t', '--timeout INT', 'Graceful timeout for all running processes') do |timeout|
@soft_timeout_seconds = timeout.to_i
end
opt.on('-d', '--dryrun', 'Print commands that would be run without this flag, and quit') do |int|
@dryrun = true
end

View File

@ -7695,6 +7695,9 @@ msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
msgid "Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again."
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
@ -7728,6 +7731,15 @@ msgstr ""
msgid "Environments|Currently showing all results."
msgstr ""
msgid "Environments|Delete"
msgstr ""
msgid "Environments|Delete environment"
msgstr ""
msgid "Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?"
msgstr ""
msgid "Environments|Deploy to..."
msgstr ""

View File

@ -295,6 +295,94 @@ describe Projects::Settings::OperationsController do
end
end
end
describe 'POST reset_alerting_token' do
let(:project) { create(:project) }
before do
project.add_maintainer(user)
end
context 'with existing alerting setting' do
let!(:alerting_setting) do
create(:project_alerting_setting, project: project)
end
let!(:old_token) { alerting_setting.token }
it 'returns newly reset token' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['token']).to eq(alerting_setting.reload.token)
expect(old_token).not_to eq(alerting_setting.token)
end
end
context 'without existing alerting setting' do
it 'creates a token' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:ok)
expect(project.alerting_setting).not_to be_nil
expect(json_response['token']).to eq(project.alerting_setting.token)
end
end
context 'when update fails' do
let(:operations_update_service) { spy(:operations_update_service) }
let(:alerting_params) do
{ alerting_setting_attributes: { regenerate_token: true } }
end
before do
expect(::Projects::Operations::UpdateService)
.to receive(:new).with(project, user, alerting_params)
.and_return(operations_update_service)
expect(operations_update_service).to receive(:execute)
.and_return(status: :error)
end
it 'returns unprocessable_entity' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to be_empty
end
end
context 'with insufficient permissions' do
before do
project.add_reporter(user)
end
it 'returns 404' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'as an anonymous user' do
before do
sign_out(user)
end
it 'returns a redirect' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:redirect)
end
end
private
def reset_alerting_token
post :reset_alerting_token,
params: project_params(project),
format: :json
end
end
end
private

View File

@ -3,6 +3,8 @@
require 'spec_helper'
describe 'User follows pipeline suggest nudge spec when feature is enabled', :js do
include CookieHelper
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :empty_repo) }
@ -38,6 +40,12 @@ describe 'User follows pipeline suggest nudge spec when feature is enabled', :js
expect(page).to have_content('1/2: Choose a template')
end
end
it 'sets the commit cookie when the Commit button is clicked' do
click_button 'Commit changes'
expect(get_cookie("suggest_gitlab_ci_yml_commit_#{project.id}")).to be_present
end
end
context 'when the page is visited without the param' do

View File

@ -44,7 +44,10 @@
"build_path": { "type": "string" }
}
]
}
},
"can_delete": { "type": "boolean" }
,
"delete_path": { "type": "string" }
},
"additionalProperties": false
}

View File

@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import Cookies from 'js-cookie';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import * as utils from '~/lib/utils/common_utils';
@ -10,9 +9,11 @@ jest.mock('~/lib/utils/common_utils', () => ({
}));
const target = 'gitlab-ci-yml-selector';
const dismissKey = 'suggest_gitlab_ci_yml_99';
const dismissKey = '99';
const defaultTrackLabel = 'suggest_gitlab_ci_yml';
const commitTrackLabel = 'suggest_commit_first_project_gitlab_ci_yml';
const dismissCookie = 'suggest_gitlab_ci_yml_99';
const humanAccess = 'owner';
describe('Suggest gitlab-ci.yml Popover', () => {
@ -46,7 +47,8 @@ describe('Suggest gitlab-ci.yml Popover', () => {
describe('when the dismiss cookie is set', () => {
beforeEach(() => {
Cookies.set(dismissKey, true);
utils.setCookie(dismissCookie, true);
createWrapper(defaultTrackLabel);
});
@ -55,7 +57,7 @@ describe('Suggest gitlab-ci.yml Popover', () => {
});
afterEach(() => {
Cookies.remove(dismissKey);
utils.removeCookie(dismissCookie);
});
});

View File

@ -0,0 +1,38 @@
import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
import DeleteComponent from '~/environments/components/environment_delete.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/environments/event_hub';
$.fn.tooltip = () => {};
describe('External URL Component', () => {
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(DeleteComponent, {
propsData: {
environment: {},
},
});
};
const findButton = () => wrapper.find(LoadingButton);
beforeEach(() => {
jest.spyOn(window, 'confirm');
createWrapper();
});
it('should render a button to delete the environment', () => {
expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('title')).toEqual('Delete environment');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', wrapper.vm.environment);
});
});

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
import DeleteComponent from '~/environments/components/environment_delete.vue';
import { environment, folder, tableData } from './mock_data';
@ -54,6 +55,10 @@ describe('Environment item', () => {
expect(wrapper.find('.environment-created-date-timeago').text()).toContain(formattedDate);
});
it('should not render the delete button', () => {
expect(wrapper.find(DeleteComponent).exists()).toBe(false);
});
describe('With user information', () => {
it('should render user avatar with link to profile', () => {
expect(wrapper.find('.js-deploy-user-container').attributes('href')).toEqual(
@ -98,7 +103,7 @@ describe('Environment item', () => {
expect(findAutoStop().exists()).toBe(false);
});
it('should not render the suto-stop button', () => {
it('should not render the auto-stop button', () => {
expect(wrapper.find(PinComponent).exists()).toBe(false);
});
});
@ -205,4 +210,22 @@ describe('Environment item', () => {
expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size);
});
});
describe('When environment can be deleted', () => {
beforeEach(() => {
factory({
propsData: {
model: {
can_delete: true,
delete_path: 'http://0.0.0.0:3000/api/v4/projects/8/environments/45',
},
tableData,
},
});
});
it('should render the delete button', () => {
expect(wrapper.find(DeleteComponent).exists()).toBe(true);
});
});
});

View File

@ -46,14 +46,12 @@ describe Gitlab::GitalyClient::BlobService do
end
describe '#get_all_lfs_pointers' do
let(:revision) { 'master' }
subject { client.get_all_lfs_pointers(revision) }
subject { client.get_all_lfs_pointers }
it 'sends a get_all_lfs_pointers message' do
expect_any_instance_of(Gitaly::BlobService::Stub)
.to receive(:get_all_lfs_pointers)
.with(gitaly_request_with_params(revision: revision), kind_of(Hash))
.with(gitaly_request_with_params({}), kind_of(Hash))
.and_return([])
subject

View File

@ -5,8 +5,9 @@ require 'rspec-parameterized'
describe Gitlab::SidekiqCluster::CLI do
let(:cli) { described_class.new('/dev/null') }
let(:timeout) { described_class::DEFAULT_SOFT_TIMEOUT_SECONDS }
let(:default_options) do
{ env: 'test', directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, dryrun: false }
{ env: 'test', directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, dryrun: false, timeout: timeout }
end
before do
@ -80,6 +81,22 @@ describe Gitlab::SidekiqCluster::CLI do
end
end
context '-timeout flag' do
it 'when given', 'starts Sidekiq workers with given timeout' do
expect(Gitlab::SidekiqCluster).to receive(:start)
.with([['foo']], default_options.merge(timeout: 10))
cli.run(%w(foo --timeout 10))
end
it 'when not given', 'starts Sidekiq workers with default timeout' do
expect(Gitlab::SidekiqCluster).to receive(:start)
.with([['foo']], default_options.merge(timeout: described_class::DEFAULT_SOFT_TIMEOUT_SECONDS))
cli.run(%w(foo))
end
end
context 'queue namespace expansion' do
it 'starts Sidekiq workers for all queues in all_queues.yml with a namespace in argv' do
expect(Gitlab::SidekiqConfig::CliMethods).to receive(:worker_queues).and_return(['cronjob:foo', 'cronjob:bar'])
@ -222,7 +239,8 @@ describe Gitlab::SidekiqCluster::CLI do
.with([], :KILL)
stub_const("Gitlab::SidekiqCluster::CLI::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
stub_const("Gitlab::SidekiqCluster::CLI::TERMINATE_TIMEOUT_SECONDS", 1)
allow(cli).to receive(:terminate_timeout_seconds) { 1 }
cli.wait_for_termination
end
@ -251,7 +269,8 @@ describe Gitlab::SidekiqCluster::CLI do
cli.run(%w(foo))
stub_const("Gitlab::SidekiqCluster::CLI::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
stub_const("Gitlab::SidekiqCluster::CLI::TERMINATE_TIMEOUT_SECONDS", 1)
allow(cli).to receive(:terminate_timeout_seconds) { 1 }
cli.wait_for_termination
end
end

View File

@ -58,6 +58,7 @@ describe Gitlab::SidekiqCluster do
directory: 'foo/bar',
max_concurrency: 20,
min_concurrency: 10,
timeout: 25,
dryrun: true
}
@ -74,6 +75,7 @@ describe Gitlab::SidekiqCluster do
max_concurrency: 50,
min_concurrency: 0,
worker_id: an_instance_of(Integer),
timeout: 25,
dryrun: false
}
@ -87,10 +89,10 @@ describe Gitlab::SidekiqCluster do
describe '.start_sidekiq' do
let(:first_worker_id) { 0 }
let(:options) do
{ env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 0, worker_id: first_worker_id, dryrun: false }
{ env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 0, worker_id: first_worker_id, timeout: 10, dryrun: false }
end
let(:env) { { "ENABLE_SIDEKIQ_CLUSTER" => "1", "SIDEKIQ_WORKER_ID" => first_worker_id.to_s } }
let(:args) { ['bundle', 'exec', 'sidekiq', anything, '-eproduction', *([anything] * 5)] }
let(:args) { ['bundle', 'exec', 'sidekiq', anything, '-eproduction', '-t10', *([anything] * 5)] }
it 'starts a Sidekiq process' do
allow(Process).to receive(:spawn).and_return(1)

View File

@ -449,6 +449,19 @@ describe CommitStatus do
end
end
describe '.match_id_and_lock_version' do
let(:status_1) { create_status(lock_version: 1) }
let(:status_2) { create_status(lock_version: 2) }
it 'returns statuses that match the given id and lock versions' do
params = [
{ id: status_1.id, lock_version: 1 },
{ id: status_2.id, lock_version: 3 }
]
expect(described_class.match_id_and_lock_version(params)).to contain_exactly(status_1)
end
end
describe '#before_sha' do
subject { commit_status.before_sha }

View File

@ -62,6 +62,21 @@ describe Noteable do
end
end
describe '#discussion_ids_relation' do
it 'returns ordered discussion_ids' do
discussion_ids = subject.discussion_ids_relation.pluck(:discussion_id)
expect(discussion_ids).to eq([
active_diff_note1,
active_diff_note3,
outdated_diff_note1,
discussion_note1,
note1,
note2
].map(&:discussion_id))
end
end
describe '#grouped_diff_discussions' do
let(:grouped_diff_discussions) { subject.grouped_diff_discussions }

View File

@ -66,6 +66,18 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
end
it 'can query when local requests are allowed' do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
aggregate_failures do
['127.0.0.1', '192.168.2.3'].each do |url|
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
expect(service.can_query?).to be true
end
end
end
context 'with self-monitoring project and internal Prometheus' do
before do
service.api_url = 'http://localhost:9090'
@ -152,6 +164,54 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
expect(service.prometheus_client).to be_nil
end
end
context 'when local requests are allowed' do
let(:manual_configuration) { true }
let(:api_url) { 'http://192.168.1.1:9090' }
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
stub_prometheus_request("#{api_url}/api/v1/query?query=1")
end
it 'allows local requests' do
expect(service.prometheus_client).not_to be_nil
expect { service.prometheus_client.ping }.not_to raise_error
end
end
context 'when local requests are blocked' do
let(:manual_configuration) { true }
let(:api_url) { 'http://192.168.1.1:9090' }
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
stub_prometheus_request("#{api_url}/api/v1/query?query=1")
end
it 'blocks local requests' do
expect(service.prometheus_client).to be_nil
end
context 'with self monitoring project and internal Prometheus URL' do
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
stub_application_setting(self_monitoring_project_id: project.id)
stub_config(prometheus: {
enable: true,
listen_address: api_url
})
end
it 'allows local requests' do
expect(service.prometheus_client).not_to be_nil
expect { service.prometheus_client.ping }.not_to raise_error
end
end
end
end
describe '#prometheus_available?' do

View File

@ -86,6 +86,50 @@ describe EnvironmentPolicy do
it { expect(policy).to be_allowed :stop_environment }
end
end
describe '#destroy_environment' do
let(:environment) do
create(:environment, project: project)
end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | true
:maintainer | true
end
with_them do
before do
project.add_user(user, access_level) unless access_level.nil?
end
it { expect(policy).to be_disallowed :destroy_environment }
context 'when environment is stopped' do
before do
environment.stop!
end
it { expect(policy.allowed?(:destroy_environment)).to be allowed? }
end
end
context 'when an admin user' do
let(:user) { create(:user, :admin) }
it { expect(policy).to be_disallowed :destroy_environment }
context 'when environment is stopped' do
before do
environment.stop!
end
it { expect(policy).to be_allowed :destroy_environment }
end
end
end
end
context 'when project is public' do

View File

@ -573,4 +573,50 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:admin_issue) }
end
end
describe 'read_prometheus_alerts' do
subject { described_class.new(current_user, project) }
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:read_prometheus_alerts) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_prometheus_alerts) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:read_prometheus_alerts) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
end
end

View File

@ -171,7 +171,15 @@ describe API::Environments do
describe 'DELETE /projects/:id/environments/:environment_id' do
context 'as a maintainer' do
it 'returns a 200 for an existing environment' do
it "rejects the requests in environment isn't stopped" do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns a 200 for stopped environment' do
environment.stop
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
@ -185,6 +193,10 @@ describe API::Environments do
end
it_behaves_like '412 response' do
before do
environment.stop
end
let(:request) { api("/projects/#{project.id}/environments/#{environment.id}", user) }
end
end

View File

@ -11,6 +11,87 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
context 'alerting setting' do
before do
project.add_maintainer(user)
end
shared_examples 'no operation' do
it 'does nothing' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting).to be_nil
end
end
context 'with valid params' do
let(:params) { { alerting_setting_attributes: alerting_params } }
shared_examples 'setting creation' do
it 'creates a setting' do
expect(project.alerting_setting).to be_nil
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting).not_to be_nil
end
end
context 'when regenerate_token is not set' do
let(:alerting_params) { { token: 'some token' } }
context 'with an existing setting' do
let!(:alerting_setting) do
create(:project_alerting_setting, project: project)
end
it 'ignores provided token' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting.token)
.to eq(alerting_setting.token)
end
end
context 'without an existing setting' do
it_behaves_like 'setting creation'
end
end
context 'when regenerate_token is set' do
let(:alerting_params) { { regenerate_token: true } }
context 'with an existing setting' do
let(:token) { 'some token' }
let!(:alerting_setting) do
create(:project_alerting_setting, project: project, token: token)
end
it 'regenerates token' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting.token).not_to eq(token)
end
end
context 'without an existing setting' do
it_behaves_like 'setting creation'
context 'with insufficient permissions' do
before do
project.add_reporter(user)
end
it_behaves_like 'no operation'
end
end
end
end
context 'with empty params' do
let(:params) { {} }
it_behaves_like 'no operation'
end
end
context 'metrics dashboard setting' do
let(:params) do
{