Skip to content

Commit

Permalink
fix(core): Add Groups and Pools for SpotEventPluginFleet
Browse files Browse the repository at this point in the history
  • Loading branch information
kozlove-aws committed Jun 24, 2021
1 parent 11e30f6 commit e83b1ca
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ export class ConfigureSpotEventPlugin extends Construct {
};
const spotFleetRequestConfigs = this.mergeSpotFleetRequestConfigs(props.spotFleets);

const deadlineGroups = Array.from(new Set(props.spotFleets?.map(fleet => fleet.deadlineGroups).reduce((p, c) => p.concat(c), [])));
const deadlinePools = Array.from(new Set(props.spotFleets?.map(fleet => fleet.deadlinePools).reduce((p, c) => p?.concat(c ?? []), [])));
const properties: SEPConfiguratorResourceProps = {
connection: {
hostname: props.renderQueue.endpoint.hostname,
Expand All @@ -468,6 +470,8 @@ export class ConfigureSpotEventPlugin extends Construct {
},
spotFleetRequestConfigurations: spotFleetRequestConfigs,
spotPluginConfigurations: pluginConfig,
deadlineGroups,
deadlinePools,
};

const resource = new CustomResource(this, 'Default', {
Expand Down
10 changes: 9 additions & 1 deletion packages/aws-rfdk/lib/deadline/lib/spot-event-plugin-fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@ export class SpotEventPluginFleet extends Construct implements ISpotEventPluginF
*/
public readonly deadlineGroups: string[];

/**
* Deadline pools the workers need to be assigned to.
*
* @default - Workers are not assigned to any pool
*/
public readonly deadlinePools?: string[];

/**
* Name of SSH keypair to grant access to instances.
*
Expand All @@ -413,6 +420,7 @@ export class SpotEventPluginFleet extends Construct implements ISpotEventPluginF
super(scope, id);

this.deadlineGroups = props.deadlineGroups.map(group => group.toLocaleLowerCase());
this.deadlinePools = props.deadlinePools?.map(pool => pool.toLocaleLowerCase());
this.validateProps(props);

this.securityGroups = props.securityGroups ?? [ new SecurityGroup(this, 'SpotFleetSecurityGroup', { vpc: props.vpc }) ];
Expand Down Expand Up @@ -463,7 +471,7 @@ export class SpotEventPluginFleet extends Construct implements ISpotEventPluginF
renderQueue: props.renderQueue,
workerSettings: {
groups: this.deadlineGroups,
pools: props.deadlinePools?.map(pool => pool.toLocaleLowerCase()),
pools: this.deadlinePools,
region: props.deadlineRegion,
},
userDataProvider: props.userDataProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { SecretsManager } from 'aws-sdk';
import { LambdaContext } from '../lib/aws-lambda';
import { SpotEventPluginClient } from '../lib/configure-spot-event-plugin';
import {
CollectionType,
PoolCollections,
SpotEventPluginClient,
} from '../lib/configure-spot-event-plugin';
import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource';
import { DeadlineClient } from '../lib/deadline-client';
import {
Expand Down Expand Up @@ -47,6 +51,37 @@ export class SEPConfiguratorResource extends SimpleCustomResource {
public async doCreate(_physicalId: string, resourceProperties: SEPConfiguratorResourceProps): Promise<object|undefined> {
const spotEventPluginClient = await this.spotEventPluginClient(resourceProperties.connection);

if (resourceProperties.deadlineGroups && resourceProperties.deadlineGroups.length) {
const deadlineGroups = await spotEventPluginClient.getCollection(CollectionType.Group);
const newDeadlineGroups = deadlineGroups.Pools
.concat(resourceProperties.deadlineGroups
.filter(group => !deadlineGroups.Pools.includes(group)));
const response = await spotEventPluginClient
.saveCollection({
Pools: newDeadlineGroups,
ObsoletePools: deadlineGroups.ObsoletePools,
} as PoolCollections,
CollectionType.Group);
if (!response) {
throw new Error('Failed to save group collection');
}
}
if (resourceProperties.deadlinePools && resourceProperties.deadlinePools.length) {
const deadlinePools = await spotEventPluginClient.getCollection(CollectionType.Pool);
const newDeadlinePools = deadlinePools.Pools
.concat(resourceProperties.deadlinePools
.filter(pool => !deadlinePools.Pools.includes(pool)));
const response = await spotEventPluginClient
.saveCollection({
Pools: newDeadlinePools,
ObsoletePools: deadlinePools.ObsoletePools,
} as PoolCollections,
CollectionType.Pool);
if (!response) {
throw new Error('Failed to save pool collection');
}
}

if (resourceProperties.spotFleetRequestConfigurations) {
const convertedSpotFleetRequestConfigs = convertSpotFleetRequestConfiguration(resourceProperties.spotFleetRequestConfigurations);
const stringConfigs = JSON.stringify(convertedSpotFleetRequestConfigs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SpotFleetRequestType,
SpotFleetResourceType,
} from '../../../../deadline';
import { CollectionType } from '../../lib/configure-spot-event-plugin';
import { SEPConfiguratorResource } from '../handler';
import {
ConnectionOptions,
Expand Down Expand Up @@ -205,24 +206,48 @@ describe('SEPConfiguratorResource', () => {
connection: validConnection,
};

const deadlineGroups = ['group_name'];
const deadlinePools = ['pool_name'];

const allConfigs: SEPConfiguratorResourceProps = {
spotPluginConfigurations: validSpotEventPluginConfig,
connection: validConnection,
spotFleetRequestConfigurations: validSpotFleetRequestConfig,
deadlineGroups,
deadlinePools,
};

const noGroupsButPools: SEPConfiguratorResourceProps = {
... allConfigs,
deadlineGroups: [],
};

const noConfigs: SEPConfiguratorResourceProps = {
connection: validConnection,
};

async function returnEmptyCollection(_v1: any): Promise<any> {
return {
Pools: [],
ObsoletePools: [],
};
}

describe('doCreate', () => {
let handler: SEPConfiguratorResource;
let mockSpotEventPluginClient: { saveServerData: jest.Mock<any, any>; configureSpotEventPlugin: jest.Mock<any, any>; };
let mockSpotEventPluginClient: {
saveServerData: jest.Mock<any, any>;
configureSpotEventPlugin: jest.Mock<any, any>;
getCollection: jest.Mock<any, any>;
saveCollection: jest.Mock<any, any>;
};

beforeEach(() => {
mockSpotEventPluginClient = {
saveServerData: jest.fn(),
configureSpotEventPlugin: jest.fn(),
getCollection: jest.fn(),
saveCollection: jest.fn(),
};

handler = new SEPConfiguratorResource(new AWS.SecretsManager());
Expand Down Expand Up @@ -250,6 +275,7 @@ describe('SEPConfiguratorResource', () => {
const mockConfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) );
mockSpotEventPluginClient.configureSpotEventPlugin = mockConfigureSpotEventPlugin;


// WHEN
const result = await handler.doCreate('physicalId', noConfigs);

Expand Down Expand Up @@ -412,12 +438,19 @@ describe('SEPConfiguratorResource', () => {
async function returnTrue(_v1: any): Promise<boolean> {
return true;
}

const mockSaveServerData = jest.fn( (a) => returnTrue(a) );
mockSpotEventPluginClient.saveServerData = mockSaveServerData;

const mockConfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) );
mockSpotEventPluginClient.configureSpotEventPlugin = mockConfigureSpotEventPlugin;

const mockGetCollection = jest.fn(async (a) => returnEmptyCollection(a));
mockSpotEventPluginClient.getCollection = mockGetCollection;

const mockSaveCollection = jest.fn( (a) => returnTrue(a) );
mockSpotEventPluginClient.saveCollection = mockSaveCollection;

const configs: { Key: string, Value: any }[] = [];
for (const [key, value] of Object.entries(validConvertedPluginConfig)) {
configs.push({
Expand Down Expand Up @@ -445,6 +478,14 @@ describe('SEPConfiguratorResource', () => {

expect(mockConfigureSpotEventPlugin.mock.calls.length).toBe(1);
expect(mockConfigureSpotEventPlugin.mock.calls[0][0]).toEqual([...configs, ...securitySettings]);

expect(mockGetCollection.mock.calls.length).toBe(2);
expect(mockGetCollection.mock.calls[0][0]).toBe(CollectionType.Group);
expect(mockGetCollection.mock.calls[1][0]).toBe(CollectionType.Pool);

expect(mockSaveCollection.mock.calls.length).toBe(2);
expect(mockSaveCollection.mock.calls[0][0]).toEqual({ Pools: deadlineGroups, ObsoletePools: []});
expect(mockSaveCollection.mock.calls[1][0]).toEqual({ Pools: deadlinePools, ObsoletePools: []});
});

test('throw when cannot save spot fleet request configs', async () => {
Expand Down Expand Up @@ -480,6 +521,31 @@ describe('SEPConfiguratorResource', () => {
.rejects
.toThrowError(/Failed to save Spot Event Plugin Configurations/);
});

test.each([
['group', allConfigs],
['pool', noGroupsButPools],
])('throws when collection fails to save %s', async (collectionType: string, config: any) => {

// GIVEN
async function returnFalse(_v1: any): Promise<boolean> {
return false;
}

const mockGetCollection = jest.fn( async (a) => returnEmptyCollection(a) );
mockSpotEventPluginClient.getCollection = mockGetCollection;

const mockSaveCollection = jest.fn( (a) => returnFalse(a) );
mockSpotEventPluginClient.saveCollection = mockSaveCollection;

// WHEN
const promise = handler.doCreate('physicalId', config);

// THEN
await expect(promise)
.rejects
.toThrowError(`Failed to save ${collectionType} collection`);
});
});

test('doDelete does not do anything', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export interface SEPConfiguratorResourceProps {
* See https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html#event-plugin-configuration-options
*/
readonly spotPluginConfigurations?: PluginSettings;

/**
* Deadline groups are used by these fleets.
*/
readonly deadlineGroups?: string[];

/**
* Deadline pools are used by these fleets.
*/
readonly deadlinePools?: string[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ interface DescribeServerDataResponse {
readonly ServerData: DescribedServerData[];
}

/**
* A response from get pool/group request
*/
export interface PoolCollections {
/**
* The collection of user-created Pools that are currently active
*/
readonly Pools: string [];

/**
* The collection of Pools that are currently obsolete
*/
readonly ObsoletePools: string [];
}

/**
* A type of collection to get/recive from Deadline.
*/
export enum CollectionType {
Pool = 'pool',

Group = 'group',
}

/**
* Provides a simple interface to send requests to the Render Queue API related to the Deadline Spot Event Plugin.
*/
Expand Down Expand Up @@ -94,6 +118,45 @@ export class SpotEventPluginClient {
}
}

public async getCollection(type: CollectionType): Promise<PoolCollections> {
console.log(`Getting ${type} collection:`);
try {
const response = await this.deadlineClient.GetRequest(`/db/settings/collections/${type}s?invalidateCache=true`, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
const deadlinePools: PoolCollections = response.data;
if (!deadlinePools.Pools || !Array.isArray(deadlinePools.Pools)) {
throw new Error(`Failed to receive a ${type} collection. Invalid response: ${JSON.stringify(response.data)}.`);
}
return deadlinePools;
} catch(e) {
throw new Error(`Failed to get ${type} collection. Reason: ${(<Error>e).message}`);
}
}

public async saveCollection(pools: PoolCollections, type: CollectionType): Promise<boolean> {
console.log(`Saving ${type} collection:`);
console.log(pools);

try {
await this.deadlineClient.PostRequest(`/db/settings/collections/${type}s/save`, {
Pools: pools.Pools,
ObsoletePools: pools.ObsoletePools,
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
return true;
} catch(e) {
console.error(`Failed to save ${type} collection. Reason: ${(<Error>e).message}`);
return false;
}
}

private async describeServerData(): Promise<Response> {
return await this.deadlineClient.PostRequest('/rcs/v1/describeServerData', {
ServerDataIds: [
Expand Down
Loading

0 comments on commit e83b1ca

Please sign in to comment.