Skip to content

Commit

Permalink
feat(google): support stateful MIG operations (#7196)
Browse files Browse the repository at this point in the history
* feat(google): add gce stateful mig feature flag

* feat(google): instance template disk UI adjustments

* feat(google): support stateful disk operations

* fix(google): couple of misc. fixes to stateful MIG UI
  • Loading branch information
maggieneterval committed Jul 9, 2019
1 parent d51b0d4 commit f340b02
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 77 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface IFeatures {
dockerBake?: boolean;
entityTags?: boolean;
fiatEnabled?: boolean;
gceStatefulMigsEnabled?: boolean;
iapRefresherEnabled?: boolean;
// whether stages affecting infrastructure (like "Create Load Balancer") should be enabled or not
infrastructureStages?: boolean;
Expand Down
10 changes: 10 additions & 0 deletions app/scripts/modules/google/src/domain/disk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface IGceDisk {
boot: boolean;
deviceName: string;
index: number;
initializeParams: {
diskSizeGb: number;
diskType: string;
sourceImage: string;
};
}
1 change: 1 addition & 0 deletions app/scripts/modules/google/src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './loadBalancer';
export * from './network';
export * from './serverGroup';
export * from './subnet';
export * from './disk';
2 changes: 2 additions & 0 deletions app/scripts/modules/google/src/gce.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GCE_SSL_LOAD_BALANCER_CTRL } from './loadBalancer/configure/ssl/gceCrea
import { GCE_TCP_LOAD_BALANCER_CTRL } from './loadBalancer/configure/tcp/gceCreateTcpLoadBalancer.controller';
import { IAP_INTERCEPTOR } from 'google/interceptors/iap.interceptor';
import { LOAD_BALANCER_SET_TRANSFORMER } from './loadBalancer/loadBalancer.setTransformer';
import { GCE_SERVER_GROUP_DISK_DESCRIPTIONS } from './serverGroup/details/ServerGroupDiskDescriptions';
import './help/gce.help';

import './logo/gce.logo.less';
Expand All @@ -26,6 +27,7 @@ module(GOOGLE_MODULE, [
GCE_SSL_LOAD_BALANCER_CTRL,
GCE_TCP_LOAD_BALANCER_CTRL,
IAP_INTERCEPTOR,
GCE_SERVER_GROUP_DISK_DESCRIPTIONS,
require('./serverGroup/details/serverGroup.details.gce.module').name,
require('./serverGroup/configure/serverGroupCommandBuilder.service').name,
require('./serverGroup/configure/wizard/cloneServerGroup.gce.controller').name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { module } from 'angular';

import { get, last } from 'lodash';
import * as React from 'react';
import { react2angular } from 'react2angular';

import { Application } from '@spinnaker/core';

import { IGceDisk, IGceServerGroup } from '../../domain';
import { StatefulMIGService } from './stateful/StatefulMIGService';
import { MarkDiskStatefulButton } from './stateful/MarkDiskStatefulButton';
import { UpdateBootImageButton } from './stateful/UpdateBootImageButton';

interface IServerGroupDiskDescriptionProps {
application: Application;
serverGroup: IGceServerGroup;
}

class ServerGroupDiskDescriptions extends React.Component<IServerGroupDiskDescriptionProps> {
public render() {
const { application, serverGroup } = this.props;
const disks: IGceDisk[] = get(serverGroup, 'launchConfig.instanceTemplate.properties.disks', []);
const statefulOperationsEnabled: boolean = StatefulMIGService.statefulMigsEnabled();
const canUpdateBootImage =
statefulOperationsEnabled && disks.some(disk => StatefulMIGService.isDiskStateful(disk.deviceName, serverGroup));

return disks.map(disk => {
if (disk.boot) {
return (
<React.Fragment key={disk.deviceName}>
<dt>
Boot Disk
{canUpdateBootImage && (
<UpdateBootImageButton
application={application}
bootImage={ServerGroupDiskDescriptions.getDiskImageName(disk)}
serverGroup={serverGroup}
/>
)}
</dt>
<dd>{ServerGroupDiskDescriptions.getDiskTypeLabel(disk)}</dd>
<dd>{ServerGroupDiskDescriptions.getDiskImageLabel(disk)}</dd>
</React.Fragment>
);
}
return (
<React.Fragment key={disk.deviceName}>
<dt>
Disk
{statefulOperationsEnabled && (
<MarkDiskStatefulButton
application={application}
deviceName={disk.deviceName}
serverGroup={serverGroup}
/>
)}
</dt>
<dd>{ServerGroupDiskDescriptions.getDiskTypeLabel(disk)}</dd>
<dd>{ServerGroupDiskDescriptions.getDiskImageLabel(disk)}</dd>
</React.Fragment>
);
});
}

private static translateDiskType = (disk: IGceDisk): string => {
const diskType = disk.initializeParams.diskType;
if (diskType === 'pd-ssd') {
return 'Persistent SSD';
} else if (diskType === 'local-ssd') {
return 'Local SSD';
} else {
return 'Persistent Std';
}
};

private static getDiskTypeLabel = (disk: IGceDisk): string => {
return `${ServerGroupDiskDescriptions.translateDiskType(disk)}: ${disk.initializeParams.diskSizeGb}GB`;
};

private static getDiskImageLabel = (disk: IGceDisk): string => {
return `Image: ${ServerGroupDiskDescriptions.getDiskImageName(disk)}`;
};

private static getDiskImageName = (disk: IGceDisk): string => {
return last(get(disk, 'initializeParams.sourceImage', '').split('/'));
};
}

export const GCE_SERVER_GROUP_DISK_DESCRIPTIONS = 'spinnaker.gce.serverGroupDiskDescriptions';
module(GCE_SERVER_GROUP_DISK_DESCRIPTIONS, []).component(
'gceServerGroupDiskDescriptions',
react2angular(ServerGroupDiskDescriptions, ['application', 'serverGroup']),
);
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ module.exports = angular
determineAssociatePublicIPAddress();

findStartupScript();
prepareDiskDescriptions();
prepareAvailabilityPolicies();
prepareShieldedVmConfig();
prepareAutoHealingPolicy();
Expand Down Expand Up @@ -169,66 +168,6 @@ module.exports = angular
}
};

const prepareDiskDescriptions = () => {
if (_.has(this.serverGroup, 'launchConfig.instanceTemplate.properties.disks')) {
const diskDescriptions = [];

this.serverGroup.launchConfig.instanceTemplate.properties.disks.forEach(disk => {
const diskLabel = disk.initializeParams.diskType + ':' + disk.initializeParams.diskSizeGb;
const existingDiskDescription = _.find(diskDescriptions, description => {
return description.bareLabel === diskLabel;
});

if (existingDiskDescription) {
existingDiskDescription.count++;
existingDiskDescription.countSuffix = ' (×' + existingDiskDescription.count + ')';
existingDiskDescription.sourceImages = getSourceImage(disk)
? [getSourceImage(disk)].concat(existingDiskDescription.sourceImages)
: existingDiskDescription.sourceImages;
} else {
diskDescriptions.push({
bareLabel: diskLabel,
count: 1,
countSuffix: '',
finalLabel:
translateDiskType(disk.initializeParams.diskType) + ': ' + disk.initializeParams.diskSizeGb + 'GB',
sourceImages: getSourceImage(disk) ? [getSourceImage(disk)] : [],
});
}
});

diskDescriptions.forEach(description => {
if (!description.sourceImages.length) {
return;
}

description.sourceImages = _.uniq(description.sourceImages);

switch (description.count) {
case 0:
break;
case 1:
if (description.sourceImages[0]) {
description.helpField = `This disk uses the source image <em>${description.sourceImages[0]}</em>.`;
}
break;
default:
description.helpField = `
These disks use the following source images:
<ul>
${description.sourceImages.map(image => `<li><em>${image}</em></li>`).join('')}
</ul>
`;
break;
}
});

this.serverGroup.diskDescriptions = diskDescriptions;
}
};

const getSourceImage = disk => _.last(_.get(disk, 'initializeParams.sourceImage', '').split('/'));

const prepareAvailabilityPolicies = () => {
if (_.has(this.serverGroup, 'launchConfig.instanceTemplate.properties.scheduling')) {
const scheduling = this.serverGroup.launchConfig.instanceTemplate.properties.scheduling;
Expand Down Expand Up @@ -301,16 +240,6 @@ module.exports = angular
}
};

const translateDiskType = diskType => {
if (diskType === 'pd-ssd') {
return 'Persistent SSD';
} else if (diskType === 'local-ssd') {
return 'Local SSD';
} else {
return 'Persistent Std';
}
};

const augmentTagsWithHelp = () => {
if (_.has(this.serverGroup, 'launchConfig.instanceTemplate.properties.tags.items') && this.securityGroups) {
const helpMap = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,7 @@ <h4 class="text-center" ng-if="ctrl.serverGroup.isDisabled">[SERVER GROUP IS DIS
<dd>{{ctrl.serverGroup.launchConfig.instanceType | customInstanceFilter }}</dd>
<dt>Minimum CPU Platform</dt>
<dd>{{ctrl.serverGroup.launchConfig.minCpuPlatform || '(Automatic)'}}</dd>
<dt ng-repeat-start="diskDescription in ctrl.serverGroup.diskDescriptions">
Disk{{diskDescription.countSuffix}}
</dt>
<dd ng-repeat-end>
{{diskDescription.finalLabel}}<help-field content="{{diskDescription.helpField}}"></help-field>
</dd>
<gce-server-group-disk-descriptions application="ctrl.application" server-group="ctrl.serverGroup" />
<dt ng-if="ctrl.serverGroup.serviceAccountEmail">Service Account</dt>
<dd ng-if="ctrl.serverGroup.serviceAccountEmail">{{ctrl.serverGroup.serviceAccountEmail}}</dd>
<dt ng-if="ctrl.serverGroup.authScopes">Auth Scopes</dt>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';

import { Application, ReactInjector } from '@spinnaker/core';

import { StatefulMIGService } from 'google/serverGroup/details/stateful/StatefulMIGService';
import { IGceServerGroup } from 'google/domain';

interface IMarkDiskStatefulButtonProps {
application: Application;
deviceName: string;
serverGroup: IGceServerGroup;
}

export function MarkDiskStatefulButton({ application, deviceName, serverGroup }: IMarkDiskStatefulButtonProps) {
function openConfirmationModal(): void {
ReactInjector.confirmationModalService.confirm({
account: serverGroup.account,
askForReason: true,
buttonText: 'Mark as stateful',
header: `Really mark disk ${deviceName} as stateful?`,
submitMethod: () => {
return StatefulMIGService.markDiskStateful(application.name, deviceName, serverGroup);
},
taskMonitorConfig: {
application,
title: 'Marking disk as stateful',
},
});
}

if (StatefulMIGService.isDiskStateful(deviceName, serverGroup)) {
return <span> (Marked as Stateful)</span>;
}

return (
<button className="btn-link" onClick={() => openConfirmationModal()}>
Mark as Stateful
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { has } from 'lodash';

import { SETTINGS, TaskExecutor } from '@spinnaker/core';

import { IGceServerGroup } from 'google/domain';

export class StatefulMIGService {
public static markDiskStateful(applicationName: string, deviceName: string, serverGroup: IGceServerGroup) {
return TaskExecutor.executeTask({
application: applicationName,
description: 'Mark disk as stateful',
job: [
{
cloudProvider: 'gce',
credentials: serverGroup.account,
deviceName,
region: serverGroup.region,
serverGroupName: serverGroup.name,
type: 'setStatefulDisk',
},
],
});
}

public static statefullyUpdateBootDisk(applicationName: string, bootImage: string, serverGroup: IGceServerGroup) {
return TaskExecutor.executeTask({
application: applicationName,
description: 'Statefully update boot disk image',
job: [
{
bootImage,
cloudProvider: 'gce',
credentials: serverGroup.account,
region: serverGroup.region,
serverGroupName: serverGroup.name,
type: 'statefullyUpdateBootImage',
},
],
});
}

public static isDiskStateful(deviceName: string, serverGroup: IGceServerGroup): boolean {
return has(serverGroup, ['statefulPolicy', 'preservedState', 'disks', deviceName]);
}

public static statefulMigsEnabled(): boolean {
return SETTINGS.feature.gceStatefulMigsEnabled;
}
}
Loading

0 comments on commit f340b02

Please sign in to comment.