diff options
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 { |