diff --git a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/appstream/appstream-sc-service.js b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/appstream/appstream-sc-service.js index f24937a422..811b2a5dfd 100644 --- a/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/appstream/appstream-sc-service.js +++ b/addons/addon-base-raas-appstream/packages/base-raas-appstream-services/lib/appstream/appstream-sc-service.js @@ -18,6 +18,7 @@ const Service = require('@aws-ee/base-services-container/lib/service'); const settingKeys = { appStreamImageName: 'appStreamImageName', + isAppStreamEnabled: 'isAppStreamEnabled', }; class AppStreamScService extends Service { @@ -76,6 +77,7 @@ class AppStreamScService extends Service { if (!stackName) { throw this.boom.badRequest( `Expected stack ${stackName} to be associated with the account ${accountId} but found`, + true, ); } @@ -88,7 +90,10 @@ class AppStreamScService extends Service { const { Names: fleetNames } = await appStream.listAssociatedFleets({ StackName: stackName }).promise(); if (!_.includes(fleetNames, fleetName)) { - throw this.boom.badRequest(`Expected fleet ${fleetName} to be associated with the AppStream stack but found`); + throw this.boom.badRequest( + `Expected fleet ${fleetName} to be associated with the AppStream stack but found`, + true, + ); } return { stackName, fleetName }; @@ -110,7 +115,10 @@ class AppStreamScService extends Service { return `${uid}-${sessionSuffix}`.replace(/[^\w+=,.@-]+/g, '').slice(0, 32); } - async urlForFirefox(requestContext, { environmentId }) { + async getStreamingUrl(requestContext, { environmentId, applicationId }) { + const isAppStreamEnabled = this.settings.get(settingKeys.isAppStreamEnabled); + if (isAppStreamEnabled !== 'true') return undefined; + const environmentScService = await this.service('environmentScService'); const appStream = await environmentScService.getClientSdkWithEnvMgmtRole( @@ -131,7 +139,7 @@ class AppStreamScService extends Service { FleetName: fleetName, StackName: stackName, UserId: this.generateUserId(requestContext, environment), - ApplicationId: 'Firefox', + ApplicationId: applicationId, }) .promise(); @@ -142,8 +150,10 @@ class AppStreamScService extends Service { } async urlForRemoteDesktop(requestContext, { environmentId, instanceId }) { - const environmentScService = await this.service('environmentScService'); + const isAppStreamEnabled = this.settings.get(settingKeys.isAppStreamEnabled); + if (isAppStreamEnabled !== 'true') return undefined; + const environmentScService = await this.service('environmentScService'); const environment = await environmentScService.mustFind(requestContext, { id: environmentId }); // Get stack and fleet 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 bbe8ffbf90..e99ab4139e 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 @@ -19,24 +19,33 @@ async function createConnectionUrl({ envId, connection }, { requestContext, cont const log = await container.find('log'); // Only wraps web urls via app stream (i.e., scheme = 'http' or 'https' or no scheme) const isHttp = connection.scheme === 'http' || connection.scheme === 'https' || _.isEmpty(connection.scheme); + const appStreamScService = await container.find('appStreamScService'); // Only wrap via AppStream if the connection.url exists let appStreamUrl; if (isHttp && connection.url) { log.debug({ - msg: `Target connection URL ${connection.url} will be made available for pasting into AppStream`, + msg: `Target connection URL ${connection.url} will be accessible via AppStream URL`, connection, }); - const appStreamScService = await container.find('appStreamScService'); - appStreamUrl = await appStreamScService.urlForFirefox(requestContext, { + appStreamUrl = await appStreamScService.getStreamingUrl(requestContext, { environmentId: envId, + applicationId: 'Firefox', + }); + } else if (connection.scheme === 'ssh') { + log.debug({ + msg: `Target instance ${connection.instanceId} will be available for SSH connection via AppStream URL`, + connection, + }); + appStreamUrl = await appStreamScService.getStreamingUrl(requestContext, { + environmentId: envId, + applicationId: 'EC2Linux', }); } else if (connection.scheme === 'rdp') { log.debug({ msg: `Will stream target RDP connection for instance ${connection.instanceId} via AppStream`, connection, }); - const appStreamScService = await container.find('appStreamScService'); appStreamUrl = await appStreamScService.urlForRemoteDesktop(requestContext, { environmentId: envId, instanceId: connection.instanceId, @@ -47,7 +56,7 @@ async function createConnectionUrl({ envId, connection }, { requestContext, cont // Retain the original destination URL so we don't have to trigger another API call connection.appstreamDestinationUrl = connection.url; - // Now rewrite connection.url to the AppStream streaming URL so it can opened in a new tab + // Now rewrite connection.url to the AppStream streaming URL so it can be opened in a new tab connection.url = appStreamUrl; log.debug({ msg: `Modified connection to use AppStream streaming URL ${connection.url}`, connection }); } diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvHttpConnectionExpanded.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvHttpConnectionExpanded.js deleted file mode 100644 index 72b91bd934..0000000000 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvHttpConnectionExpanded.js +++ /dev/null @@ -1,141 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { decorate, action, runInAction, observable, computed } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { withRouter } from 'react-router-dom'; -import { Table, Segment, Grid } from 'semantic-ui-react'; - -import CopyToClipboard from '../../helpers/CopyToClipboard'; - -// expected props -// destinationUrl (via props) -// keyName (via props) -// connectionId (via props) -// timeout (via props) -class ScEnvHttpConnectionExpanded extends React.Component { - constructor(props) { - super(props); - runInAction(() => { - // The count down value - this.countDown = undefined; - this.intervalId = undefined; - this.expired = false; - }); - } - - get destinationUrl() { - return this.props.destinationUrl; - } - - get timeout() { - return this.props.timeout; - } - - get keyName() { - return this.props.keyName; - } - - get connectionId() { - return this.props.connectionId; - } - - componentDidMount() { - this.startCountDown(); - } - - componentWillUnmount() { - this.clearInterval(); - } - - clearInterval() { - if (!_.isUndefined(this.intervalId)) { - clearInterval(this.intervalId); - this.intervalId = undefined; - } - this.countDown = undefined; - this.expired = false; - } - - startCountDown = () => { - if (!_.isUndefined(this.intervalId)) return; - this.countDown = this.timeout; - - this.intervalId = setInterval(async () => { - // eslint-disable-next-line consistent-return - runInAction(() => { - if (this.countDown <= 0) { - this.clearInterval(); - this.expired = true; - return; - } - this.countDown -= 1; - }); - }, 1000); - }; - - render() { - const connectionId = this.connectionId; - - return ( - - - - - {this.renderInfo()} - - -
{this.renderCountDown()}
-
-
-
-
-
-
- ); - } - - renderInfo() { - const destinationUrl = this.destinationUrl; - - return ( - <> -
- Click on this icon to copy the workspace destination URL: - -
-
(You can come back to this page to copy the destination URL)
- - ); - } - - renderCountDown() { - const countDown = this.countDown; - const expired = this.expired; - - if (expired) { - return
AppStream session has been opened in a new tab
; - } - - return ( -
-
- Secure AppStream tab opens in
{countDown}
seconds -
-
- ); - } -} - -// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da -decorate(ScEnvHttpConnectionExpanded, { - destinationUrl: computed, - keyName: computed, - connectionId: computed, - intervalId: observable, - countDown: observable, - expired: observable, - startCountDown: action, - clearInterval: action, -}); - -export default inject()(withRouter(observer(ScEnvHttpConnectionExpanded))); diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvSshConnRowExpanded.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvSshConnRowExpanded.js index 706c7ec2c7..62602caaf3 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvSshConnRowExpanded.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvSshConnRowExpanded.js @@ -3,46 +3,72 @@ import React from 'react'; import { decorate, action, runInAction, observable, computed } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Table, List, Segment, Label, Grid } from 'semantic-ui-react'; +import { Table, List, Segment, Label, Grid, Button } from 'semantic-ui-react'; + +import { displayError } from '@aws-ee/base-ui/dist/helpers/notification'; import CopyToClipboard from '../../helpers/CopyToClipboard'; +const openWindow = (url, windowFeatures) => { + return window.open(url, '_blank', windowFeatures); +}; + // expected props // networkInterfaces (via props) // keyName (via props) // connectionId (via props) +// environment (via props) class ScEnvSshConnRowExpanded extends React.Component { constructor(props) { super(props); + runInAction(() => { // The count down value this.countDown = undefined; this.intervalId = undefined; this.expired = false; + this.processingId = undefined; }); } + get isAppStreamEnabled() { + return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true'; + } + get networkInterfaces() { const entries = this.props.networkInterfaces; if (_.isEmpty(entries)) return []; const result = []; _.forEach(entries, item => { - if (item.publicDnsName) result.push({ value: item.publicDnsName, type: 'dns', scope: 'public', info: 'Public' }); + if (item.publicDnsName && !this.isAppStreamEnabled) + result.push({ value: item.publicDnsName, type: 'dns', scope: 'public', info: 'Public' }); if (item.privateIp) result.push({ value: item.privateIp, type: 'ip', scope: 'private', info: 'Private' }); }); return result; } + get environment() { + return this.props.scEnvironment; + } + get keyName() { return this.props.keyName; } + get envsStore() { + return this.props.scEnvironmentsStore; + } + get connectionId() { return this.props.connectionId; } + getConnectionStore() { + return this.envsStore.getScEnvConnectionStore(this.environment.id); + } + componentDidMount() { this.startCountDown(); } @@ -60,6 +86,33 @@ class ScEnvSshConnRowExpanded extends React.Component { this.expired = false; } + handleConnect = id => + action(async () => { + try { + runInAction(() => { + this.processingId = id; + }); + const store = this.getConnectionStore(); + const urlObj = await store.createConnectionUrl(id); + const appStreamUrl = urlObj.url; + if (appStreamUrl) { + const newTab = openWindow('about:blank'); + newTab.location = appStreamUrl; + } else { + throw Error('AppStream URL was not returned by the API'); + } + runInAction(() => { + this.processingId = id; + }); + } catch (error) { + displayError(error); + } finally { + runInAction(() => { + this.processingId = undefined; + }); + } + }); + startCountDown = () => { if (!_.isUndefined(this.intervalId)) return; this.countDown = 60; @@ -88,25 +141,65 @@ class ScEnvSshConnRowExpanded extends React.Component { - {this.renderInfo()} + {this.isAppStreamEnabled ? ( + {this.renderAppStreamInfo()} + ) : ( + {this.renderInfo()} + )} -
{this.renderCountDown()}
+
{this.renderCountDown()}
-
- Example: - - {example} - -
+ {!this.isAppStreamEnabled && ( +
+ Example: + + {example} + +
+ )}
); } + renderAppStreamInfo() { + const interfaces = this.networkInterfaces; + const network = interfaces[0]; + const connectionId = this.connectionId; + + return ( + + + + The Private IP Address to be used:{' '} + + + {this.renderHostLabel(network)} + + + + + + {this.isAppStreamEnabled && ( + + )} + + ); + } + renderInfo() { const interfaces = this.networkInterfaces; const moreThanOne = _.size(interfaces) > 1; @@ -189,10 +282,12 @@ class ScEnvSshConnRowExpanded extends React.Component { decorate(ScEnvSshConnRowExpanded, { networkInterfaces: computed, keyName: computed, + envsStore: computed, connectionId: computed, intervalId: observable, countDown: observable, expired: observable, + processingId: observable, startCountDown: action, clearInterval: action, }); diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentHttpConnections.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentHttpConnections.js index 77e07257e5..b6af9f8d57 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentHttpConnections.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentHttpConnections.js @@ -3,11 +3,11 @@ import React from 'react'; import { decorate, computed, action, runInAction, observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Segment, Icon, Button, Header, Table, List } from 'semantic-ui-react'; +import { Segment, Icon, Button, Header, Grid, Table, List } from 'semantic-ui-react'; import { displayError } from '@aws-ee/base-ui/dist/helpers/notification'; -import ScEnvHttpConnectionExpanded from './ScEnvHttpConnectionExpanded'; +import CopyToClipboard from '../../helpers/CopyToClipboard'; const openWindow = (url, windowFeatures) => { return window.open(url, '_blank', windowFeatures); @@ -22,11 +22,18 @@ class ScEnvironmentHttpConnections extends React.Component { runInAction(() => { // The id of the connection that is being processed this.processingId = ''; + this.appStreamGeneratingId = ''; + this.appStreamConnectingId = ''; this.destinationUrl = undefined; + this.streamingUrl = undefined; this.timeout = 10; }); } + get isAppStreamEnabled() { + return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true'; + } + get environment() { return this.props.scEnvironment; } @@ -55,7 +62,10 @@ class ScEnvironmentHttpConnections extends React.Component { const connectInfo = _.find(connections, ['id', id]) || {}; let url = connectInfo.url; - this.processingId = id; + runInAction(() => { + this.processingId = id; + }); + try { if (url) { // We use noopener and noreferrer for good practices https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener @@ -64,17 +74,7 @@ class ScEnvironmentHttpConnections extends React.Component { const urlObj = await store.createConnectionUrl(id); url = urlObj.url; - // If AppStream is enabled, copy destination URL to clipboard before new tab loads - if (process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true') { - runInAction(() => { - this.destinationUrl = urlObj.appstreamDestinationUrl; - }); - // Allow users time to copy the destination URL - setTimeout(() => { - const newTab = openWindow('about:blank'); - newTab.location = url; - }, this.timeout * 1000); - } else { + if (url) { const newTab = openWindow('about:blank'); newTab.location = url; } @@ -88,84 +88,177 @@ class ScEnvironmentHttpConnections extends React.Component { } }); + handleAppStreamConnect = (url, id) => + action(async () => { + try { + runInAction(() => { + this.appStreamConnectingId = id; + }); + if (url) { + const newTab = openWindow('about:blank'); + newTab.location = url; + } + } catch (error) { + displayError(error); + } finally { + runInAction(() => { + this.appStreamConnectingId = ''; + }); + } + }); + + handleGenerateAppStreamUrl = id => + action(async () => { + const store = this.getConnectionStore(); + runInAction(() => { + this.appStreamGeneratingId = id; + }); + try { + const urlObj = await store.createConnectionUrl(id); + runInAction(() => { + this.destinationUrl = urlObj.appstreamDestinationUrl; + this.streamingUrl = urlObj.url; + }); + } catch (error) { + displayError(error); + } finally { + runInAction(() => { + this.appStreamGeneratingId = ''; + }); + } + }); + render() { const env = this.environment; const state = env.state; const canConnect = state.canConnect; const connections = this.connections; - const destinationUrl = this.destinationUrl; - const processingId = this.processingId; - const isDisabled = id => processingId !== id && !_.isEmpty(processingId); - const isLoading = id => processingId === id; if (!canConnect) return null; return (
- + HTTP Connections - - {_.map(connections, item => ( + {this.renderBody(connections)} +
+
+ ); + } + + renderAppStreamBody(connections) { + const appStreamGeneratingId = this.appStreamGeneratingId; + const streamingUrl = this.streamingUrl; + const destinationUrl = this.destinationUrl; + const isDisabled = id => appStreamGeneratingId !== id && !_.isEmpty(appStreamGeneratingId); + const isLoading = id => appStreamGeneratingId === id; + + return ( + <> + {_.map(connections, item => ( + <> + {this.renderAppstreamInstructions(item)} + + + + +
{item.name || 'Generate'}
+
+
+ + {destinationUrl && ( <> - {process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true' ? ( - this.renderAppstreamInstructions(item) - ) : ( - <> - )} - - + + + + + +
+ Click on this icon to copy the workspace destination URL: + +
+
+
+
+
+
+ + + - -
{item.name || 'Connect'}
- {destinationUrl ? ( - - ) : ( - <> - )} - ))} - - - + )} + + ))} + + ); + } + + renderBody(connections) { + if (this.isAppStreamEnabled) { + return this.renderAppStreamBody(connections); + } + + const processingId = this.processingId; + const isDisabled = id => processingId !== id && !_.isEmpty(processingId); + const isLoading = id => processingId === id; + return ( + <> + {_.map(connections, item => ( + <> + + + + +
{item.name || 'Connect'}
+
+
+ + ))} + ); } renderAppstreamInstructions(item) { return ( <> - + - Connection instructions for your {item.id} AppStream workspace: + Connection instructions for your AppStream workspace: - - Step 1: Click the Connect button to start an AppStream session - - - Step 2: Copy the destination URL that becomes available below - - - Step 3: Paste (Ctrl + V) this destination URL in the new AppStream FireFox tab - + Click the 'Generate URL' button to start an AppStream Firefox session + Copy the destination URL that becomes available below + Paste (Ctrl + V) this destination URL in the new AppStream FireFox tab @@ -192,7 +285,10 @@ decorate(ScEnvironmentHttpConnections, { environment: computed, connections: computed, processingId: observable, + appStreamGeneratingId: observable, + appStreamConnectingId: observable, destinationUrl: observable, + streamingUrl: observable, }); export default inject('scEnvironmentsStore')(withRouter(observer(ScEnvironmentHttpConnections))); diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentRdpConnectionRow.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentRdpConnectionRow.js index 7ab15cf560..a07c416d0b 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentRdpConnectionRow.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentRdpConnectionRow.js @@ -9,6 +9,10 @@ import { displayError } from '@aws-ee/base-ui/dist/helpers/notification'; import CopyToClipboard from '../../helpers/CopyToClipboard'; +const openWindow = (url, windowFeatures) => { + return window.open(url, '_blank', windowFeatures); +}; + // expected props // - scEnvironment (via prop) // - connectionId (via prop) @@ -24,9 +28,14 @@ class ScEnvironmentRdpConnectionRow extends React.Component { this.processingGetInfo = false; // Should the password be shown this.showPassword = false; + this.processingId = undefined; }); } + get isAppStreamEnabled() { + return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true'; + } + get environment() { return this.props.scEnvironment; } @@ -71,6 +80,30 @@ class ScEnvironmentRdpConnectionRow extends React.Component { return result; } + handleConnect = id => + action(async () => { + try { + const store = this.getConnectionStore(); + const urlObj = await store.createConnectionUrl(id); + const appStreamUrl = urlObj.url; + if (appStreamUrl) { + const newTab = openWindow('about:blank'); + newTab.location = appStreamUrl; + } else { + throw Error('AppStream URL was not returned by the API'); + } + runInAction(() => { + this.processingId = id; + }); + } catch (error) { + displayError(error); + } finally { + runInAction(() => { + this.processingId = ''; + }); + } + }); + handleGetInfo = async () => { const store = this.getConnectionStore(); const connectionId = this.connectionId; @@ -128,59 +161,79 @@ class ScEnvironmentRdpConnectionRow extends React.Component { const username = 'Administrator'; const password = windowsRdpInfo.password; const showPassword = this.showPassword; + const connectionId = this.connectionId; const moreThanOne = _.size(interfaces) > 1; return ( - - - - Your Windows workspace can be accessed via an RDP client by using the DNS host name and credentials defined - below. - - - - The IP Address or DNS of the instance.{' '} - {moreThanOne ? 'Ask your administrator if you are not sure which one to use:' : ''} - - {_.map(interfaces, network => ( - - {this.renderHostLabel(network)} - - - ))} - - - - The username and password: - - - {this.renderUsernameLabel(username)} - + <> + + + + Your Windows workspace can be accessed via an RDP client by using the DNS host name and credentials + defined below. + + + {this.isAppStreamEnabled ? ( + <> + Click the 'Connect' button to navigate to the AppStream instance + + ) : ( + + The IP Address or DNS of the instance.{' '} + {moreThanOne ? 'Ask your administrator if you are not sure which one to use:' : ''} + + {_.map(interfaces, network => ( + + {this.renderHostLabel(network)} + + + ))} + - - {this.renderPasswordLabel(password)} - - - - - - -
- Additional information about connecting via RDP can be found in the documentation below: -
- - - Connect to Your Windows Instance - - -
-
+ )} + + + The username and password: + + + {this.renderUsernameLabel(username)} + + + + {this.renderPasswordLabel(password)} + + + + + + +
+ Additional information about connecting via RDP can be found in the documentation below: +
+ + + Connect to Your Windows Instance + + +
+
+ + {this.isAppStreamEnabled && windowsRdpInfo && ( + + + + + + )} + ); } @@ -223,6 +276,7 @@ decorate(ScEnvironmentRdpConnectionRow, { connection: computed, connectionId: computed, networkInterfaces: computed, + processingId: observable, windowsRdpInfo: observable, processingGetInfo: observable, showPassword: observable, diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnectionRow.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnectionRow.js index 077a858ee7..59dc5ecade 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnectionRow.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnectionRow.js @@ -86,6 +86,7 @@ class ScEnvironmentSshConnectionRow extends React.Component { this.processingSendKey = true; try { const result = await store.sendSshKey(connectionId, keyId); + runInAction(() => { this.networkInterfaces = _.get(result, 'networkInterfaces'); }); @@ -114,6 +115,7 @@ class ScEnvironmentSshConnectionRow extends React.Component { const options = this.keyPairOptions; const selectedKeyId = this.selectedKeyId; const selectedKeyName = this.selectedKeyName; + const rows = [ @@ -155,6 +157,8 @@ class ScEnvironmentSshConnectionRow extends React.Component { networkInterfaces={networkInterfaces} keyName={selectedKeyName} connectionId={item.id} + scEnvironmentsStore={this.envsStore} + scEnvironment={this.environment} />, ); } diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnections.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnections.js index fb5dd5f00e..1bbbdf828c 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnections.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/parts/ScEnvironmentSshConnections.js @@ -3,7 +3,7 @@ import React from 'react'; import { decorate, computed, action, runInAction, observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Segment, Icon, Button, Header, Table, Message } from 'semantic-ui-react'; +import { Segment, Icon, Button, Header, Table, Message, List } from 'semantic-ui-react'; import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils'; import { isStoreLoading, isStoreError, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore'; @@ -32,6 +32,10 @@ class ScEnvironmentSshConnections extends React.Component { } } + get isAppStreamEnabled() { + return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true'; + } + get environment() { return this.props.scEnvironment; } @@ -75,6 +79,34 @@ class ScEnvironmentSshConnections extends React.Component { return
{content}
; } + renderAppStreamInfo() { + return ( + + Connection instructions for your AppStream workspace: + + Select your SSH key below. You must have downloaded this already. + Paste the key's contents into AppStream Notepad in .PEM format + + Save the file in the Downloads folder in .PEM format named like "KeyName.pem" (with quotes) + + Open PuttyGen in AppStream and convert your private PEM key to PPK format + Enter the PPK file and private IP address in Putty to SSH into EC2 + Delete this file once EC2 connection is established + +
More information on connecting to your Linux instance from Windows OS:
+ + + Connecting from Windows via Putty + + +
+ ); + } + renderConnections() { const env = this.environment; const showCreateKey = this.showCreateKey; @@ -86,6 +118,7 @@ class ScEnvironmentSshConnections extends React.Component { return (
+ {this.isAppStreamEnabled && this.renderAppStreamInfo()} {empty && ( Attention! @@ -114,7 +147,9 @@ class ScEnvironmentSshConnections extends React.Component { {_.map(connections, item => ( - + <> + + ))} diff --git a/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js index 8e7e09ca37..8a76de6bed 100644 --- a/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js +++ b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js @@ -30,6 +30,8 @@ const environmentScWfPlugin = require('@aws-ee/environment-sc-workflows/lib/plug const bassRaasEnvTypeVarsPlugin = require('@aws-ee/base-raas-services/lib/plugins/env-provisioning-plugin'); const rolesOnlyStrategyPlugin = require('@aws-ee/base-raas-services/lib/plugins/roles-only-strategy-plugin'); const legacyStrategyPlugin = require('@aws-ee/base-raas-services/lib/plugins/legacy-strategy-plugin'); +const baseRaasAppstreamServicesPlugin = require('@aws-ee/base-raas-appstream-rest-api/lib/plugins/services-plugin'); +const baseRaasAppstreamEnvTypeVarsPlugin = require('@aws-ee/base-raas-appstream-services/lib/plugins/env-sc-provisioning-plugin'); const servicesPlugin = require('services/lib/plugins/services-plugin'); @@ -38,6 +40,7 @@ const extensionPoints = { baseServicesPlugin, baseWfServicesPlugin, baseRaasServicesPlugin, + baseRaasAppstreamServicesPlugin, environmentTypeServicesPlugin, keyPairServicesPlugin, servicesPlugin, @@ -48,7 +51,7 @@ const extensionPoints = { 'workflows': [baseRaasWorkflowsPlugin, environmentScWfPlugin], 'workflow-assignments': [], 'cfn-templates': [baseRaasCfnTemplatesPlugin], - 'env-provisioning': [bassRaasEnvTypeVarsPlugin], // Plugins to participate in resolving list of "Environment Type Configuration Variables". See "addons/addon-environment-sc-api/README.md" to understand what "Environment Type Configuration Variables" are + 'env-provisioning': [bassRaasEnvTypeVarsPlugin, baseRaasAppstreamEnvTypeVarsPlugin], // Plugins to participate in resolving list of "Environment Type Configuration Variables". See "addons/addon-environment-sc-api/README.md" to understand what "Environment Type Configuration Variables" are // --- Authorization Plugins ---/ diff --git a/scripts/app-stream/SETUP.md b/scripts/app-stream/SETUP.md index 9dd8c0b03d..e61132198b 100644 --- a/scripts/app-stream/SETUP.md +++ b/scripts/app-stream/SETUP.md @@ -29,7 +29,7 @@ Note: Please set up your [AWS Profile](https://docs.aws.amazon.com/cli/latest/us cd ~\Documents # Pull the Image Builder script from Github -Invoke-WebRequest -Uri https://raw.githubusercontent.com/awslabs/service-workbench-on-aws/feat-secure-workspace-egress/scripts/app-stream/buildImage.ps1 -OutFile buildImage.ps1 +Invoke-WebRequest -Uri https://raw.githubusercontent.com/awslabs/service-workbench-on-aws/mainline/scripts/app-stream/buildImage.ps1 -OutFile buildImage.ps1 # Execute Image builder script .\buildImage.ps1 diff --git a/scripts/app-stream/buildImage.ps1 b/scripts/app-stream/buildImage.ps1 index 19435846ee..f0f8ef30a1 100644 --- a/scripts/app-stream/buildImage.ps1 +++ b/scripts/app-stream/buildImage.ps1 @@ -3,6 +3,8 @@ Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManage choco install -y putty.install +Invoke-WebRequest -Uri https://raw.githubusercontent.com/awslabs/service-workbench-on-aws/mainline/scripts/app-stream/ec2linux.ps1 -OutFile 'C:\Users\Public\Documents\EC2Linux.ps1' + # Prepare remote desktop script with arguments Set-Content C:\Users\public\Documents\MicrosoftRemoteDesktop.ps1 "mstsc /f /v:`"`$env:APPSTREAM_SESSION_CONTEXT`":3389" @@ -18,6 +20,10 @@ cd "C:\Program Files\Amazon\Photon\ConsoleImageBuilder" .\image-assistant.exe add-application --absolute-app-path "C:\Program Files (x86)\Mozilla Firefox\firefox.exe" --display-name Firefox --name Firefox +.\image-assistant.exe add-application --absolute-app-path "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" ` +--display-name EC2Linux --name EC2Linux ` +--launch-parameters '`"-File "C:\Users\Public\Documents\EC2Linux.ps1" -ExecutionPolicy Bypass`"' + .\image-assistant.exe add-application --absolute-app-path "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" ` --display-name MicrosoftRemoteDesktop --name MicrosoftRemoteDesktop ` --launch-parameters '`"-File "C:\Users\Public\Documents\MicrosoftRemoteDesktop.ps1" -ExecutionPolicy Bypass`"' ` diff --git a/scripts/app-stream/ec2linux.ps1 b/scripts/app-stream/ec2linux.ps1 new file mode 100644 index 0000000000..c0ee86766c --- /dev/null +++ b/scripts/app-stream/ec2linux.ps1 @@ -0,0 +1,3 @@ +Start-Process -FilePath "C:\Program Files\PuTTY\putty.exe" +Start-Process -FilePath "C:\Program Files\PuTTY\puttygen.exe" +Start-Process -FilePath "C:\Windows\System32\notepad.exe" \ No newline at end of file