diff --git a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/aws-account-mgmt-plugin.test.js b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/aws-account-mgmt-plugin.test.js new file mode 100644 index 0000000000..aef8065241 --- /dev/null +++ b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/aws-account-mgmt-plugin.test.js @@ -0,0 +1,165 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container'); + +// Mocked dependencies +jest.mock('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service'); +const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service'); + +jest.mock('@aws-ee/base-services/lib/settings/env-settings-service'); +const SettingsServiceMock = require('@aws-ee/base-services/lib/settings/env-settings-service'); + +jest.mock('@aws-ee/base-raas-services/lib/environment/service-catalog/environment-sc-service'); +const EnvironmentScServiceMock = require('@aws-ee/base-raas-services/lib/environment/service-catalog/environment-sc-service'); + +jest.mock('@aws-ee/base-raas-services/lib/indexes/indexes-service'); +const IndexesServiceMock = require('@aws-ee/base-raas-services/lib/indexes/indexes-service'); + +const plugin = require('../aws-account-mgmt-plugin'); + +// CHECKed Functions: getActiveNonAppStreamEnvs +describe('awsAccountMgmtPlugin', () => { + let container; + let settings; + let environmentScService; + let indexesService; + beforeEach(async () => { + // Initialize services container and register dependencies + container = new ServicesContainer(); + container.register('pluginRegistryService', new PluginRegistryService()); + container.register('settings', new SettingsServiceMock()); + container.register('environmentScService', new EnvironmentScServiceMock()); + container.register('indexesService', new IndexesServiceMock()); + + await container.initServices(); + settings = await container.find('settings'); + environmentScService = await container.find('environmentScService'); + indexesService = await container.find('indexesService'); + }); + + describe('getActiveNonAppStreamEnvs', () => { + const requestContext = { principalIdentifier: { uid: 'u-testuser' } }; + it('should return empty list if AppStream is disabled', async () => { + // BUILD + const awsAccountId = 'sampleAwsAccountId'; + settings.getBoolean = jest.fn(() => { + return false; + }); + const expected = []; + + // OPERATE + const retVal = await plugin.getActiveNonAppStreamEnvs({ awsAccountId }, { requestContext, container }); + + // CHECK + expect(retVal).toEqual(expected); + }); + + it('should return a list of active non-AppStream environments for an account if AppStream is enabled', async () => { + // BUILD + const awsAccountId = 'sampleAwsAccountId'; + settings.getBoolean = jest.fn(() => { + return true; + }); + const scEnvs = [ + { id: 'env1', indexId: 'index1', isAppStreamConfigured: true, status: 'COMPLETED' }, + { id: 'env2', indexId: 'index1', isAppStreamConfigured: false, status: 'COMPLETED' }, // This will be returned + { id: 'env3', indexId: 'index1', isAppStreamConfigured: false, status: 'FAILED' }, + { id: 'env4', indexId: 'index1', isAppStreamConfigured: false, status: 'TERMINATED' }, + { id: 'env5', indexId: 'index1', isAppStreamConfigured: false, status: 'UNKNOWN' }, + ]; + const indexes = [ + { id: 'index1', awsAccountId }, + { id: 'index2', awsAccountId: 'someOtherAccount' }, + ]; + environmentScService.list = jest.fn(() => { + return scEnvs; + }); + indexesService.list = jest.fn(() => { + return indexes; + }); + + const expected = [{ id: 'env2', indexId: 'index1', isAppStreamConfigured: false, status: 'COMPLETED' }]; + + // OPERATE + const retVal = await plugin.getActiveNonAppStreamEnvs({ awsAccountId }, { requestContext, container }); + + // CHECK + expect(retVal).toEqual(expected); + }); + + it('should return an empty list if no active non-AppStream environments for an account are found', async () => { + // BUILD + const awsAccountId = 'sampleAwsAccountId'; + settings.getBoolean = jest.fn(() => { + return true; + }); + const scEnvs = [ + { id: 'env1', indexId: 'index1', isAppStreamConfigured: true, status: 'COMPLETED' }, + { id: 'env2', indexId: 'index1', isAppStreamConfigured: false, status: 'TERMINATED' }, + { id: 'env3', indexId: 'index1', isAppStreamConfigured: false, status: 'FAILED' }, + { id: 'env4', indexId: 'index1', isAppStreamConfigured: false, status: 'UNKNOWN' }, + ]; + const indexes = [ + { id: 'index1', awsAccountId }, + { id: 'index2', awsAccountId: 'someOtherAccount' }, + ]; + environmentScService.list = jest.fn(() => { + return scEnvs; + }); + indexesService.list = jest.fn(() => { + return indexes; + }); + + const expected = []; + + // OPERATE + const retVal = await plugin.getActiveNonAppStreamEnvs({ awsAccountId }, { requestContext, container }); + + // CHECK + expect(retVal).toEqual(expected); + }); + + it('should return an empty list if active non-AppStream environments exist but for a different account', async () => { + // BUILD + const awsAccountId = 'sampleAwsAccountId'; + settings.getBoolean = jest.fn(() => { + return true; + }); + const scEnvs = [ + { id: 'env1', indexId: 'index1', isAppStreamConfigured: true, status: 'COMPLETED' }, + { id: 'env2', indexId: 'index1', isAppStreamConfigured: false, status: 'STOPPED' }, + ]; + const indexes = [ + { id: 'index1', awsAccountId: 'someOtherAccount' }, + { id: 'index2', awsAccountId }, + ]; + environmentScService.list = jest.fn(() => { + return scEnvs; + }); + indexesService.list = jest.fn(() => { + return indexes; + }); + + const expected = []; + + // OPERATE + const retVal = await plugin.getActiveNonAppStreamEnvs({ awsAccountId }, { requestContext, container }); + + // CHECK + expect(retVal).toEqual(expected); + }); + }); +}); diff --git a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/env-sc-connection-url-plugin.test.js b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/env-sc-connection-url-plugin.test.js index 592aaa0009..64dec8baf3 100644 --- a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/env-sc-connection-url-plugin.test.js +++ b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/__tests__/env-sc-connection-url-plugin.test.js @@ -25,15 +25,15 @@ const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry jest.mock('@aws-ee/base-services/lib/settings/env-settings-service'); const SettingsServiceMock = require('@aws-ee/base-services/lib/settings/env-settings-service'); -jest.mock('../../appstream/appstream-sc-service'); -const AppStreamScService = require('../../appstream/appstream-sc-service'); - jest.mock('@aws-ee/base-raas-services/lib/environment/service-catalog/environment-sc-connection-service'); const EnvironmentScConnectionServiceMock = require('@aws-ee/base-raas-services/lib/environment/service-catalog/environment-sc-connection-service'); +jest.mock('../../appstream/appstream-sc-service'); +const AppStreamScService = require('../../appstream/appstream-sc-service'); + const plugin = require('../env-sc-connection-url-plugin'); -// Tested Functions: create, update, delete +// Tested Functions: createConnectionUrl describe('envScConnectionUrlPlugin', () => { let container; let appStreamScService; @@ -60,7 +60,7 @@ describe('envScConnectionUrlPlugin', () => { // BUILD const connection = { scheme: 'http', operation: 'create' }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return false; }); @@ -75,7 +75,7 @@ describe('envScConnectionUrlPlugin', () => { // BUILD const connection = { scheme: 'http', operation: 'list' }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); @@ -90,7 +90,7 @@ describe('envScConnectionUrlPlugin', () => { // BUILD const connection = { scheme: 'random', operation: 'create' }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); appStreamScService.getStreamingUrl = jest.fn(); @@ -110,7 +110,7 @@ describe('envScConnectionUrlPlugin', () => { const destinationUrl = 'originalPublicDestinationUrl'; let connection = { scheme: 'http', operation: 'create', url: destinationUrl, type: 'SageMaker' }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); environmentScConnectionService.createPrivateSageMakerUrl = jest.fn(() => { @@ -152,7 +152,7 @@ describe('envScConnectionUrlPlugin', () => { const destinationUrl = 'destinationUrl'; let connection = { scheme: 'http', operation: 'create', url: destinationUrl }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); const streamingUrl = 'sampleAppStreamUrl'; @@ -185,7 +185,7 @@ describe('envScConnectionUrlPlugin', () => { const destinationUrl = 'destinationUrl'; let connection = { scheme: 'ssh', operation: 'create', url: destinationUrl }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); const streamingUrl = 'sampleAppStreamUrl'; @@ -216,7 +216,7 @@ describe('envScConnectionUrlPlugin', () => { // BUILD let connection = { scheme: 'rdp', operation: 'create', instanceId: 'sampleInstanceId' }; const envId = 'sampleEnvId'; - settings.optionalBoolean = jest.fn(() => { + settings.getBoolean = jest.fn(() => { return true; }); const streamingUrl = 'sampleAppStreamUrl'; diff --git a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/aws-account-mgmt-plugin.js b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/aws-account-mgmt-plugin.js new file mode 100644 index 0000000000..5033ead9f8 --- /dev/null +++ b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/aws-account-mgmt-plugin.js @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const _ = require('lodash'); + +const settingKeys = { + isAppStreamEnabled: 'isAppStreamEnabled', +}; + +/** + * Returns a list of active non-AppStream environments linked to a given AWS Account ID + * This check is only performed when the deployment has AppStream enabled, + * and is triggered if the user attempts to update the AWS account using SWB APIs. + * A similar check is performed on the UI components (AccountUtils) as well. + */ +async function getActiveNonAppStreamEnvs({ awsAccountId }, { requestContext, container }) { + const settings = await container.find('settings'); + const isAppStreamEnabled = settings.getBoolean(settingKeys.isAppStreamEnabled); + if (!isAppStreamEnabled) return []; + + const nonActiveStates = ['FAILED', 'TERMINATED', 'UNKNOWN']; + const environmentScService = await container.find('environmentScService'); + const indexesService = await container.find('indexesService'); + + const indexes = await indexesService.list(requestContext); + const indexesIdsOfInterest = _.map( + _.filter(indexes, index => index.awsAccountId === awsAccountId), + 'id', + ); + + const scEnvs = await environmentScService.list(requestContext); + const retVal = _.filter( + scEnvs, + scEnv => + _.includes(indexesIdsOfInterest, scEnv.indexId) && + !scEnv.isAppStreamConfigured && + !_.includes(nonActiveStates, scEnv.status), + ); + + return retVal; +} + +const plugin = { getActiveNonAppStreamEnvs }; + +module.exports = plugin; diff --git a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/env-sc-connection-url-plugin.js b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/env-sc-connection-url-plugin.js index a83e035928..398d453288 100644 --- a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/env-sc-connection-url-plugin.js +++ b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/plugins/env-sc-connection-url-plugin.js @@ -28,7 +28,7 @@ async function createConnectionUrl({ envId, connection }, { requestContext, cont const appStreamScService = await container.find('appStreamScService'); const environmentScConnectionService = await container.find('environmentScConnectionService'); const settings = await container.find('settings'); - const isAppStreamEnabled = settings.optionalBoolean(settingKeys.isAppStreamEnabled, false); + const isAppStreamEnabled = settings.getBoolean(settingKeys.isAppStreamEnabled); // This plugin will only contribute to URL creation when AppStream is enabled // Since this plugin is also called upon during listConnections cycle diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironment.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironment.js index 71b174111a..4c0130dde8 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironment.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironment.js @@ -167,6 +167,7 @@ const ScEnvironment = types studyIds: types.frozen([]), cidr: types.frozen([]), outputs: types.frozen([]), + isAppStreamConfigured: types.optional(types.boolean, false), }) .actions(self => ({ setScEnvironment(rawEnvironment) { diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountCard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountCard.js index 0ec912bb94..42a08cf23d 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountCard.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountCard.js @@ -19,10 +19,9 @@ import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; import { Header, Segment, Accordion, Icon, Label, Table, Button } from 'semantic-ui-react'; import c from 'classnames'; - import { createLink } from '@aws-ee/base-ui/dist/helpers/routing'; - import { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification'; +import { isAppStreamEnabled } from '../../helpers/settings'; const { getAccountIdsOfActiveEnvironments } = require('./AccountUtils'); @@ -54,12 +53,8 @@ class AccountCard extends React.Component { return this.props.account; } - get isAppStreamEnabled() { - return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true'; - } - get appStreamStatusMismatch() { - return this.isAppStreamEnabled && !this.account.isAppStreamConfigured; + return isAppStreamEnabled && !this.account.isAppStreamConfigured; } get awsAccountsStore() { @@ -181,7 +176,7 @@ class AccountCard extends React.Component { : `Service Workbench is waiting for the CFN stack to complete. Please wait a few minutes for provisioning to complete. If you did not create a CFN stack for this account, click - "Re-Onboard Account" to return to the account onboarding page.`} + "Re-Onboard Account" to return to the account onboarding page.`} )} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountUtils.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountUtils.js index 7e3c892962..da1d0ccbcf 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountUtils.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AccountUtils.js @@ -1,9 +1,9 @@ import _ from 'lodash'; function getAccountIdsOfActiveEnvironments(scEnvs, projects, indexes) { - const nonActivateStates = ['FAILED', 'TERMINATED', 'UNKNOWN']; + const nonActiveStates = ['FAILED', 'TERMINATED', 'UNKNOWN']; const activeEnvs = scEnvs.filter(env => { - return !nonActivateStates.includes(env.status); + return !nonActiveStates.includes(env.status); }); const projectToActiveEnvs = _.groupBy(activeEnvs, 'projectId'); diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentCard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentCard.js index c42d23f5bf..84e9721cae 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentCard.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentCard.js @@ -7,6 +7,7 @@ import { Header, Label, Popup, Icon, Divider, Message, Table, Grid, Segment } fr import TimeAgo from 'react-timeago'; import { niceNumber } from '@aws-ee/base-ui/dist/helpers/utils'; +import { isAppStreamEnabled } from '../../helpers/settings'; import By from '../helpers/By'; import ScEnvironmentButtons from './parts/ScEnvironmentButtons'; import ScEnvironmentCost from './parts/ScEnvironmentCost'; @@ -40,6 +41,7 @@ class ScEnvironmentCard extends React.Component { {this.renderStatus(state)} {this.renderTitle(env)} {this.renderError(env)} + {this.renderWarning(env)} {this.renderButtons(env)} @@ -120,6 +122,20 @@ class ScEnvironmentCard extends React.Component { ); } + renderWarning(env) { + if (isAppStreamEnabled && !env.isAppStreamConfigured && env.state.canTerminate) { + return ( + + ); + } + + return null; + } + renderError(env) { if (_.isEmpty(env.error)) return null; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js index 4f445a7ab0..8c737a0189 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js @@ -112,7 +112,7 @@ class ScEnvironmentsList extends React.Component { } else if (isStoreEmpty(store)) { content = this.renderEmpty(); } else if (isStoreNotEmpty(store)) { - content = this.renderMain(appStreamProjectIds); + content = this.renderMain(); } else { content = null; } @@ -143,11 +143,10 @@ class ScEnvironmentsList extends React.Component { ); } - renderMain(appStreamProjectIds) { + renderMain() { const store = this.envsStore; const selectedFilter = this.selectedFilter; - let list = store.filtered(selectedFilter); - list = this.isAppStreamEnabled ? _.filter(list, env => _.includes(appStreamProjectIds, env.projectId)) : list; + const list = store.filtered(selectedFilter); const isEmpty = _.isEmpty(list); return ( diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentButtons.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentButtons.js index 5452a3f428..f35725aa9d 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentButtons.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentButtons.js @@ -194,7 +194,11 @@ class ScEnvironmentButtons extends React.Component { )} - {canConnect && ( + {/* Only let users connect to the environment if either of these conditions is true: + 1. AppStream is not enabled and environment can be connected to + 2. AppStream is enabled, environment is linked to an AppStream-configured account, and environment can be connected to + */} + {canConnect && (!isAppStreamEnabled || env.isAppStreamConfigured) && (