Commit 837619d3 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'move_deployment_boards_to_core' into 'master'

[RUN AS-IF-FOSS] Move deployment boards frontend to core

See merge request gitlab-org/gitlab!47147
parents 91c66b41 d200c064
...@@ -18,7 +18,8 @@ import { ...@@ -18,7 +18,8 @@ import {
GlTooltipDirective, GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml, GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui'; } from '@gitlab/ui';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg'; import deployBoardSvg from 'empty_states/icons/_deploy_board.svg';
import instanceComponent from '~/vue_shared/components/deployment_instance.vue';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATUS_MAP, CANARY_STATUS } from '../constants'; import { STATUS_MAP, CANARY_STATUS } from '../constants';
...@@ -26,7 +27,7 @@ import CanaryIngress from './canary_ingress.vue'; ...@@ -26,7 +27,7 @@ import CanaryIngress from './canary_ingress.vue';
export default { export default {
components: { components: {
instanceComponent: () => import('ee_component/vue_shared/components/deployment_instance.vue'), instanceComponent,
CanaryIngress, CanaryIngress,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
......
...@@ -6,16 +6,18 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -6,16 +6,18 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp'; import { flow, reverse, sortBy } from 'lodash/fp';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import EnvironmentItem from './environment_item.vue'; import EnvironmentItem from './environment_item.vue';
import DeployBoard from './deploy_board.vue';
import CanaryUpdateModal from './canary_update_modal.vue';
import CanaryDeploymentCallout from './canary_deployment_callout.vue';
export default { export default {
components: { components: {
EnvironmentItem, EnvironmentItem,
GlLoadingIcon, GlLoadingIcon,
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'), DeployBoard,
CanaryDeploymentCallout: () => CanaryDeploymentCallout,
import('ee_component/environments/components/canary_deployment_callout.vue'),
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'), CanaryUpdateModal,
}, },
props: { props: {
environments: { environments: {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
*/ */
import { isEqual, isFunction, omitBy } from 'lodash'; import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
......
import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers'; import { setDeployBoard } from './helpers';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
/** /**
...@@ -81,6 +81,17 @@ export default class EnvironmentsStore { ...@@ -81,6 +81,17 @@ export default class EnvironmentsStore {
this.state.environments = filteredEnvironments; this.state.environments = filteredEnvironments;
/**
* Add the canary callout banner underneath the second environment listed.
*
* If there is only one environment, then add to it underneath the first.
*/
if (this.state.environments.length >= 2) {
this.state.environments[1].showCanaryCallout = true;
} else if (this.state.environments.length === 1) {
this.state.environments[0].showCanaryCallout = true;
}
return filteredEnvironments; return filteredEnvironments;
} }
...@@ -135,12 +146,22 @@ export default class EnvironmentsStore { ...@@ -135,12 +146,22 @@ export default class EnvironmentsStore {
/** /**
* Toggles deploy board visibility for the provided environment ID. * Toggles deploy board visibility for the provided environment ID.
* Currently only works on EE.
* *
* @param {Object} environment * @param {Object} environment
* @return {Array} * @return {Array}
*/ */
toggleDeployBoard() { toggleDeployBoard(environmentID) {
const environments = this.state.environments.slice();
this.state.environments = environments.map((env) => {
let updated = { ...env };
if (env.id === environmentID) {
updated = { ...updated, isDeployBoardVisible: !env.isDeployBoardVisible };
}
return updated;
});
return this.state.environments; return this.state.environments;
} }
......
/** /**
* Deploy boards are EE only.
*
* @param {Object} environment * @param {Object} environment
* @returns {Object} * @returns {Object}
*/ */
export const setDeployBoard = (oldEnvironmentState, environment) => environment; export const setDeployBoard = (oldEnvironmentState, environment) => {
let parsedEnvironment = environment;
if (environment.size === 1 && environment.rollout_status) {
parsedEnvironment = {
...environment,
hasDeployBoard: true,
isDeployBoardVisible:
oldEnvironmentState.isDeployBoardVisible === false
? oldEnvironmentState.isDeployBoardVisible
: true,
deployBoardData:
environment.rollout_status.status === 'found' ? environment.rollout_status : {},
isLoadingDeployBoard: environment.rollout_status.status === 'loading',
isEmptyDeployBoard: environment.rollout_status.status === 'not_found',
};
}
return parsedEnvironment;
};
...@@ -129,3 +129,146 @@ ...@@ -129,3 +129,146 @@
width: 38px; width: 38px;
} }
} }
/**
* Deploy boards
*/
.deploy-board {
background-color: var(--gray-50, $gray-50);
min-height: 20px;
> .loading-icon,
> .deploy-board-empty,
> .deploy-board-information {
padding: 10px;
}
.deploy-board-information {
display: flex;
justify-content: space-between;
.deploy-board-status {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 0 5px;
}
.deploy-board-instances {
order: 2;
margin-left: 20px;
width: 100%;
}
.deploy-board-canary-ingress {
order: 7;
}
.deploy-board-actions {
order: 3;
align-self: center;
min-width: 150px;
margin-left: 10px;
}
&.deploy-board-error-message {
justify-content: center;
}
.deploy-board-empty-state-svg {
order: 1;
width: 90px;
margin: auto 0 auto 20px;
}
.deploy-board-empty-state-text {
order: 2;
flex-wrap: wrap;
margin: auto auto 15px 0;
}
.deploy-board-empty-state-title {
order: 1;
font-size: 17px;
line-height: 40px;
}
}
.deploy-board-legend .legend-text {
color: var(--gray-900, $gray-900);
font-size: $gl-font-size-small;
font-weight: $gl-font-weight-bold;
line-height: $gl-line-height-14;
}
}
.deploy-board-icon {
display: none;
@include media-breakpoint-up(md) {
float: left;
display: block;
}
i {
cursor: pointer;
color: var(--gray-200, $gray-200);
padding-right: 10px;
}
}
.canary-deployment-callout {
border-bottom: 1px solid var(--gray-500, $gray-500);
display: flex;
@include media-breakpoint-down(sm) {
display: none;
}
&-lock {
height: 82px;
width: 92px;
}
&-message {
max-width: 600px;
color: var(--gray-500, $gray-500);
}
&-close {
color: var(--gray-500, $gray-500);
cursor: pointer;
}
&-button {
border-color: var(--blue-500, $blue-500);
color: var(--blue-500, $blue-500);
&:not(:disabled):not(.disabled):active {
background-color: var(--blue-200, $blue-200);
border: 2px solid var(--blue-600, $blue-600);
color: var(--blue-700, $blue-700);
height: 34px;
padding: 5px 9px;
}
&:focus {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
box-shadow: 0 0 4px 1px var(--blue-200, $blue-200);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
&:hover {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
}
}
...@@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate) push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
end end
before_action do
push_frontend_feature_flag(:canary_ingress_weight_control, default_enabled: true)
end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect] before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop] before_action :authorize_stop_environment!, only: [:stop]
......
...@@ -236,6 +236,7 @@ class ProjectPolicy < BasePolicy ...@@ -236,6 +236,7 @@ class ProjectPolicy < BasePolicy
enable :read_commit_status enable :read_commit_status
enable :read_build enable :read_build
enable :read_container_image enable :read_container_image
enable :read_deploy_board
enable :read_pipeline enable :read_pipeline
enable :read_pipeline_schedule enable :read_pipeline_schedule
enable :read_environment enable :read_environment
......
...@@ -19,6 +19,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -19,6 +19,7 @@ class EnvironmentEntity < Grape::Entity
expose :name_without_type expose :name_without_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action expose :stop_action_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :upcoming_deployment, expose_nil: false do |environment, ops| expose :upcoming_deployment, expose_nil: false do |environment, ops|
DeploymentEntity.represent(environment.upcoming_deployment, DeploymentEntity.represent(environment.upcoming_deployment,
...@@ -104,6 +105,10 @@ class EnvironmentEntity < Grape::Entity ...@@ -104,6 +105,10 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project) can?(current_user, :read_pod_logs, environment.project)
end end
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
def cluster_platform_kubernetes? def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes) deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end end
......
---
title: Move deploy boards to Core
merge_request: 47147
author:
type: changed
...@@ -141,7 +141,7 @@ Its feature set is listed on the table below according to DevOps stages. ...@@ -141,7 +141,7 @@ Its feature set is listed on the table below according to DevOps stages.
| **Release** | | | **Release** | |
| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. | | [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | | [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
| [Canary Deployments](../user/project/canary_deployments.md) **(PREMIUM)** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. | | [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
| [Deploy Boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. | | [Deploy Boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [Feature Flags](../operations/feature_flags.md) **(PREMIUM)** | Deploy your features behind Feature Flags. | | [Feature Flags](../operations/feature_flags.md) **(PREMIUM)** | Deploy your features behind Feature Flags. |
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. | | [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
......
...@@ -147,7 +147,7 @@ according to each stage (Verify, Package, Release). ...@@ -147,7 +147,7 @@ according to each stage (Verify, Package, Release).
- Continuous Deployment, automatically deploying your app to production. - Continuous Deployment, automatically deploying your app to production.
- Continuous Delivery, manually click to deploy your app to production. - Continuous Delivery, manually click to deploy your app to production.
- Deploy static websites with [GitLab Pages](../../user/project/pages/index.md). - Deploy static websites with [GitLab Pages](../../user/project/pages/index.md).
- Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature with [Canary Deployments](../../user/project/canary_deployments.md). **(PREMIUM)** - Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature with [Canary Deployments](../../user/project/canary_deployments.md).
- Deploy your features behind [Feature Flags](../../operations/feature_flags.md). - Deploy your features behind [Feature Flags](../../operations/feature_flags.md).
- Add release notes to any Git tag with [GitLab Releases](../../user/project/releases/index.md). - Add release notes to any Git tag with [GitLab Releases](../../user/project/releases/index.md).
- View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](../../user/project/deploy_boards.md). - View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](../../user/project/deploy_boards.md).
......
...@@ -4,9 +4,10 @@ group: unassigned ...@@ -4,9 +4,10 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
--- ---
# Canary Deployments **(PREMIUM)** # Canary Deployments **(CORE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1659) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.1. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1659) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.1.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212320) to GitLab Core in 13.7.
A popular [Continuous Deployment](https://en.wikipedia.org/wiki/Continuous_deployment) A popular [Continuous Deployment](https://en.wikipedia.org/wiki/Continuous_deployment)
strategy, where a small portion of the fleet is updated to the new version of strategy, where a small portion of the fleet is updated to the new version of
......
...@@ -308,7 +308,7 @@ combined with either ...@@ -308,7 +308,7 @@ combined with either
### Integrations ### Integrations
#### Canary Deployments **(PREMIUM)** #### Canary Deployments
Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments) Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
and visualize your canary deployments right inside the Deploy Board, without and visualize your canary deployments right inside the Deploy Board, without
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlTable, GlEmptyState, GlLoadingIcon, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import { GlTable, GlEmptyState, GlLoadingIcon, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import deploymentInstance from '~/vue_shared/components/deployment_instance.vue';
export default { export default {
components: { components: {
...@@ -12,7 +13,7 @@ export default { ...@@ -12,7 +13,7 @@ export default {
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
deploymentInstance: () => import('ee_component/vue_shared/components/deployment_instance.vue'), deploymentInstance,
}, },
props: { props: {
isFetching: { isFetching: {
......
import CeEnvironmentsStore from '~/environments/stores/environments_store';
export default class EnvironmentsStore extends CeEnvironmentsStore {
storeEnvironments(environments = []) {
super.storeEnvironments(environments);
/**
* Add the canary callout banner underneath the second environment listed.
*
* If there is only one environment, then add to it underneath the first.
*/
if (this.state.environments.length >= 2) {
this.state.environments[1].showCanaryCallout = true;
} else if (this.state.environments.length === 1) {
this.state.environments[0].showCanaryCallout = true;
}
}
/**
* Toggles deploy board visibility for the provided environment ID.
*
* @param {Object} environment
* @return {Array}
*/
toggleDeployBoard(environmentID) {
const environments = this.state.environments.slice();
this.state.environments = environments.map((env) => {
let updated = { ...env };
if (env.id === environmentID) {
updated = { ...updated, isDeployBoardVisible: !env.isDeployBoardVisible };
}
return updated;
});
return this.state.environments;
}
}
/**
* Deploy boards are EE only.
*
* @param {Object} environment
* @returns {Object}
*/
export const setDeployBoard = (oldEnvironmentState, environment) => {
let parsedEnvironment = environment;
if (environment.size === 1 && environment.rollout_status) {
parsedEnvironment = {
...environment,
hasDeployBoard: true,
isDeployBoardVisible:
oldEnvironmentState.isDeployBoardVisible === false
? oldEnvironmentState.isDeployBoardVisible
: true,
deployBoardData:
environment.rollout_status.status === 'found' ? environment.rollout_status : {},
isLoadingDeployBoard: environment.rollout_status.status === 'loading',
isEmptyDeployBoard: environment.rollout_status.status === 'not_found',
};
}
return parsedEnvironment;
};
@import 'page_bundles/mixins_and_variables_and_functions'; @import '../../../../../app/assets/stylesheets/page_bundles/environments';
/**
* Deploy boards
*/
.deploy-board {
background-color: var(--gray-50, $gray-50);
min-height: 20px;
> .loading-icon,
> .deploy-board-empty,
> .deploy-board-information {
padding: 10px;
}
.deploy-board-information {
display: flex;
justify-content: space-between;
.deploy-board-status {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 0 5px;
}
.deploy-board-instances {
order: 2;
margin-left: 20px;
width: 100%;
}
.deploy-board-canary-ingress {
order: 7;
}
.deploy-board-actions {
order: 3;
align-self: center;
min-width: 150px;
margin-left: 10px;
}
&.deploy-board-error-message {
justify-content: center;
}
.deploy-board-empty-state-svg {
order: 1;
width: 90px;
margin: auto 0 auto 20px;
}
.deploy-board-empty-state-text {
order: 2;
flex-wrap: wrap;
margin: auto auto 15px 0;
}
.deploy-board-empty-state-title {
order: 1;
font-size: 17px;
line-height: 40px;
}
}
.deploy-board-legend .legend-text {
color: var(--gray-900, $gray-900);
font-size: $gl-font-size-small;
font-weight: $gl-font-weight-bold;
line-height: $gl-line-height-14;
}
}
.deploy-board-icon {
display: none;
@include media-breakpoint-up(md) {
float: left;
display: block;
}
i {
cursor: pointer;
color: var(--gray-200, $gray-200);
padding-right: 10px;
}
}
.alert-dropdown-button { .alert-dropdown-button {
margin-left: $btn-side-margin; margin-left: $btn-side-margin;
...@@ -132,57 +43,3 @@ ...@@ -132,57 +43,3 @@
display: flex; display: flex;
} }
} }
.canary-deployment-callout {
border-bottom: 1px solid var(--gray-500, $gray-500);
display: flex;
@include media-breakpoint-down(sm) {
display: none;
}
&-lock {
height: 82px;
width: 92px;
}
&-message {
max-width: 600px;
color: var(--gray-500, $gray-500);
}
&-close {
color: var(--gray-500, $gray-500);
cursor: pointer;
}
&-button {
border-color: var(--blue-500, $blue-500);
color: var(--blue-500, $blue-500);
&:not(:disabled):not(.disabled):active {
background-color: var(--blue-200, $blue-200);
border: 2px solid var(--blue-600, $blue-600);
color: var(--blue-700, $blue-700);
height: 34px;
padding: 5px 9px;
}
&:focus {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
box-shadow: 0 0 4px 1px var(--blue-200, $blue-200);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
&:hover {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
}
}
...@@ -7,9 +7,6 @@ module EE ...@@ -7,9 +7,6 @@ module EE
prepended do prepended do
before_action :authorize_create_environment_terminal!, only: [:terminal] before_action :authorize_create_environment_terminal!, only: [:terminal]
before_action do
push_frontend_feature_flag(:canary_ingress_weight_control, default_enabled: true)
end
end end
private private
......
...@@ -16,10 +16,7 @@ module EE ...@@ -16,10 +16,7 @@ module EE
THREAT_MONITORING_INFO = 'threat_monitoring_info' THREAT_MONITORING_INFO = 'threat_monitoring_info'
def show_canary_deployment_callout?(project) def show_canary_deployment_callout?(project)
!user_dismissed?(CANARY_DEPLOYMENT) && false
show_promotions? &&
# use :canary_deployments if we create a feature flag for it in the future
!project.feature_available?(:deploy_board)
end end
def render_enable_hashed_storage_warning def render_enable_hashed_storage_warning
......
...@@ -72,7 +72,6 @@ class License < ApplicationRecord ...@@ -72,7 +72,6 @@ class License < ApplicationRecord
db_load_balancing db_load_balancing
default_branch_protection_restriction_in_groups default_branch_protection_restriction_in_groups
default_project_deletion_protection default_project_deletion_protection
deploy_board
disable_name_update_for_users disable_name_update_for_users
email_additional_text email_additional_text
epics epics
...@@ -189,7 +188,6 @@ class License < ApplicationRecord ...@@ -189,7 +188,6 @@ class License < ApplicationRecord
# Add on codes that may occur in legacy licenses that don't have a plan yet. # Add on codes that may occur in legacy licenses that don't have a plan yet.
FEATURES_FOR_ADD_ONS = { FEATURES_FOR_ADD_ONS = {
'GitLab_Auditor_User' => :auditor_user, 'GitLab_Auditor_User' => :auditor_user,
'GitLab_DeployBoard' => :deploy_board,
'GitLab_FileLocks' => :file_locks, 'GitLab_FileLocks' => :file_locks,
'GitLab_Geo' => :geo 'GitLab_Geo' => :geo
}.freeze }.freeze
......
...@@ -12,9 +12,6 @@ module EE ...@@ -12,9 +12,6 @@ module EE
with_scope :subject with_scope :subject
condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) } condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) }
with_scope :subject
condition(:deploy_board_disabled) { !@subject.feature_available?(:deploy_board) }
with_scope :subject with_scope :subject
condition(:iterations_available) { @subject.feature_available?(:iterations) } condition(:iterations_available) { @subject.feature_available?(:iterations) }
...@@ -188,7 +185,6 @@ module EE ...@@ -188,7 +185,6 @@ module EE
rule { can?(:reporter_access) }.policy do rule { can?(:reporter_access) }.policy do
enable :admin_board enable :admin_board
enable :read_deploy_board
enable :admin_epic_issue enable :admin_epic_issue
enable :read_group_timelogs enable :read_group_timelogs
end end
...@@ -254,8 +250,6 @@ module EE ...@@ -254,8 +250,6 @@ module EE
rule { repository_mirrors_enabled & ((mirror_available & can?(:admin_project)) | admin) }.enable :admin_mirror rule { repository_mirrors_enabled & ((mirror_available & can?(:admin_project)) | admin) }.enable :admin_mirror
rule { deploy_board_disabled & ~is_development }.prevent :read_deploy_board
rule { can?(:maintainer_access) }.policy do rule { can?(:maintainer_access) }.policy do
enable :push_code_to_protected_branches enable :push_code_to_protected_branches
enable :admin_path_locks enable :admin_path_locks
......
...@@ -6,16 +6,11 @@ module EE ...@@ -6,16 +6,11 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: ::RolloutStatusEntity
expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
end end
private private
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
def can_read_alert_management_alert? def can_read_alert_management_alert?
can?(current_user, :read_alert_management_alert, environment.project) && can?(current_user, :read_alert_management_alert, environment.project) &&
environment.project.feature_available?(:environment_alerts) environment.project.feature_available?(:environment_alerts)
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Projects::EnvironmentsController do RSpec.describe Projects::EnvironmentsController do
include KubernetesHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
...@@ -18,64 +16,6 @@ RSpec.describe Projects::EnvironmentsController do ...@@ -18,64 +16,6 @@ RSpec.describe Projects::EnvironmentsController do
sign_in(user) sign_in(user)
end end
describe 'GET index' do
context 'when requesting JSON response for folders' do
before do
allow_any_instance_of(EE::Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(EE::Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
name: 'staging/review-2',
state: :available)
create(:environment, project: project,
name: 'staging/review-3',
state: :stopped)
end
let(:environments) { json_response['environments'] }
context 'when requesting available environments scope' do
before do
stub_licensed_features(deploy_board: true)
get :index, params: environment_params(format: :json, nested: true, scope: :available)
end
it 'responds with matching schema' do
expect(response).to match_response_schema('environments', dir: 'ee')
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status']).to be_present
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status']).to be_present
end
end
context 'when license does not have the GitLab_DeployBoard add-on' do
before do
stub_licensed_features(deploy_board: false)
get :index, params: environment_params(format: :json, nested: true)
end
it 'does not return the rollout_status_path attribute' do
expect(environments.first['latest']['rollout_status']).not_to be_present
expect(environments.second['latest']['rollout_status']).not_to be_present
end
end
end
end
describe '#GET terminal' do describe '#GET terminal' do
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) } let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import CanaryDeploymentBoard from 'ee/environments/components/canary_deployment_callout.vue'; import CanaryDeploymentBoard from '~/environments/components/canary_deployment_callout.vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue'; import DeployBoard from '~/environments/components/deploy_board.vue';
import EnvironmentsComponent from '~/environments/components/environments_app.vue'; import EnvironmentsComponent from '~/environments/components/environments_app.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { environment } from './mock_data'; import { environment } from './mock_data';
......
import Store from 'ee/environments/stores/environments_store';
import { serverDataList, deployBoardMockData } from './mock_data';
describe('Store', () => {
let store;
beforeEach(() => {
store = new Store();
});
it('should store a non folder environment with deploy board if rollout_status key is provided', () => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
rollout_status: deployBoardMockData,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
});
it('should keep deploy board data when updating environments', () => {
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
describe('canaryCallout', () => {
it('should add banner underneath the second environment', () => {
store.storeEnvironments(serverDataList);
expect(store.state.environments[1].showCanaryCallout).toEqual(true);
});
it('should add banner underneath first environment when only one environment', () => {
store.storeEnvironments(serverDataList.slice(0, 1));
expect(store.state.environments[0].showCanaryCallout).toEqual(true);
});
});
});
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import EnvironmentAlert from 'ee/environments/components/environment_alert.vue'; import EnvironmentAlert from 'ee/environments/components/environment_alert.vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryUpdateModal from 'ee/environments/components/canary_update_modal.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue'; import EnvironmentTable from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub';
import { deployBoardMockData } from './mock_data';
describe('Environment table', () => { describe('Environment table', () => {
let wrapper; let wrapper;
...@@ -25,125 +21,6 @@ describe('Environment table', () => { ...@@ -25,125 +21,6 @@ describe('Environment table', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('Should render a table', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
};
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.classes()).toContain('ci-table');
});
it('should render deploy board container when data is provided', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.js-deploy-board-row').exists()).toBe(true);
expect(wrapper.find('.deploy-board-icon').exists()).toBe(true);
});
it('should toggle deploy board visibility when arrow is clicked', (done) => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: {
instances: [{ status: 'ready', tooltip: 'foo' }],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
canary_ingress: { canary_weight: 60 },
},
isDeployBoardVisible: false,
};
eventHub.$on('toggleDeployBoard', (env) => {
expect(env.id).toEqual(mockItem.id);
done();
});
factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find('.deploy-board-icon').trigger('click');
});
it('should render canary callout', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
showCanaryCallout: true,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.canary-deployment-callout').exists()).toBe(true);
});
it('should render the alert if there is one', async () => { it('should render the alert if there is one', async () => {
const mockItem = { const mockItem = {
name: 'review', name: 'review',
...@@ -172,40 +49,4 @@ describe('Environment table', () => { ...@@ -172,40 +49,4 @@ describe('Environment table', () => {
expect(wrapper.find(EnvironmentAlert).exists()).toBe(true); expect(wrapper.find(EnvironmentAlert).exists()).toBe(true);
}); });
it('should set the enviornment to change and weight when a change canary weight event is recevied', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40);
await wrapper.vm.$nextTick();
expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({
weight: 40,
environment: mockItem,
});
});
}); });
export const deployBoardMockData = {
instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting', pod_name: 'production-tanuki-1' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
status: 'found',
canary_ingress: {
canary_weight: 50,
},
};
export const environment = { export const environment = {
name: 'production', name: 'production',
size: 1, size: 1,
......
...@@ -141,10 +141,6 @@ RSpec.describe EE::UserCalloutsHelper do ...@@ -141,10 +141,6 @@ RSpec.describe EE::UserCalloutsHelper do
end end
context 'when user needs to upgrade to canary deployments' do context 'when user needs to upgrade to canary deployments' do
before do
allow(project).to receive(:feature_available?).with(:deploy_board).and_return(false)
end
context 'when user has dismissed' do context 'when user has dismissed' do
before do before do
allow(helper).to receive(:user_dismissed?).and_return(true) allow(helper).to receive(:user_dismissed?).and_return(true)
...@@ -158,17 +154,8 @@ RSpec.describe EE::UserCalloutsHelper do ...@@ -158,17 +154,8 @@ RSpec.describe EE::UserCalloutsHelper do
allow(helper).to receive(:user_dismissed?).and_return(false) allow(helper).to receive(:user_dismissed?).and_return(false)
end end
it { is_expected.to be_truthy } it { is_expected.to be_falsey }
end
end
context 'when user already has access to canary deployments' do
before do
allow(project).to receive(:feature_available?).with(:deploy_board).and_return(true)
allow(helper).to receive(:user_dismissed?).and_return(false)
end end
it { is_expected.to be_falsey }
end end
end end
......
...@@ -481,7 +481,7 @@ RSpec.describe Namespace do ...@@ -481,7 +481,7 @@ RSpec.describe Namespace do
end end
context 'when feature not available in the plan' do context 'when feature not available in the plan' do
let(:feature) { :deploy_board } let(:feature) { :cluster_deployments }
let(:hosted_plan) { create(:bronze_plan) } let(:hosted_plan) { create(:bronze_plan) }
it 'returns false' do it 'returns false' do
......
...@@ -368,7 +368,7 @@ RSpec.describe License do ...@@ -368,7 +368,7 @@ RSpec.describe License do
it 'returns features for premium plan' do it 'returns features for premium plan' do
expect(described_class.features_for_plan('premium')) expect(described_class.features_for_plan('premium'))
.to include(:multiple_issue_assignees, :deploy_board, :file_locks, :group_wikis) .to include(:multiple_issue_assignees, :cluster_deployments, :file_locks, :group_wikis)
end end
it 'returns empty array if no features for given plan' do it 'returns empty array if no features for given plan' do
...@@ -377,7 +377,7 @@ RSpec.describe License do ...@@ -377,7 +377,7 @@ RSpec.describe License do
end end
describe '.plan_includes_feature?' do describe '.plan_includes_feature?' do
let(:feature) { :deploy_board } let(:feature) { :cluster_deployments }
subject { described_class.plan_includes_feature?(plan, feature) } subject { described_class.plan_includes_feature?(plan, feature) }
...@@ -735,15 +735,15 @@ RSpec.describe License do ...@@ -735,15 +735,15 @@ RSpec.describe License do
context 'with add-ons' do context 'with add-ons' do
it 'returns all available add-ons' do it 'returns all available add-ons' do
license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 2 }) license = build_license_with_add_ons({ 'GitLab_FileLocks' => 2 })
expect(license.features_from_add_ons).to match_array([:deploy_board, :file_locks]) expect(license.features_from_add_ons).to eq([:file_locks])
end end
end end
context 'with nil add-ons' do context 'with nil add-ons' do
it 'returns an empty array' do it 'returns an empty array' do
license = build_license_with_add_ons({ 'GitLab_DeployBoard' => nil, 'GitLab_FileLocks' => nil }) license = build_license_with_add_ons({ 'GitLab_FileLocks' => nil })
expect(license.features_from_add_ons).to eq([]) expect(license.features_from_add_ons).to eq([])
end end
...@@ -752,9 +752,9 @@ RSpec.describe License do ...@@ -752,9 +752,9 @@ RSpec.describe License do
describe '#feature_available?' do describe '#feature_available?' do
it 'returns true if add-on exists and have a quantity greater than 0' do it 'returns true if add-on exists and have a quantity greater than 0' do
license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 1 }) license = build_license_with_add_ons({ 'GitLab_FileLocks' => 1 })
expect(license.feature_available?(:deploy_board)).to eq(true) expect(license.feature_available?(:file_locks)).to eq(true)
end end
it 'returns true if the feature is included in the plan do' do it 'returns true if the feature is included in the plan do' do
...@@ -764,16 +764,15 @@ RSpec.describe License do ...@@ -764,16 +764,15 @@ RSpec.describe License do
end end
it 'returns false if add-on exists but have a quantity of 0' do it 'returns false if add-on exists but have a quantity of 0' do
license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 0 }) license = build_license_with_add_ons({ 'GitLab_FileLocks' => 0 })
expect(license.feature_available?(:deploy_board)).to eq(false) expect(license.feature_available?(:file_locks)).to eq(false)
end end
it 'returns false if add-on does not exists' do it 'returns false if add-on does not exists' do
license = build_license_with_add_ons({}) license = build_license_with_add_ons({})
expect(license.feature_available?(:deploy_board)).to eq(false) expect(license.feature_available?(:file_locks)).to eq(false)
expect(license.feature_available?(:auditor_user)).to eq(false)
end end
context 'with an expired trial license' do context 'with an expired trial license' do
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe EnvironmentEntity do RSpec.describe EnvironmentEntity do
include KubernetesHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:environment) { create(:environment) } let(:environment) { create(:environment) }
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -52,34 +50,6 @@ RSpec.describe EnvironmentEntity do ...@@ -52,34 +50,6 @@ RSpec.describe EnvironmentEntity do
end end
end end
context 'when deploy_boards are available' do
before do
stub_licensed_features(deploy_board: true)
end
context 'with deployment service ready' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
environment.project.add_maintainer(user)
end
it 'exposes rollout_status' do
expect(subject).to include(:rollout_status)
end
end
end
context 'when deploy_boards are not available' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
end
it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status)
end
end
context 'when environment has a review app' do context 'when environment has a review app' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) } let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
......
...@@ -8,9 +8,9 @@ module EE ...@@ -8,9 +8,9 @@ module EE
# #
# Example: # Example:
# #
# stub_licensed_features(geo: true, deploy_board: false) # stub_licensed_features(geo: true, file_locks: false)
# #
# This enables `geo` and disables `deploy_board` features for a spec. # This enables `geo` and disables `file_locks` features for a spec.
# Other features are still enabled/disabled as defined in the license. # Other features are still enabled/disabled as defined in the license.
prepended do prepended do
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::EnvironmentsController do RSpec.describe Projects::EnvironmentsController do
include MetricsDashboardHelpers include MetricsDashboardHelpers
include KubernetesHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user, name: 'main-dos').tap { |u| project.add_maintainer(u) } } let_it_be(:maintainer) { create(:user, name: 'main-dos').tap { |u| project.add_maintainer(u) } }
...@@ -34,6 +35,9 @@ RSpec.describe Projects::EnvironmentsController do ...@@ -34,6 +35,9 @@ RSpec.describe Projects::EnvironmentsController do
context 'when requesting JSON response for folders' do context 'when requesting JSON response for folders' do
before do before do
allow_any_instance_of(Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project, create(:environment, project: project,
name: 'staging/review-1', name: 'staging/review-1',
state: :available) state: :available)
...@@ -91,9 +95,11 @@ RSpec.describe Projects::EnvironmentsController do ...@@ -91,9 +95,11 @@ RSpec.describe Projects::EnvironmentsController do
it 'responds with a payload describing available environments' do it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2 expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production' expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status']).to be_present
expect(environments.second['name']).to eq 'staging' expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2 expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2' expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status']).to be_present
end end
it 'contains values describing environment scopes sizes' do it 'contains values describing environment scopes sizes' do
......
...@@ -17,6 +17,8 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do ...@@ -17,6 +17,8 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do
stub_kubeclient_pods(environment.deployment_namespace) stub_kubeclient_pods(environment.deployment_namespace)
stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0') stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0')
stub_kubeclient_deployments(environment.deployment_namespace)
stub_kubeclient_ingresses(environment.deployment_namespace)
stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url) stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url)
sign_in(project.owner) sign_in(project.owner)
......
...@@ -37,6 +37,12 @@ ...@@ -37,6 +37,12 @@
"has_opened_alert": { "type": "boolean" }, "has_opened_alert": { "type": "boolean" },
"cluster_type": { "type": "types/nullable_string.json" }, "cluster_type": { "type": "types/nullable_string.json" },
"terminal_path": { "type": "types/nullable_string.json" }, "terminal_path": { "type": "types/nullable_string.json" },
"rollout_status": {
"oneOf": [
{ "type": "null" },
{ "$ref": "rollout_status.json" }
]
},
"last_deployment": { "last_deployment": {
"oneOf": [ "oneOf": [
{ "type": "null" }, { "type": "null" },
......
{
"type": "object",
"additionalProperties": false,
"required": [
"status"
],
"properties": {
"status": {
"type": "string"
},
"completion": {
"type": "integer"
},
"is_completed": {
"type": "boolean"
},
"has_legacy_app_label": {
"type": "boolean"
},
"instances": {
"type": "array",
"items": {
"additionalProperties": false,
"type": "object",
"required": [
"status",
"pod_name",
"tooltip",
"track",
"stable"
],
"properties": {
"status": {
"type": "string"
},
"pod_name": {
"type": "string"
},
"tooltip": {
"type": "string"
},
"track": {
"type": "string"
},
"stable": {
"type": "boolean"
}
}
}
}
}
}
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui'; import { GlDropdownItem } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { CANARY_UPDATE_MODAL } from 'ee/environments/constants'; import { CANARY_UPDATE_MODAL } from '~/environments/constants';
import CanaryIngress from 'ee/environments/components/canary_ingress.vue'; import CanaryIngress from '~/environments/components/canary_ingress.vue';
describe('ee/environments/components/canary_ingress.vue', () => { describe('/environments/components/canary_ingress.vue', () => {
let wrapper; let wrapper;
const setWeightTo = (weightWrapper, x) => const setWeightTo = (weightWrapper, x) =>
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlAlert, GlModal } from '@gitlab/ui'; import { GlAlert, GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import CanaryUpdateModal from 'ee/environments/components/canary_update_modal.vue'; import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import updateCanaryIngress from 'ee/environments/graphql/mutations/update_canary_ingress.mutation.graphql'; import updateCanaryIngress from '~/environments/graphql/mutations/update_canary_ingress.mutation.graphql';
describe('ee/environments/components/canary_update_modal.vue', () => { describe('/environments/components/canary_update_modal.vue', () => {
let wrapper; let wrapper;
let modal; let modal;
let mutate; let mutate;
......
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue'; import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryIngress from 'ee/environments/components/canary_ingress.vue'; import CanaryIngress from '~/environments/components/canary_ingress.vue';
import { deployBoardMockData, environment } from './mock_data'; import { deployBoardMockData, environment } from './mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`; const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import EnvironmentTable from '~/environments/components/environments_table.vue'; import EnvironmentTable from '~/environments/components/environments_table.vue';
import { folder } from './mock_data'; import eventHub from '~/environments/event_hub';
import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import { folder, deployBoardMockData } from './mock_data';
const eeOnlyProps = { const eeOnlyProps = {
canaryDeploymentFeatureId: 'canary_deployment', canaryDeploymentFeatureId: 'canary_deployment',
...@@ -37,10 +40,161 @@ describe('Environment table', () => { ...@@ -37,10 +40,161 @@ describe('Environment table', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('Should render a table', () => { it('Should render a table', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
};
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.classes()).toContain('ci-table'); expect(wrapper.classes()).toContain('ci-table');
}); });
it('should render deploy board container when data is provided', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.js-deploy-board-row').exists()).toBe(true);
expect(wrapper.find('.deploy-board-icon').exists()).toBe(true);
});
it('should toggle deploy board visibility when arrow is clicked', (done) => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: {
instances: [{ status: 'ready', tooltip: 'foo' }],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
canary_ingress: { canary_weight: 60 },
},
isDeployBoardVisible: false,
};
eventHub.$on('toggleDeployBoard', (env) => {
expect(env.id).toEqual(mockItem.id);
done();
});
factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find('.deploy-board-icon').trigger('click');
});
it('should set the enviornment to change and weight when a change canary weight event is recevied', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40);
await wrapper.vm.$nextTick();
expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({
weight: 40,
environment: mockItem,
});
});
it('should render canary callout', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
showCanaryCallout: true,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.canary-deployment-callout').exists()).toBe(true);
});
describe('sortEnvironments', () => { describe('sortEnvironments', () => {
it('should sort environments by last updated', () => { it('should sort environments by last updated', () => {
const mockItems = [ const mockItems = [
......
...@@ -5,6 +5,8 @@ import EnableReviewAppModal from '~/environments/components/enable_review_app_mo ...@@ -5,6 +5,8 @@ import EnableReviewAppModal from '~/environments/components/enable_review_app_mo
import Container from '~/environments/components/container.vue'; import Container from '~/environments/components/container.vue';
import EmptyState from '~/environments/components/empty_state.vue'; import EmptyState from '~/environments/components/empty_state.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue'; import EnvironmentsApp from '~/environments/components/environments_app.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryDeploymentBoard from '~/environments/components/canary_deployment_callout.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { environment, folder } from './mock_data'; import { environment, folder } from './mock_data';
...@@ -36,6 +38,9 @@ describe('Environment', () => { ...@@ -36,6 +38,9 @@ describe('Environment', () => {
}); });
}; };
const canaryPromoKeyValue = () =>
wrapper.find(CanaryDeploymentBoard).attributes('data-js-canary-promo-key');
const createWrapper = (shallow = false, props = {}) => { const createWrapper = (shallow = false, props = {}) => {
const fn = shallow ? shallowMount : mount; const fn = shallow ? shallowMount : mount;
wrapper = extendedWrapper(fn(EnvironmentsApp, { propsData: { ...mockData, ...props } })); wrapper = extendedWrapper(fn(EnvironmentsApp, { propsData: { ...mockData, ...props } }));
...@@ -114,6 +119,57 @@ describe('Environment', () => { ...@@ -114,6 +119,57 @@ describe('Environment', () => {
expect(wrapper.vm.updateContent).toHaveBeenCalledTimes(0); expect(wrapper.vm.updateContent).toHaveBeenCalledTimes(0);
}); });
}); });
describe('deploy boards', () => {
beforeEach(() => {
const deployEnvironment = {
...environment,
rollout_status: {
status: 'found',
},
};
mockRequest(200, {
environments: [deployEnvironment],
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render deploy boards', () => {
expect(wrapper.find(DeployBoard).exists()).toBe(true);
});
it('should render arrow to open deploy boards', () => {
expect(
wrapper.find('.deploy-board-icon [data-testid="chevron-down-icon"]').exists(),
).toBe(true);
});
});
describe('canary callout with one environment', () => {
it('should render banner underneath first environment', () => {
expect(canaryPromoKeyValue()).toBe('0');
});
});
describe('canary callout with multiple environments', () => {
beforeEach(() => {
mockRequest(200, {
environments: [environment, environment],
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render banner underneath second environment', () => {
expect(canaryPromoKeyValue()).toBe('1');
});
});
}); });
}); });
......
import Store from '~/environments/stores/environments_store'; import Store from '~/environments/stores/environments_store';
import { environmentsList, serverData } from './mock_data'; import { environmentsList, serverData, deployBoardMockData } from './mock_data';
describe('Store', () => { describe('Store', () => {
let store; let store;
...@@ -160,4 +160,72 @@ describe('Store', () => { ...@@ -160,4 +160,72 @@ describe('Store', () => {
expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]); expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
}); });
}); });
it('should store a non folder environment with deploy board if rollout_status key is provided', () => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
rollout_status: deployBoardMockData,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
});
it('should keep deploy board data when updating environments', () => {
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
describe('canaryCallout', () => {
it('should add banner underneath the second environment', () => {
store.storeEnvironments(serverData);
expect(store.state.environments[1].showCanaryCallout).toEqual(true);
});
it('should add banner underneath first environment when only one environment', () => {
store.storeEnvironments(serverData.slice(0, 1));
expect(store.state.environments[0].showCanaryCallout).toEqual(true);
});
});
}); });
...@@ -26,6 +26,45 @@ const buildProps = { ...@@ -26,6 +26,45 @@ const buildProps = {
updated_at: '2017-02-01T19:42:18.400Z', updated_at: '2017-02-01T19:42:18.400Z',
}; };
const deployBoardMockData = {
instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting', pod_name: 'production-tanuki-1' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
status: 'found',
canary_ingress: {
canary_weight: 50,
},
};
const environment = { const environment = {
name: 'production', name: 'production',
size: 1, size: 1,
...@@ -262,4 +301,4 @@ const tableData = { ...@@ -262,4 +301,4 @@ const tableData = {
}, },
}; };
export { environment, environmentsList, folder, serverData, tableData }; export { environment, environmentsList, folder, serverData, tableData, deployBoardMockData };
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DeployBoardInstance from 'ee/vue_shared/components/deployment_instance.vue'; import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue';
import { folder } from './mock_data'; import { folder } from './mock_data';
describe('Deploy Board Instance', () => { describe('Deploy Board Instance', () => {
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe EnvironmentEntity do RSpec.describe EnvironmentEntity do
include KubernetesHelpers
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
let(:request) { double('request') } let(:request) { double('request', current_user: user, project: project) }
let(:entity) do let(:entity) do
described_class.new(environment, request: request) described_class.new(environment, request: request)
end end
...@@ -167,4 +168,23 @@ RSpec.describe EnvironmentEntity do ...@@ -167,4 +168,23 @@ RSpec.describe EnvironmentEntity do
end end
end end
end end
context 'with deployment service ready' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
end
it 'exposes rollout_status' do
expect(subject).to include(:rollout_status)
end
end
context 'with deployment service not ready' do
let(:user) { create(:user) }
it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment