Skip to content

Commit

Permalink
feat(google): Add support for accelerators when deploying VMs (#6467)
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott authored Feb 1, 2019
1 parent 1f97215 commit d09a910
Show file tree
Hide file tree
Showing 9 changed files with 3,050 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GCE_AUTOHEALING_POLICY_SELECTOR } from './wizard/autoHealingPolicy/auto
import { GCE_CACHE_REFRESH } from 'google/cache/cacheRefresh.component';
import { GCE_CUSTOM_INSTANCE_CONFIGURER } from './wizard/customInstance/customInstanceConfigurer.component';
import { GCE_DISK_CONFIGURER } from './wizard/advancedSettings/diskConfigurer.component';
import { GCE_ACCELERATOR_CONFIGURER } from './wizard/advancedSettings/acceleratorConfigurer.component';

module.exports = angular.module('spinnaker.serverGroup.configure.gce', [
require('../../autoscalingPolicy/components/basicSettings/basicSettings.component').name,
Expand All @@ -17,6 +18,7 @@ module.exports = angular.module('spinnaker.serverGroup.configure.gce', [
GCE_CACHE_REFRESH,
GCE_CUSTOM_INSTANCE_CONFIGURER,
GCE_DISK_CONFIGURER,
GCE_ACCELERATOR_CONFIGURER,
require('../serverGroup.transformer').name,
require('./serverGroupConfiguration.service').name,
require('./wizard/advancedSettings/advancedSettingsSelector.directive').name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,33 @@ module.exports = angular
return result;
}

function configureAccelerators(command) {
const result = { dirty: {} };
const { credentials, zone, backingData } = command;
const accountBackingData = _.get(backingData, ['credentialsKeyedByAccount', credentials], {});
const acceleratorMap = accountBackingData.zoneToAcceleratorTypesMap || {};
const acceleratorTypes = _.get(acceleratorMap, [zone, 'acceleratorTypes', 'acceleratorTypes'], []).map(a => {
const maxCards = _.isFinite(a.maximumCardsPerInstance) ? a.maximumCardsPerInstance : 4;
const availableCardCounts = [];
for (let i = 1; i <= maxCards; i *= 2) {
availableCardCounts.push(i);
}
return _.assign({}, a, { availableCardCounts });
});
_.set(command, ['viewState', 'acceleratorTypes'], acceleratorTypes);
result.dirty.acceleratorTypes = true;
const chosenAccelerators = _.get(command, 'acceleratorConfigs', []);
if (chosenAccelerators.length > 0) {
command.acceleratorConfigs = chosenAccelerators.filter(
chosen => !!acceleratorTypes.find(a => a.name === chosen.acceleratorType),
);
if (command.acceleratorConfigs.length === 0) {
delete command.acceleratorConfigs;
}
}
return result;
}

// n1-standard-8 should come before n1-standard-16, so we must sort by the individual segments of the names.
function sortInstanceTypes(instanceTypes) {
const tokenizedInstanceTypes = _.map(instanceTypes, instanceType => {
Expand Down Expand Up @@ -726,6 +753,7 @@ module.exports = angular
angular.extend(command.viewState.dirty, result.dirty);
angular.extend(command.viewState.dirty, configureInstanceTypes(command).dirty);
angular.extend(command.viewState.dirty, configureCpuPlatforms(command).dirty);
angular.extend(command.viewState.dirty, configureAccelerators(command).dirty);
return result;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { module, IComponentOptions, IComponentController } from 'angular';
import { get } from 'lodash';

interface IGceAcceleratorType {
name: string;
description: string;
availableCardCounts: number[];
}

interface IGceAcceleratorConfig {
acceleratorType: string;
acceleratorCount: number;
}

class GceAcceleratorConfigurerController implements IComponentController {
public command: any;

private getAvailableAcceleratorTypes(): IGceAcceleratorType[] {
return get(this.command, ['viewState', 'acceleratorTypes'], []);
}

public addAccelerator(): void {
this.command.acceleratorConfigs = this.command.acceleratorConfigs || [];
const types = this.getAvailableAcceleratorTypes();
if (types.length === 0) {
return;
}
this.command.acceleratorConfigs.push({
acceleratorType: types[0].name,
acceleratorCount: 1,
});
}

public removeAccelerator(acceleratorConfig: IGceAcceleratorConfig): void {
const index = (this.command.acceleratorConfigs || []).indexOf(acceleratorConfig);
if (index > -1) {
this.command.acceleratorConfigs.splice(index, 1);
}
}

public getAvailableCardCounts(acceleratorName: string): number[] {
if (!acceleratorName) {
return null;
}
const config = this.getAvailableAcceleratorTypes().find(a => a.name === acceleratorName);
if (!config) {
return null;
}
return config.availableCardCounts;
}

public onAcceleratorTypeChanged(acceleratorConfig: IGceAcceleratorConfig): void {
if (!acceleratorConfig) {
return;
}
const counts = this.getAvailableCardCounts(acceleratorConfig.acceleratorType);
if (!counts) {
return;
}
if (!counts.includes(acceleratorConfig.acceleratorCount)) {
let nearestCount = 0;
for (const count of counts) {
nearestCount = count;
if (count > acceleratorConfig.acceleratorCount) {
break;
}
}
acceleratorConfig.acceleratorCount = nearestCount;
}
}
}

class GceAcceleratorConfigurer implements IComponentOptions {
public controller = GceAcceleratorConfigurerController;
public bindings = { command: '<' };
public template = `
<div class="form-group">
<div class="sm-label-left" style="margin-bottom: 5px;">
Accelerators
</div>
<table class="table table-condensed packed tags">
<thead>
<tr>
<th style="width: 75%;">Type</th>
<th style="width: 15%;">Count</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="acceleratorConfig in $ctrl.command.acceleratorConfigs">
<td>
<ui-select ng-model="acceleratorConfig.acceleratorType" on-select="$ctrl.onAcceleratorTypeChanged(acceleratorConfig)" class="form-control input-sm">
<ui-select-match placeholder="Accelerator Type">{{ $select.selected.description }}</ui-select-match>
<ui-select-choices repeat="accelerator.name as accelerator in $ctrl.command.viewState.acceleratorTypes | filter: $select.search">
<span ng-bind-html="accelerator.description | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</td>
<td>
<ui-select ng-model="acceleratorConfig.acceleratorCount" class="form-control input-sm">
<ui-select-match placeholder="Accelerator Count">{{ $select.selected }}</ui-select-match>
<ui-select-choices repeat="count in $ctrl.getAvailableCardCounts(acceleratorConfig.acceleratorType)">
{{ count }}
</ui-select-choices>
</td>
<td>
<a class="btn btn-link sm-label" style="margin-top: 0;" ng-click="$ctrl.removeAccelerator(acceleratorConfig)">
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
</tr>
</tbody>
<tfoot>
<tr ng-if="$ctrl.command.acceleratorConfigs && $ctrl.command.acceleratorConfigs.length > 0">
<td colspan="3">
Adding Accelerators places constraints on the instances that you can deploy. For a complete list of
these restrictions see <a href="https://cloud.google.com/compute/docs/gpus/#restrictions">the docs on GPUs</a>.
</td>
</tr>
<tr>
<td colspan="3">
<button class="btn btn-block btn-sm add-new" ng-click="$ctrl.addAccelerator()">
<span class="glyphicon glyphicon-plus-sign"></span> Add Accelerator
</button>
</td>
</tr>
</div>
</tfoot>
</table>
</div>
`;
}

export const GCE_ACCELERATOR_CONFIGURER = 'spinnaker.gce.accelerator.component';
module(GCE_ACCELERATOR_CONFIGURER, []).component('gceAcceleratorConfigurer', new GceAcceleratorConfigurer());
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
disks="vm.command.disks"
update-disks="vm.setDisks(disks)"></gce-disk-configurer>

<gce-accelerator-configurer command="vm.command"></gce-accelerator-configurer>

<div class="form-group">
<div class="sm-label-left" style="margin-bottom: 5px;">User Data <help-field key="gce.serverGroup.userData"></help-field></div>
<div class="col-md-12">
Expand Down
1 change: 1 addition & 0 deletions test/functional/tests/core/pages/ApplicationConfigPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export class ApplicationConfigPage extends Page {
confirmDeleteButton: (appLabel: string) => `//button[contains(., 'Delete ${appLabel}')]`,
},
};

public openForApp(appName: string, section = '') {
let url = `/#/applications/${appName}/config`;
if (section) {
Expand Down
1 change: 1 addition & 0 deletions test/functional/tests/core/pages/InfrastructurePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class InfrastructurePage extends Page {
clickableServerGroup: '.server-group.clickable',
actionsButton: '.details-panel .actions button',
cloneMenuItem: `//*[contains(@class, 'dropdown-menu')]//a[contains(text(), 'Clone')]`,
createServerGroupButton: `//*[contains(@class, 'application-actions')]//button[contains(., 'Create Server Group')]`,
};

public openClustersForApplication(application: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { InfrastructurePage } from '../core/pages/InfrastructurePage';
import { CreateServerGroupModalPage } from './pages/CreateServerGroupModalPage';

describe('Create Server Group Modal', () => {
describe('GPU Accelerators', () => {
it(`provides different accelerators according to the server group's chosen zone`, () => {
const infra = new InfrastructurePage();
const createModal = new CreateServerGroupModalPage();
infra.openClustersForApplication('compute');
infra.click(InfrastructurePage.locators.createServerGroupButton);
createModal.addAccelerator();
const usCentralAcceleratorNames = createModal.getAcceleratorList();
createModal.selectRegion('us-east1');
createModal.selectZone('us-east1-c');
createModal.addAccelerator();
const usEastAcceleratorNames = createModal.getAcceleratorList();
expect(usCentralAcceleratorNames).not.toContain('NVIDIA Tesla K80');
expect(usEastAcceleratorNames).toContain('NVIDIA Tesla K80');
});
});
});
Loading

0 comments on commit d09a910

Please sign in to comment.