@spotinst/spinnaker-deck
Version:
Spinnaker-Deck service, forked with support to Spotinst
460 lines (404 loc) • 15.7 kB
text/typescript
import { IController, IScope, module } from 'angular';
import { IModalService } from 'angular-ui-bootstrap';
import { cloneDeep, map, mapValues, reduce } from 'lodash';
import {
Application,
ConfirmationModalService,
IConfirmationModalParams,
ILoadBalancer,
IServerGroup,
ITaskMonitorConfig,
SERVER_GROUP_WRITER,
ServerGroupReader,
ServerGroupWarningMessageService,
ServerGroupWriter,
} from '@spinnaker/core';
import { AppengineHealth } from 'appengine/common/appengineHealth';
import { IAppengineLoadBalancer, IAppengineServerGroup } from 'appengine/domain/index';
import { AppengineServerGroupCommandBuilder } from '../configure/serverGroupCommandBuilder.service';
import { APPENGINE_SERVER_GROUP_WRITER, AppengineServerGroupWriter } from '../writer/serverGroup.write.service';
interface IPrivateScope extends IScope {
$$destroyed: boolean;
}
interface IServerGroupFromStateParams {
accountId: string;
region: string;
name: string;
}
class AppengineServerGroupDetailsController implements IController {
public state = { loading: true };
public serverGroup: IAppengineServerGroup;
private static buildExpectedAllocationsTable(expectedAllocations: { [key: string]: number }): string {
const tableRows = map(expectedAllocations, (allocation, serverGroupName) => {
return `
<tr>
<td>${serverGroupName}</td>
<td>${allocation * 100}%</td>
</tr>`;
}).join('');
return `
<table class="table table-condensed">
<thead>
<tr>
<th>Server Group</th>
<th>Allocation</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>`;
}
public static $inject = [
'$state',
'$scope',
'$uibModal',
'serverGroup',
'app',
'serverGroupWriter',
'appengineServerGroupWriter',
'appengineServerGroupCommandBuilder',
];
constructor(
private $state: any,
private $scope: IPrivateScope,
private $uibModal: IModalService,
serverGroup: IServerGroupFromStateParams,
public app: Application,
private serverGroupWriter: ServerGroupWriter,
private appengineServerGroupWriter: AppengineServerGroupWriter,
private appengineServerGroupCommandBuilder: AppengineServerGroupCommandBuilder,
) {
this.app
.ready()
.then(() => this.extractServerGroup(serverGroup))
.then(() => {
if (!this.$scope.$$destroyed) {
this.app.getDataSource('serverGroups').onRefresh(this.$scope, () => this.extractServerGroup(serverGroup));
}
})
.catch(() => this.autoClose());
}
public canDisableServerGroup(): boolean {
if (this.serverGroup) {
if (this.serverGroup.disabled) {
return false;
}
const expectedAllocations = this.expectedAllocationsAfterDisableOperation(this.serverGroup, this.app);
if (expectedAllocations) {
return Object.keys(expectedAllocations).length > 0;
} else {
return false;
}
} else {
return false;
}
}
public canDestroyServerGroup(): boolean {
if (this.serverGroup) {
if (this.serverGroup.disabled) {
return true;
}
const expectedAllocations = this.expectedAllocationsAfterDisableOperation(this.serverGroup, this.app);
if (expectedAllocations) {
return Object.keys(expectedAllocations).length > 0;
} else {
return false;
}
} else {
return false;
}
}
public destroyServerGroup(): void {
const stateParams = {
name: this.serverGroup.name,
accountId: this.serverGroup.account,
region: this.serverGroup.region,
};
const taskMonitor = {
application: this.app,
title: 'Destroying ' + this.serverGroup.name,
onTaskComplete: () => {
if (this.$state.includes('**.serverGroup', stateParams)) {
this.$state.go('^');
}
},
};
const submitMethod = (params: any) => this.serverGroupWriter.destroyServerGroup(this.serverGroup, this.app, params);
const confirmationModalParams = {
header: 'Really destroy ' + this.serverGroup.name + '?',
buttonText: 'Destroy ' + this.serverGroup.name,
account: this.serverGroup.account,
taskMonitorConfig: taskMonitor,
submitMethod,
askForReason: true,
platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride,
platformHealthType: AppengineHealth.PLATFORM,
body: this.getBodyTemplate(this.serverGroup, this.app),
interestingHealthProviderNames: [] as string[],
};
if (this.app.attributes.platformHealthOnlyShowOverride && this.app.attributes.platformHealthOnly) {
confirmationModalParams.interestingHealthProviderNames = [AppengineHealth.PLATFORM];
}
ConfirmationModalService.confirm(confirmationModalParams);
}
public enableServerGroup(): void {
const taskMonitor: ITaskMonitorConfig = {
application: this.app,
title: 'Enabling ' + this.serverGroup.name,
};
const submitMethod = (params: any) =>
this.serverGroupWriter.enableServerGroup(this.serverGroup, this.app, { ...params });
const modalBody = `<div class="well well-sm">
<p>
Enabling <b>${this.serverGroup.name}</b> will set its traffic allocation for
<b>${this.serverGroup.loadBalancers[0]}</b> to 100%.
</p>
<p>
If you would like more fine-grained control over your server groups' allocations,
edit <b>${this.serverGroup.loadBalancers[0]}</b> under the <b>Load Balancers</b> tab.
</p>
</div>
`;
const confirmationModalParams = {
header: 'Really enable ' + this.serverGroup.name + '?',
buttonText: 'Enable ' + this.serverGroup.name,
body: modalBody,
account: this.serverGroup.account,
taskMonitorConfig: taskMonitor,
platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride,
platformHealthType: AppengineHealth.PLATFORM,
submitMethod,
askForReason: true,
interestingHealthProviderNames: [] as string[],
};
if (this.app.attributes.platformHealthOnlyShowOverride && this.app.attributes.platformHealthOnly) {
confirmationModalParams.interestingHealthProviderNames = [AppengineHealth.PLATFORM];
}
ConfirmationModalService.confirm(confirmationModalParams);
}
public disableServerGroup(): void {
const taskMonitor = {
application: this.app,
title: 'Disabling ' + this.serverGroup.name,
};
const submitMethod = (params: any) =>
this.serverGroupWriter.disableServerGroup(this.serverGroup, this.app.name, params);
const expectedAllocations = this.expectedAllocationsAfterDisableOperation(this.serverGroup, this.app);
const modalBody = `<div class="well well-sm">
<p>
For App Engine, a disable operation sets this server group's allocation
to 0% and sets the other enabled server groups' allocations to their relative proportions
before the disable operation. The approximate allocations that will result from this operation are shown below.
</p>
<p>
If you would like more fine-grained control over your server groups' allocations,
edit <b>${this.serverGroup.loadBalancers[0]}</b> under the <b>Load Balancers</b> tab.
</p>
<div class="row">
<div class="col-md-12">
${AppengineServerGroupDetailsController.buildExpectedAllocationsTable(expectedAllocations)}
</div>
</div>
</div>
`;
const confirmationModalParams = {
header: 'Really disable ' + this.serverGroup.name + '?',
buttonText: 'Disable ' + this.serverGroup.name,
body: modalBody,
account: this.serverGroup.account,
taskMonitorConfig: taskMonitor,
platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride,
platformHealthType: AppengineHealth.PLATFORM,
submitMethod,
askForReason: true,
interestingHealthProviderNames: [] as string[],
};
if (this.app.attributes.platformHealthOnlyShowOverride && this.app.attributes.platformHealthOnly) {
confirmationModalParams.interestingHealthProviderNames = [AppengineHealth.PLATFORM];
}
ConfirmationModalService.confirm(confirmationModalParams);
}
public stopServerGroup(): void {
const taskMonitor = {
application: this.app,
title: 'Stopping ' + this.serverGroup.name,
};
const submitMethod = () => this.appengineServerGroupWriter.stopServerGroup(this.serverGroup, this.app);
let modalBody: string;
if (!this.serverGroup.disabled) {
modalBody = `<div class="alert alert-danger">
<p>Stopping this server group will scale it down to zero instances.</p>
<p>
This server group is currently serving traffic from <b>${this.serverGroup.loadBalancers[0]}</b>.
Traffic directed to this server group after it has been stopped will not be handled.
</p>
</div>`;
}
const confirmationModalParams = {
header: 'Really stop ' + this.serverGroup.name + '?',
buttonText: 'Stop ' + this.serverGroup.name,
account: this.serverGroup.account,
body: modalBody,
platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride,
platformHealthType: AppengineHealth.PLATFORM,
taskMonitorConfig: taskMonitor,
submitMethod,
askForReason: true,
};
ConfirmationModalService.confirm(confirmationModalParams);
}
public startServerGroup(): void {
const taskMonitor = {
application: this.app,
title: 'Starting ' + this.serverGroup.name,
};
const submitMethod = () => this.appengineServerGroupWriter.startServerGroup(this.serverGroup, this.app);
const confirmationModalParams = {
header: 'Really start ' + this.serverGroup.name + '?',
buttonText: 'Start ' + this.serverGroup.name,
account: this.serverGroup.account,
platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride,
platformHealthType: AppengineHealth.PLATFORM,
taskMonitorConfig: taskMonitor,
submitMethod,
askForReason: true,
};
ConfirmationModalService.confirm(confirmationModalParams);
}
public cloneServerGroup(): void {
this.$uibModal.open({
templateUrl: require('../configure/wizard/serverGroupWizard.html'),
controller: 'appengineCloneServerGroupCtrl as ctrl',
size: 'lg',
resolve: {
title: () => 'Clone ' + this.serverGroup.name,
application: () => this.app,
serverGroup: () => this.serverGroup,
serverGroupCommand: () =>
this.appengineServerGroupCommandBuilder.buildServerGroupCommandFromExisting(this.app, this.serverGroup),
},
});
}
public canStartServerGroup(): boolean {
if (this.canStartOrStopServerGroup()) {
return this.serverGroup.servingStatus === 'STOPPED';
} else {
return false;
}
}
public canStopServerGroup(): boolean {
if (this.canStartOrStopServerGroup()) {
return this.serverGroup.servingStatus === 'SERVING';
} else {
return false;
}
}
private canStartOrStopServerGroup(): boolean {
const isFlex = this.serverGroup.env === 'FLEXIBLE';
return isFlex || ['MANUAL', 'BASIC'].includes(this.serverGroup.scalingPolicy?.type);
}
private getBodyTemplate(serverGroup: IAppengineServerGroup, app: Application): string {
let template = '';
const params: IConfirmationModalParams = {};
ServerGroupWarningMessageService.addDestroyWarningMessage(app, serverGroup, params);
if (params.body) {
template += params.body;
}
if (!serverGroup.disabled) {
const expectedAllocations = this.expectedAllocationsAfterDisableOperation(serverGroup, app);
template += `
<div class="well well-sm">
<p>
A destroy operation will first disable this server group.
</p>
<p>
For App Engine, a disable operation sets this server group's allocation
to 0% and sets the other enabled server groups' allocations to their relative proportions
before the disable operation. The approximate allocations that will result from this operation are shown below.
</p>
<p>
If you would like more fine-grained control over your server groups' allocations,
edit <b>${serverGroup.loadBalancers[0]}</b> under the <b>Load Balancers</b> tab.
</p>
<div class="row">
<div class="col-md-12">
${AppengineServerGroupDetailsController.buildExpectedAllocationsTable(expectedAllocations)}
</div>
</div>
</div>
`;
}
return template;
}
private expectedAllocationsAfterDisableOperation(
serverGroup: IServerGroup,
app: Application,
): { [key: string]: number } {
const loadBalancer = app.getDataSource('loadBalancers').data.find((toCheck: IAppengineLoadBalancer): boolean => {
const allocations = toCheck.split?.allocations ?? {};
const enabledServerGroups = Object.keys(allocations);
return enabledServerGroups.includes(serverGroup.name);
});
if (loadBalancer) {
let allocations = cloneDeep(loadBalancer.split.allocations);
delete allocations[serverGroup.name];
const denominator = reduce(allocations, (partialSum: number, allocation: number) => partialSum + allocation, 0);
const precision = loadBalancer.split.shardBy === 'COOKIE' ? 1000 : 100;
allocations = mapValues(
allocations,
(allocation) => Math.round((allocation / denominator) * precision) / precision,
);
return allocations;
} else {
return null;
}
}
private autoClose(): void {
if (this.$scope.$$destroyed) {
return;
} else {
this.$state.params.allowModalToStayOpen = true;
this.$state.go('^', null, { location: 'replace' });
}
}
private extractServerGroup(fromParams: IServerGroupFromStateParams): PromiseLike<void> {
return ServerGroupReader.getServerGroup(
this.app.name,
fromParams.accountId,
fromParams.region,
fromParams.name,
).then((serverGroupDetails: IServerGroup) => {
let fromApp = this.app.getDataSource('serverGroups').data.find((toCheck: IServerGroup) => {
return (
toCheck.name === fromParams.name &&
toCheck.account === fromParams.accountId &&
toCheck.region === fromParams.region
);
});
if (!fromApp) {
this.app.getDataSource('loadBalancers').data.some((loadBalancer: ILoadBalancer) => {
if (loadBalancer.account === fromParams.accountId) {
return loadBalancer.serverGroups.some((toCheck: IServerGroup) => {
let result = false;
if (toCheck.name === fromParams.name) {
fromApp = toCheck;
result = true;
}
return result;
});
} else {
return false;
}
});
}
this.serverGroup = { ...serverGroupDetails, ...fromApp };
this.state.loading = false;
});
}
}
export const APPENGINE_SERVER_GROUP_DETAILS_CTRL = 'spinnaker.appengine.serverGroup.details.controller';
module(APPENGINE_SERVER_GROUP_DETAILS_CTRL, [APPENGINE_SERVER_GROUP_WRITER, SERVER_GROUP_WRITER]).controller(
'appengineServerGroupDetailsCtrl',
AppengineServerGroupDetailsController,
);