Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow creating new AppStream enabled account #566

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
49bb5c0
Working offline sagemaker
Jun 23, 2021
123b2d2
Working linux ec2
Jun 25, 2021
92d1144
Working windows
Jun 25, 2021
02acfa2
Add offline packages
Jun 28, 2021
7957c2b
Cleanup
Jun 28, 2021
bbf8824
Add isAppStreamEnabled param and SolutionNamespace param
Jun 28, 2021
d1ad641
Move packages
Jun 28, 2021
43da77a
Merge branch 'feat-secure-workspace-egress' into workspace-remove-int…
Jun 28, 2021
2d3be6b
Allow unit tests to run
Jun 28, 2021
1615df3
Add IsAppStreamEnabled param for templates
Jun 30, 2021
b742a6c
Add isAppStreamEnabled and solutionNamespace to resolved var params
Jun 30, 2021
d29e1b9
Working connect to ec2 instance from AppStream
Jul 1, 2021
619a724
Update environment-config-vars-service-test
Jul 1, 2021
d12606a
Fixing window stack
Jul 1, 2021
a87ec81
Merge branch 'feat-secure-workspace-egress' into workspace-remove-int…
Jul 1, 2021
3e2481f
WIP, app stream account onboard
Jul 1, 2021
adb9f3a
Update AppStream setup
Jul 2, 2021
3019b0b
Merge branch 'workspace-remove-internet' into appstream-onboard-api
Jul 2, 2021
737a690
Remove code accidentally added to the PR
Jul 2, 2021
1cb5ede
Delete duplicate isAppStreamEnabled value
Jul 2, 2021
c1e75dc
WIP: UI part
Jul 2, 2021
da512e8
WIP
Jul 6, 2021
2b8dae6
Working
Jul 7, 2021
e15e2c9
Working end to end
Jul 7, 2021
900f28e
Some cleanup
Jul 7, 2021
26628b1
Merge branch 'feat-secure-workspace-egress' into appstream-onboard-api
Jul 8, 2021
5824414
Update lock file
Jul 8, 2021
9959846
Update tests
Jul 9, 2021
741b50f
Update CFN permission for images
Jul 9, 2021
8ba2441
Remove duplicate export in onboard-account
Jul 9, 2021
7a71da0
Start fleet
Jul 9, 2021
af43236
Automatically create AppStream roles
Jul 12, 2021
32dc77f
Wait for AppStream fleet to transition to RUNNING state before comple…
Jul 12, 2021
173d0b6
Need to pass in default value for when AppStream not enabled
Jul 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import { createForm } from '../../helpers/form';

const createAwsAccountFormFields = {
const createBaseAwsAccountFormFields = {
accountName: {
label: 'Account Name',
placeholder: 'Type the name of this account',
Expand Down Expand Up @@ -43,12 +43,56 @@ const createAwsAccountFormFields = {
},
};

function getCreateAwsAccountFormFields() {
return createAwsAccountFormFields;
const createAwsAccountAppStreamFormFields = {
appStreamFleetDesiredInstances: {
label: 'AppStream Fleet Desired Instance',
placeholder: 'Number of users that can concurrently access a workspace through AppStream',
rules: 'required|integer',
},
appStreamDisconnectTimeoutSeconds: {
label: 'AppStreamDisconnectTimeoutSeconds',
placeholder: 'The amount of time that a streaming session remains active after users disconnect',
rules: 'required|integer',
},
appStreamIdleDisconnectTimeoutSeconds: {
label: 'AppStreamIdleDisconnectTimeoutSeconds',
placeholder:
'The amount of time that users can be idle (inactive) before they are disconnected from their streaming session',
rules: 'required|integer',
},
appStreamMaxUserDurationSeconds: {
label: 'AppStreamMaxUserDurationSeconds',
placeholder: 'The maximum amount of time that a streaming session can remain active, in seconds',
rules: 'required|integer',
},
appStreamImageName: {
label: 'AppStreamImageName',
placeholder: 'The name of the image used to create the fleet',
rules: 'required|string',
},
appStreamInstanceType: {
label: 'AppStreamInstanceType',
placeholder:
'The instance type to use when launching fleet instances. List of images available at https://aws.amazon.com/appstream2/pricing/',
rules: 'required|string',
},
appStreamFleetType: {
label: 'AppStreamFleetType',
placeholder: 'The fleet type. Should be either ALWAYS_ON or ON_DEMAND',
rules: ['required', 'regex:/^ALWAYS_ON|ON_DEMAND$/'],
},
};

function getCreateBaseAwsAccountFormFields() {
return createBaseAwsAccountFormFields;
}

function getCreateAwsAccountAppStreamFormFields() {
return createAwsAccountAppStreamFormFields;
}

function getCreateAwsAccountForm() {
return createForm(createAwsAccountFormFields);
function getCreateAwsAccountForm(fields) {
return createForm(fields);
}

export { getCreateAwsAccountFormFields, getCreateAwsAccountForm };
export { getCreateBaseAwsAccountFormFields, getCreateAwsAccountForm, getCreateAwsAccountAppStreamFormFields };
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
import validate from '@aws-ee/base-ui/dist/models/forms/Validate';

import { getCreateAwsAccountForm, getCreateAwsAccountFormFields } from '../../models/forms/CreateAwsAccountForm';
import {
getCreateAwsAccountForm,
getCreateBaseAwsAccountFormFields,
getCreateAwsAccountAppStreamFormFields,
} from '../../models/forms/CreateAwsAccountForm';

class CreateAwsAccount extends React.Component {
constructor(props) {
Expand All @@ -34,8 +38,13 @@ class CreateAwsAccount extends React.Component {
this.validationErrors = new Map();
this.awsAccount = {};
});
this.form = getCreateAwsAccountForm();
this.createAwsAccountFormFields = getCreateAwsAccountFormFields();

let fields = getCreateBaseAwsAccountFormFields();
if (process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true') {
fields = { ...fields, ...getCreateAwsAccountAppStreamFormFields() };
}
this.form = getCreateAwsAccountForm(fields);
this.createAwsAccountFormFields = fields;
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,10 +670,10 @@ Resources:
EnableDefaultInternetAccess: False
FleetType: !Ref AppStreamFleetType
IdleDisconnectTimeoutInSeconds: !Ref AppStreamIdleDisconnectTimeoutSeconds
ImageName: !Ref AppStreamImageName
ImageArn: !Sub 'arn:aws:appstream:${AWS::Region}:${CentralAccountId}:image/${AppStreamImageName}'
InstanceType: !Ref AppStreamInstanceType
MaxUserDurationInSeconds: !Ref AppStreamMaxUserDurationSeconds
Name: 'ServiceWorkbenchFleet'
Name: !Sub ${Namespace}-ServiceWorkbenchFleet
StreamView: 'APP'
VpcConfig:
SecurityGroupIds:
Expand All @@ -689,7 +689,7 @@ Resources:
Enabled: False
Description: 'SWB AppStream Stack'
DisplayName: 'SWB Stack'
Name: 'ServiceWorkbenchStack'
Name: !Sub ${Namespace}-ServiceWorkbenchStack
UserSettings:
- Action: 'CLIPBOARD_COPY_FROM_LOCAL_DEVICE'
Permission: 'ENABLED'
Expand Down Expand Up @@ -777,3 +777,8 @@ Outputs:
Value: !GetAtt VPC.DefaultSecurityGroup
Export:
Name: !Join ['', [Ref: Namespace, '-SwbVPCDefaultSG']]

AppStreamStackName:
Description: Name of the stack created by AppStream
Condition: isAppStream
Value: !Sub ${Namespace}-ServiceWorkbenchStack
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const AccountService = require('../account-service');
// Tested Methods: provisionAccount, update, delete
describe('accountService', () => {
let service = null;
let settingsService = null;
let dbService = null;
let wfService = null;
const error = { code: 'ConditionalCheckFailedException' };
Expand All @@ -56,6 +57,7 @@ describe('accountService', () => {
await container.initServices();

service = await container.find('accountService');
settingsService = await container.find('settings');
dbService = await container.find('dbService');
wfService = await container.find('workflowTriggerService');
service.assertAuthorized = jest.fn();
Expand All @@ -71,87 +73,176 @@ describe('accountService', () => {
});

describe('provisionAccount tests', () => {
it('should fail to create account with no credentials', async () => {
// BUILD
// OPERATE
try {
await service.provisionAccount({}, {});
expect.hasAssertions();
} catch (err) {
// CHECK
expect(err.message).toEqual('Input has validation errors');
}
});

it('should fail to create account with partial credentials', async () => {
// BUILD
const dataMissing = {
accountName: 'Winston Bishop',
accountEmail: 'beanbagchair@example.com',
masterRoleArn: '',
externalId: '',
description: '',
};

// OPERATE
try {
await service.provisionAccount({}, dataMissing);
expect.hasAssertions();
} catch (err) {
// CHECK
expect(err.message).toEqual(
'Creating AWS account process has not been correctly configured: missing AWS account ID, VPC ID and VPC Subnet ID.',
);
}
});

it('should successfully try to provision an account with full credentials', async () => {
// BUILD
describe('crendentials', () => {
const iptData = {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
masterRoleArn: 'reagan :/',
externalId: '837 Traction Ave',
description: 'A classic nodejs-lodash mess-around',
};
const outputData = {

it('should fail to create account with no credentials', async () => {
// BUILD
// OPERATE
try {
await service.provisionAccount({}, {});
expect.hasAssertions();
} catch (err) {
// CHECK
expect(err.message).toEqual('Input has validation errors');
}
});

it('should fail to create account with partial credentials', async () => {
// BUILD
const inputDataMissing = {
...iptData,
masterRoleArn: '',
externalId: '',
description: '',
};

// OPERATE
try {
await service.provisionAccount({}, inputDataMissing);
expect.hasAssertions();
} catch (err) {
// CHECK
expect(err.message).toEqual(
'Creating AWS account process has not been correctly configured: missing AWS account ID, VPC ID and VPC Subnet ID.',
);
}
});

it('should successfully try to provision an account with full credentials', async () => {
// BUILD
const fullIptData = { ...iptData };
const outputData = {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
masterRoleArn: 'reagan :/',
externalId: '837 Traction Ave',
description: 'A classic nodejs-lodash mess-around',
callerAccountId: '111111111111',
};
service.audit = jest.fn();
wfService.triggerWorkflow = jest.fn();
AWSMock.mock('STS', 'getCallerIdentity', {
UserId: 'Jeet',
Account: '111111111111',
Arn: 'arn:aws:sts::111111111111:assumed-role/Jeet/Jeet',
});

// OPERATE
await service.provisionAccount({}, fullIptData);

// CHECK
expect(wfService.triggerWorkflow).toHaveBeenCalledWith(
{},
{
workflowId: 'wf-provision-account',
},
expect.objectContaining(outputData),
);
expect(service.audit).toHaveBeenCalledWith(
{},
{
action: 'provision-account',
body: {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
description: 'A classic nodejs-lodash mess-around',
},
},
);
});
});
describe('AppStream', () => {
const iptData = {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
masterRoleArn: 'reagan :/',
externalId: '837 Traction Ave',
description: 'A classic nodejs-lodash mess-around',
callerAccountId: '111111111111',
appStreamFleetDesiredInstances: '2',
appStreamDisconnectTimeoutSeconds: '60',
appStreamIdleDisconnectTimeoutSeconds: '600',
appStreamMaxUserDurationSeconds: '86400',
appStreamImageName: 'SWB_v1',
appStreamInstanceType: 'stream.standard.medium',
appStreamFleetType: 'ALWAYS_ON',
};
service.audit = jest.fn();
wfService.triggerWorkflow = jest.fn();
AWSMock.mock('STS', 'getCallerIdentity', {
UserId: 'Jeet',
Account: '111111111111',
Arn: 'arn:aws:sts::111111111111:assumed-role/Jeet/Jeet',
it('should fail to create account without required AppStream params', async () => {
// BUILD
const inputMissingAppStreamData = {
...iptData,
};
delete inputMissingAppStreamData.appStreamImageName;
delete inputMissingAppStreamData.appStreamInstanceType;

settingsService.get = jest.fn(key => {
if (key === 'isAppStreamEnabled') {
return 'true';
}
return undefined;
});

// OPERATE && CHECK
await expect(service.provisionAccount({}, inputMissingAppStreamData)).rejects.toThrow(
'Not all required App Stream params are defined. These params need to be defined: appStreamImageName,appStreamInstanceType',
);
});

// OPERATE
await service.provisionAccount({}, iptData);
it('should successfully try to provision an account with full all provided AppStream params', async () => {
// BUILD
const inputFullData = { ...iptData };
const outputData = {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
masterRoleArn: 'reagan :/',
externalId: '837 Traction Ave',
description: 'A classic nodejs-lodash mess-around',
callerAccountId: '111111111111',
};
service.audit = jest.fn();
wfService.triggerWorkflow = jest.fn();
AWSMock.mock('STS', 'getCallerIdentity', {
UserId: 'Jeet',
Account: '111111111111',
Arn: 'arn:aws:sts::111111111111:assumed-role/Jeet/Jeet',
});

// CHECK
expect(wfService.triggerWorkflow).toHaveBeenCalledWith(
{},
{
workflowId: 'wf-provision-account',
},
expect.objectContaining(outputData),
);
expect(service.audit).toHaveBeenCalledWith(
{},
{
action: 'provision-account',
body: {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
description: 'A classic nodejs-lodash mess-around',
settingsService.get = jest.fn(key => {
if (key === 'isAppStreamEnabled') {
return 'true';
}
return undefined;
});

// OPERATE
await service.provisionAccount({}, inputFullData);

// CHECK
expect(wfService.triggerWorkflow).toHaveBeenCalledWith(
{},
{
workflowId: 'wf-provision-account',
},
},
);
expect.objectContaining(outputData),
);
expect(service.audit).toHaveBeenCalledWith(
{},
{
action: 'provision-account',
body: {
accountName: "Who's that girl?",
accountEmail: 'itsjest@example.com',
description: 'A classic nodejs-lodash mess-around',
},
},
);
});
});
});

Expand Down
Loading