Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9c83aadd26
commit
5064bf8c56
61 changed files with 1044 additions and 154 deletions
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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') }} /
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
32
app/assets/javascripts/environments/mount_show.js
Normal file
32
app/assets/javascripts/environments/mount_show.js
Normal 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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initShowEnvironment from '~/environments/mount_show';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 } }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve pagination in discussions API
|
||||
merge_request: 27697
|
||||
author:
|
||||
type: performance
|
5
changelogs/unreleased/41845-delete-environment.yml
Normal file
5
changelogs/unreleased/41845-delete-environment.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds features to delete stopped environments
|
||||
merge_request: 22629
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support custom graceful timeout for Sidekiq Cluster processes
|
||||
merge_request: 27710
|
||||
author:
|
||||
type: added
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
5
spec/fixtures/api/schemas/environment.json
vendored
5
spec/fixtures/api/schemas/environment.json
vendored
|
@ -44,7 +44,10 @@
|
|||
"build_path": { "type": "string" }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"can_delete": { "type": "boolean" }
|
||||
,
|
||||
"delete_path": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
38
spec/frontend/environments/environment_delete_spec.js
Normal file
38
spec/frontend/environments/environment_delete_spec.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue