Skip to content
Snippets Groups Projects
Commit 2f96930d authored by Phil Hughes's avatar Phil Hughes
Browse files

Merge branch 'winh-stop-all-environments' into 'master'

Support manually stopping any environment from the UI

Closes #25388

See merge request gitlab-org/gitlab-ce!20077
parents ca1deb9e d79cef3a
No related branches found
No related tags found
1 merge request!10495Merge Requests - Assignee
Showing
with 827 additions and 662 deletions
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
   
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
loadingIcon,
Icon,
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
}, },
components: { },
loadingIcon, data() {
Icon, return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
}, },
props: { },
actions: { methods: {
type: Array, onClickAction(endpoint) {
required: false, this.isLoading = true;
default: () => [],
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
   
eventHub.$emit('postAction', endpoint); eventHub.$emit('postAction', { endpoint });
}, },
   
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
} }
   
return !action.playable; return !action.playable;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div <div
Loading
@@ -61,10 +61,7 @@
Loading
@@ -61,10 +61,7 @@
data-toggle="dropdown" data-toggle="dropdown"
> >
<span> <span>
<icon <icon name="play" />
:size="12"
name="play"
/>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" aria-hidden="true"
Loading
@@ -85,10 +82,6 @@
Loading
@@ -85,10 +82,6 @@
class="js-manual-action-link no-btn btn" class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)" @click="onClickAction(action.play_path)"
> >
<icon
:size="12"
name="play"
/>
<span> <span>
{{ action.name }} {{ action.name }}
</span> </span>
Loading
Loading
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
   
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
components: { components: {
Icon, Icon,
},
directives: {
tooltip,
},
props: {
externalUrl: {
type: String,
required: true,
}, },
directives: { },
tooltip, computed: {
title() {
return s__('Environments|Open live environment');
}, },
props: { },
externalUrl: { };
type: String,
required: true,
},
},
computed: {
title() {
return s__('Environments|Open');
},
},
};
</script> </script>
<template> <template>
<a <a
Loading
@@ -37,9 +37,6 @@
Loading
@@ -37,9 +37,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
> >
<icon <icon name="external-link" />
:size="12"
name="external-link"
/>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
   
export default { export default {
components: { components: {
Icon, Icon,
},
directives: {
tooltip,
},
props: {
monitoringUrl: {
type: String,
required: true,
}, },
directives: { },
tooltip, computed: {
title() {
return 'Monitoring';
}, },
props: { },
monitoringUrl: { };
type: String,
required: true,
},
},
computed: {
title() {
return 'Monitoring';
},
},
};
</script> </script>
<template> <template>
<a <a
Loading
@@ -35,9 +35,6 @@
Loading
@@ -35,9 +35,6 @@
data-container="body" data-container="body"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
> >
<icon <icon name="chart" />
:size="12"
name="chart"
/>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
* *
* Makes a post request when the button is clicked. * Makes a post request when the button is clicked.
*/ */
import eventHub from '../event_hub'; import { s__ } from '~/locale';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { import eventHub from '../event_hub';
components: { import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
loadingIcon,
export default {
components: {
Icon,
LoadingIcon,
},
directives: {
tooltip,
},
props: {
retryUrl: {
type: String,
default: '',
}, },
props: {
retryUrl: { isLastDeployment: {
type: String, type: Boolean,
default: '', default: true,
},
isLastDeployment: {
type: Boolean,
default: true,
},
}, },
data() { },
return { data() {
isLoading: false, return {
}; isLoading: false,
};
},
computed: {
title() {
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
}, },
methods: { },
onClick() {
this.isLoading = true; methods: {
onClick() {
this.isLoading = true;
   
eventHub.$emit('postAction', this.retryUrl); eventHub.$emit('postAction', { endpoint: this.retryUrl });
},
}, },
}; },
};
</script> </script>
<template> <template>
<button <button
v-tooltip
:disabled="isLoading" :disabled="isLoading"
:title="title"
type="button" type="button"
class="btn d-none d-sm-none d-md-block" class="btn d-none d-sm-none d-md-block"
@click="onClick" @click="onClick"
> >
   
<span v-if="isLastDeployment"> <icon
{{ s__("Environments|Re-deploy") }} v-if="isLastDeployment"
</span> name="repeat" />
<span v-else> <icon
{{ s__("Environments|Rollback") }} v-else
</span> name="redo"/>
   
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</button> </button>
Loading
Loading
<script> <script>
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
*/ */
   
import $ from 'jquery'; import $ from 'jquery';
import eventHub from '../event_hub'; import Icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { s__ } from '~/locale';
import tooltip from '../../vue_shared/directives/tooltip'; import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
   
export default { export default {
components: { components: {
loadingIcon, Icon,
}, LoadingButton,
},
   
directives: { directives: {
tooltip, tooltip,
}, },
   
props: { props: {
stopUrl: { environment: {
type: String, type: Object,
default: '', required: true,
},
}, },
},
   
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
   
computed: { computed: {
title() { title() {
return 'Stop'; return s__('Environments|Stop environment');
},
}, },
},
   
methods: { mounted() {
onClick() { eventHub.$on('stopEnvironment', this.onStopEnvironment);
// eslint-disable-next-line no-alert },
if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
   
$(this.$el).tooltip('dispose'); beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
   
eventHub.$emit('postAction', this.stopUrl); methods: {
} onClick() {
}, $(this.$el).tooltip('dispose');
eventHub.$emit('requestStopEnvironment', this.environment);
},
onStopEnvironment(environment) {
if (this.environment.id === environment.id) {
this.isLoading = true;
}
}, },
}; },
};
</script> </script>
<template> <template>
<button <loading-button
v-tooltip v-tooltip
:disabled="isLoading" :loading="isLoading"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
type="button" container-class="btn btn-danger d-none d-sm-none d-md-block"
class="btn stop-env-link d-none d-sm-none d-md-block"
data-container="body" data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick" @click="onClick"
> >
<i <icon name="stop"/>
class="fa fa-stop stop-env-icon" </loading-button>
aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" />
</button>
</template> </template>
<script> <script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
   
export default { export default {
components: { components: {
Icon, Icon,
},
directives: {
tooltip,
},
props: {
terminalPath: {
type: String,
required: false,
default: '',
}, },
directives: { },
tooltip, computed: {
title() {
return 'Terminal';
}, },
props: { },
terminalPath: { };
type: String,
required: false,
default: '',
},
},
computed: {
title() {
return 'Terminal';
},
},
};
</script> </script>
<template> <template>
<a <a
Loading
@@ -36,9 +36,6 @@
Loading
@@ -36,9 +36,6 @@
class="btn terminal-button d-none d-sm-none d-md-block" class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body" data-container="body"
> >
<icon <icon name="terminal" />
:size="12"
name="terminal"
/>
</a> </a>
</template> </template>
Loading
@@ -5,10 +5,12 @@
Loading
@@ -5,10 +5,12 @@
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue';
   
