summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.overcommit.yml.example4
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue19
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js11
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue66
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue70
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue16
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue7
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js86
-rw-r--r--app/assets/javascripts/environments/mount_show.js32
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js8
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js3
-rw-r--r--app/controllers/projects/settings/operations_controller.rb19
-rw-r--r--app/helpers/environments_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/concerns/noteable.rb6
-rw-r--r--app/models/project_services/prometheus_service.rb9
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/policies/environment_policy.rb6
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/serializers/environment_entity.rb8
-rw-r--r--app/services/projects/operations/update_service.rb18
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml141
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_data.svg1
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_index.svg3
-rw-r--r--changelogs/unreleased/32455-merge-request-discussions-api-degrades-with-comments-count.yml5
-rw-r--r--changelogs/unreleased/41845-delete-environment.yml5
-rw-r--r--changelogs/unreleased/osw-allow-custom-term-timeout-sk-cluster.yml5
-rw-r--r--changelogs/unreleased/rp-allow-local-prom-queries-self-monitoring.yml5
-rw-r--r--config/routes/project.rb7
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb4
-rw-r--r--doc/ci/environments.md27
-rw-r--r--lib/api/discussions.rb11
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/gitlab/cleanup/orphan_lfs_file_references.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb2
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb7
-rw-r--r--lib/gitlab/sidekiq_cluster.rb13
-rw-r--r--lib/gitlab/sidekiq_cluster/cli.rb32
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb88
-rw-r--r--spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/environment.json5
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js10
-rw-r--r--spec/frontend/environments/environment_delete_spec.js38
-rw-r--r--spec/frontend/environments/environment_item_spec.js25
-rw-r--r--spec/lib/gitlab/gitaly_client/blob_service_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_cluster/cli_spec.rb25
-rw-r--r--spec/lib/gitlab/sidekiq_cluster_spec.rb6
-rw-r--r--spec/models/commit_status_spec.rb13
-rw-r--r--spec/models/concerns/noteable_spec.rb15
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb60
-rw-r--r--spec/policies/environment_policy_spec.rb44
-rw-r--r--spec/policies/project_policy_spec.rb46
-rw-r--r--spec/requests/api/environments_spec.rb14
-rw-r--r--spec/services/projects/operations/update_service_spec.rb81
61 files changed, 1042 insertions, 152 deletions
diff --git a/.overcommit.yml.example b/.overcommit.yml.example
index 4e6d084a95d..2cca4c0b488 100644
--- a/.overcommit.yml.example
+++ b/.overcommit.yml.example
@@ -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:
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 7f0c232eea8..7418ca9edfc 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -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();
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index f4ce98037c8..5a77896f5ef 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -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);
+ });
+ }
}
};
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
new file mode 100644
index 00000000000..f731dc49a5b
--- /dev/null
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -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>
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
new file mode 100644
index 00000000000..b53c5fa6583
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -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>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index dc489c804e9..ec5b1092c14 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -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,
@@ -113,6 +115,15 @@ export default {
},
/**
+ * 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>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 07b8d20fde0..cc1d86d06ed 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -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">
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 3caf723442e..d3e8fb7ff08 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -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>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index d60c2efd618..30b02585692 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -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') }} /
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 1c5884b541c..4fadecdd3e9 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -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 (!Visibility.hidden()) {
- this.isLoading = true;
- this.poll.makeRequest();
- } else {
- this.fetchEnvironments();
- }
+ 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;
+ },
+ });
- Visibility.change(() => {
if (!Visibility.hidden()) {
- this.poll.restart();
+ this.isLoading = true;
+ this.poll.makeRequest();
} else {
- this.poll.stop();
+ this.fetchEnvironments();
}
- });
+
+ 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);
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
new file mode 100644
index 00000000000..1929ed080a1
--- /dev/null
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -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,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index cb4ff6856db..122c8f84a2c 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -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}`);
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index abecfba5718..9b0ee40a30a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -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);
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
new file mode 100644
index 00000000000..10e3e28f024
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -0,0 +1,3 @@
+import initShowEnvironment from '~/environments/mount_show';
+
+document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 164cd5b9384..a9d1dc0759d 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -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
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 6bf920448a5..68d78959407 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -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
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 1fb0b83b010..4474534045b 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -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
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 35b727720ba..03260b28335 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -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
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 19f2daa1b01..a7f1fb66a88 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -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
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 75dfad4f3df..fd4ee069041 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -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
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b6127baca90..c7b5d7c8278 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -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
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
index be512dd3b94..f0187a39687 100644
--- a/app/policies/environment_policy.rb
+++ b/app/policies/environment_policy.rb
@@ -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')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index aecefcc89ab..99aeca17699 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -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
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index d9af7af8a8b..7da5910a75b 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -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
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index 27bbf5c6e57..c06f572b52f 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -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
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 51b0b2722d1..b67f9d0cd08 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -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
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 1afbe1fe24e..8f166e9aa16 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -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 } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index ff78abfddf4..3a7a93dc4e6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -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'
diff --git a/app/views/shared/icons/_dev_ops_score_no_data.svg b/app/views/shared/icons/_dev_ops_score_no_data.svg
index ed32b2333e7..5de929859ae 100644
--- a/app/views/shared/icons/_dev_ops_score_no_data.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_data.svg
@@ -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>
diff --git a/app/views/shared/icons/_dev_ops_score_no_index.svg b/app/views/shared/icons/_dev_ops_score_no_index.svg
index 95c00e81d10..0577efca93f 100644
--- a/app/views/shared/icons/_dev_ops_score_no_index.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_index.svg
@@ -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>
diff --git a/changelogs/unreleased/32455-merge-request-discussions-api-degrades-with-comments-count.yml b/changelogs/unreleased/32455-merge-request-discussions-api-degrades-with-comments-count.yml
new file mode 100644
index 00000000000..5fca3beb3fa
--- /dev/null
+++ b/changelogs/unreleased/32455-merge-request-discussions-api-degrades-with-comments-count.yml
@@ -0,0 +1,5 @@
+---
+title: Improve pagination in discussions API
+merge_request: 27697
+author:
+type: performance
diff --git a/changelogs/unreleased/41845-delete-environment.yml b/changelogs/unreleased/41845-delete-environment.yml
new file mode 100644
index 00000000000..d1e2db4d3a0
--- /dev/null
+++ b/changelogs/unreleased/41845-delete-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Adds features to delete stopped environments
+merge_request: 22629
+author:
+type: added
diff --git a/changelogs/unreleased/osw-allow-custom-term-timeout-sk-cluster.yml b/changelogs/unreleased/osw-allow-custom-term-timeout-sk-cluster.yml
new file mode 100644
index 00000000000..8949c95400e
--- /dev/null
+++ b/changelogs/unreleased/osw-allow-custom-term-timeout-sk-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Support custom graceful timeout for Sidekiq Cluster processes
+merge_request: 27710
+author:
+type: added
diff --git a/changelogs/unreleased/rp-allow-local-prom-queries-self-monitoring.yml b/changelogs/unreleased/rp-allow-local-prom-queries-self-monitoring.yml
new file mode 100644
index 00000000000..2fd9f4cb4dc
--- /dev/null
+++ b/changelogs/unreleased/rp-allow-local-prom-queries-self-monitoring.yml
@@ -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
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 4b2bac97678..9bae328cde6 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -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
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 1bdaabad704..958412f30ed 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -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!
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 5bb1e221781..fdd2791aa1d 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -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.
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index a1cec148aeb..8ff275a3a1b 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -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
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index ec58b3b7bb9..e5db9cdedc8 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -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
diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
index 5789fe4f92d..a9961cb8968 100644
--- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb
+++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
@@ -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
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index a0fab67e450..a8d1ea08275 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -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
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 5cde06bb6aa..8c704c2ceea 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -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)
diff --git a/lib/gitlab/sidekiq_cluster.rb b/lib/gitlab/sidekiq_cluster.rb
index 70df40fc35d..e74ae8d0f03 100644
--- a/lib/gitlab/sidekiq_cluster.rb
+++ b/lib/gitlab/sidekiq_cluster.rb
@@ -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}"
diff --git a/lib/gitlab/sidekiq_cluster/cli.rb b/lib/gitlab/sidekiq_cluster/cli.rb
index 245d918e382..f1befe4aff1 100644
--- a/lib/gitlab/sidekiq_cluster/cli.rb
+++ b/lib/gitlab/sidekiq_cluster/cli.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 07aee723e82..cffc43a7dea 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 62b906e8507..c9afff0b73d 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -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
diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
index b23cea65b37..09130d34281 100644
--- a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
+++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
@@ -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
diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json
index 84217a2a01c..f42d701834a 100644
--- a/spec/fixtures/api/schemas/environment.json
+++ b/spec/fixtures/api/schemas/environment.json
@@ -44,7 +44,10 @@
"build_path": { "type": "string" }
}
]
- }
+ },
+ "can_delete": { "type": "boolean" }
+ ,
+ "delete_path": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 43e92bdca5f..68f4c5c9e02 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/environments/environment_delete_spec.js b/spec/frontend/environments/environment_delete_spec.js
new file mode 100644
index 00000000000..b4ecb24cbac
--- /dev/null
+++ b/spec/frontend/environments/environment_delete_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 004687fcf44..5d374a162ab 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
index fc6ac491671..e609acc8fb0 100644
--- a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
index 5bda8ff8c72..72727aab601 100644
--- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/lib/gitlab/sidekiq_cluster_spec.rb
index fa5de04f2f3..9316ac29dd6 100644
--- a/spec/lib/gitlab/sidekiq_cluster_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster_spec.rb
@@ -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)
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index e1a748da7fd..40d9afcdd14 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -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 }
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index e8991a3a015..097bc24d90f 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -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 }
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index fd4783a60f2..297411f7980 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -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
diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb
index 63a9512afcd..a098b52023d 100644
--- a/spec/policies/environment_policy_spec.rb
+++ b/spec/policies/environment_policy_spec.rb
@@ -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
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index e7d49377b78..a729da5afad 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -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
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 56af64342c0..4e2dfe7725e 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -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
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index de028ecb693..99a9fdd4184 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -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
{