export default { export default {
components: { components: {
emptyState, emptyState,
StopEnvironmentModal,
}, },
   
mixins: [ mixins: [
Loading
@@ -90,6 +92,8 @@
Loading
@@ -90,6 +92,8 @@
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div class="top-area"> <div class="top-area">
<tabs <tabs
:tabs="tabs" :tabs="tabs"
Loading
Loading
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
export default {
id: 'stop-environment-modal',
name: 'StopEnvironmentModal',
components: {
GlModal,
LoadingButton,
},
directives: {
tooltip,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
noStopActionMessage() {
return sprintf(
s__(
`Environments|Note that this action will stop the environment,
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
due to no “stop environment action” being defined
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
ciConfigLinkStart:
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
ciConfigLinkEnd: '</a>',
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('stopEnvironment', this.environment);
},
},
};
</script>
<template>
<gl-modal
:id="$options.id"
:footer-primary-button-text="s__('Environments|Stop environment')"
footer-primary-button-variant="danger"
@submit="onSubmit"
>
<template slot="header">
<h4
class="modal-title d-flex mw-100"
>
Stopping
<span
v-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
?
</h4>
</template>
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
<div
v-if="!environment.has_stop_action"
class="warning_message"
>
<p v-html="noStopActionMessage"></p>
<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') }}</a>
</div>
</gl-modal>
</template>
<script> <script>
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
   
export default { export default {
components: {
StopEnvironmentModal,
},
mixins: [ mixins: [
environmentsMixin, environmentsMixin,
CIPaginationMixin, CIPaginationMixin,
], ],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
Loading
@@ -38,6 +44,8 @@
Loading
@@ -38,6 +44,8 @@
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div <div
v-if="!isLoading" v-if="!isLoading"
class="top-area" class="top-area"
Loading
Loading
Loading
@@ -40,6 +40,7 @@ export default {
Loading
@@ -40,6 +40,7 @@ export default {
scope: getParameterByName('scope') || 'available', scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
environmentInStopModal: {},
}; };
}, },
   
Loading
@@ -85,7 +86,7 @@ export default {
Loading
@@ -85,7 +86,7 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.')); Flash(s__('Environments|An error occurred while fetching the environments.'));
}, },
   
postAction(endpoint) { postAction({ endpoint, errorMessage }) {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
   
Loading
@@ -93,7 +94,7 @@ export default {
Loading
@@ -93,7 +94,7 @@ export default {
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
Flash(s__('Environments|An error occurred while making the request.')); Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
}); });
} }
}, },
Loading
@@ -106,6 +107,15 @@ export default {
Loading
@@ -106,6 +107,15 @@ export default {
.catch(this.errorCallback); .catch(this.errorCallback);
}, },
   
updateStopModal(environment) {
this.environmentInStopModal = environment;
},
stopEnvironment(environment) {
const endpoint = environment.stop_path;
const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again');
this.postAction({ endpoint, errorMessage });
},
}, },
   
computed: { computed: {
Loading
@@ -162,9 +172,13 @@ export default {
Loading
@@ -162,9 +172,13 @@ export default {
}); });
   
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
}, },
   
beforeDestroyed() { beforeDestroy() {
eventHub.$off('postAction'); eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
}, },
}; };
Loading
@@ -13,7 +13,7 @@ export default class EnvironmentsService {
Loading
@@ -13,7 +13,7 @@ export default class EnvironmentsService {
   
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
postAction(endpoint) { postAction(endpoint) {
return axios.post(endpoint, {}, { emulateJSON: true }); return axios.post(endpoint, {});
} }
   
getFolderContent(folderUrl) { getFolderContent(folderUrl) {
Loading
Loading
Loading
@@ -23,7 +23,7 @@
Loading
@@ -23,7 +23,7 @@
} }
   
.btn-group { .btn-group {
> a { > .btn:not(.btn-danger) {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
   
Loading
Loading
Loading
@@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Loading
@@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_environment! before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_stop_environment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
Loading
@@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Loading
@@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def environment def environment
@environment ||= project.environments.find(params[:id]) @environment ||= project.environments.find(params[:id])
end end
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
end end
Loading
@@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Loading
@@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
deployment = environment.first_deployment_for(@merge_request.diff_head_sha) deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
   
stop_url = stop_url =
if environment.stop_action? && can?(current_user, :create_deployment, environment) if can?(current_user, :stop_environment, environment)
stop_project_environment_path(project, environment) stop_project_environment_path(project, environment)
end end
   
Loading
Loading
class EnvironmentPolicy < BasePolicy class EnvironmentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
   
condition(:stop_action_allowed) do condition(:stop_with_deployment_allowed) do
@subject.stop_action? && can?(:update_build, @subject.stop_action) @subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action)
end end
   
rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment condition(:stop_with_update_allowed) do
!@subject.stop_action? && can?(:update_environment, @subject)
end
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
end end
Loading
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
Loading
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url expose :external_url
expose :environment_type expose :environment_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action? expose :stop_action?, as: :has_stop_action
   
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_project_environment_path(environment.project, environment) metrics_project_environment_path(environment.project, environment)
Loading
@@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity
Loading
@@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity
end end
   
expose :created_at, :updated_at expose :created_at, :updated_at
expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment)
end
private
def current_user
request.current_user
end
end end
Loading
@@ -3,13 +3,12 @@
Loading
@@ -3,13 +3,12 @@
- if actions.present? - if actions.present?
.btn-group .btn-group
.dropdown .dropdown
%button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } %button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') }
= custom_icon('icon_play') = sprite_icon('play')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right %ul.dropdown-menu.dropdown-menu-right
- actions.each do |action| - actions.each do |action|
- next unless can?(current_user, :update_build, action) - next unless can?(current_user, :update_build, action)
%li %li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do
= custom_icon('icon_play')
%span= action.name.humanize %span= action.name.humanize
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last? - if deployment.last?
= _("Re-deploy") = sprite_icon('repeat')
- else - else
= _("Rollback") = sprite_icon('redo')
- if environment.external_url && can?(current_user, :read_environment, environment) - if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do
= sprite_icon('external-link') = sprite_icon('external-link')
View deployment View deployment
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment