Skip to content

Commit

Permalink
Add option for private PCUI deployment (#251)
Browse files Browse the repository at this point in the history
* Move ApiGateway from HTTP to REST

* Configure routing for new stage 'pcui'

* Add VpcEndpointId parameter to conditionally make PCUI API private
  • Loading branch information
rkilpadi authored and gmarciani committed Nov 8, 2023
1 parent 38398b9 commit 90b8c3c
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 49 deletions.
2 changes: 1 addition & 1 deletion api/PclusterApiHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ def login():
id_token = code_resp.json().get("id_token")
refresh_token = code_resp.json().get("refresh_token", None)

resp = redirect("/index.html", code=302)
resp = redirect("/pcui/index.html", code=302)
resp.set_cookie("accessToken", access_token, httponly=True, secure=True, samesite="Lax")
resp.set_cookie("idToken", id_token, httponly=True, secure=True, samesite="Lax")
if refresh_token is not None:
Expand Down
1 change: 1 addition & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def default(self, obj):

def run():
app = utils.build_flask_app(__name__)
app.config["APPLICATION_ROOT"] = '/pcui'
app.json_encoder = PClusterJSONEncoder
app.url_map.converters["regex"] = RegexConverter
CSRF(app, CognitoFingerprintGenerator(CLIENT_ID, CLIENT_SECRET, USER_POOL_ID))
Expand Down
3 changes: 2 additions & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
basePath: '/pcui',
experimental: {
images: {
unoptimized: true,
Expand Down Expand Up @@ -47,4 +48,4 @@ const withTM = require("next-transpile-modules")([
"@cloudscape-design/design-tokens"
]);

module.exports = withTM(nextConfig);
module.exports = withTM(nextConfig);
Binary file removed frontend/public/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion frontend/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
}

private redirectToHomepage = () => {
window.location.href = '/'
window.location.href = '/pcui'
}

public static getDerivedStateFromError(_: Error): State {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/NoMatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function NoMatch() {
<p>{t('noMatch.links')}</p>
<ul>
<li>
<Link href="/">{t('noMatch.home')}</Link>
<Link href="/pcui">{t('noMatch.home')}</Link>
</li>
</ul>
</TextContent>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function SideBar() {
{
type: 'link',
text: t('global.menu.viewLicense'),
href: '/license.txt',
href: '/pcui/license.txt',
external: true,
},
]
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default function Topbar() {
{
id: 'signout',
text: t('global.topBar.signOut'),
href: '/logout',
href: '/pcui/logout',
},
],
[t],
Expand All @@ -146,9 +146,9 @@ export default function Topbar() {
<TopNavigation
identity={{
title: t('global.projectDisplayName'),
href: '/',
href: '/pcui',
logo: {
src: '/img/pcluster.svg',
src: '/pcui/img/pcluster.svg',
alt: t('global.topBar.logoAltText'),
},
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/http/executeRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const axiosInstance = axios.create({

function getHost() {
if (process.env.NODE_ENV !== 'production') return 'http://localhost:5001/'
return '/'
return '/pcui'
}

export type HTTPMethod = 'get' | 'put' | 'post' | 'patch' | 'delete'
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/old-pages/Configure/Queues/Queues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ function Queue({index}: any) {
])

const capacityTypes: [string, string, string][] = [
['ONDEMAND', 'On-Demand', '/img/od.svg'],
['SPOT', 'Spot', '/img/spot.svg'],
['ONDEMAND', 'On-Demand', '/pcui/img/od.svg'],
['SPOT', 'Spot', '/pcui/img/spot.svg'],
]
const capacityTypePath = [...queuesPath, index, 'CapacityType']
const capacityType: string = useState(capacityTypePath) || 'ONDEMAND'
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function App({Component, pageProps}: AppProps) {

const onAceLoad = useCallback(() => {
window.editor = window.ace.edit('editor')
window.ace.config.set('basePath', '/third-party/ace-1.4.13/')
window.ace.config.set('basePath', '/pcui/third-party/ace-1.4.13/')
window.ace.config.set('loadWorkerFromBlob', false)
window.ace.config.set('showFoldWidgets', false)
window.ace.config.set('showPrintMargin', false)
Expand All @@ -75,7 +75,10 @@ function App({Component, pageProps}: AppProps) {
</I18nextProvider>
</QueryClientProvider>
<div id="editor"></div>
<Script src="/third-party/ace-1.4.13/ace.min.js" onLoad={onAceLoad} />
<Script
src="/pcui/third-party/ace-1.4.13/ace.min.js"
onLoad={onAceLoad}
/>
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function Document() {
<Html lang="en">
<Head>
<meta charSet="utf-8" />
<link rel="icon" href="favicon.ico" />
<link rel="icon" href="/pcui/img/pcluster.svg" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="AWS ParallelCluster UI" />
</Head>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {useLoadingState} from '../components/useLoadingState'

export default function App() {
const {loading, content} = useLoadingState(
<BrowserRouter>
<BrowserRouter basename='/pcui'>
<Routes>
<Route
path="index.html"
Expand Down
168 changes: 134 additions & 34 deletions infrastructure/parallelcluster-ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ Parameters:
Description: When specified, the URI of the Docker image for the Lambda of the ParallelCluster UI container
Type: String
Default: public.ecr.aws/pcm/parallelcluster-ui:2023.10.0
VpcEndpointId:
Description: Enter a VPC endpoint to enable private PCUI implementation. When enabled, the API will only accept requests from within the given VPC.
Type: String
Default: ''
LambdaSubnetIds:
Description: Comma separated list of subnet IDs to be associated with the PCUI Lambda function. These subnets should be private and associated with your VPC endpoint.
Type: CommaDelimitedList
Default: ''
LambdaSecurityGroupIds:
Description: Comma separated list of security groups to be associated with the PCUI Lambda function.
Type: CommaDelimitedList
Default: ''
UserPoolId:
Description: UserPoolId of a previously deployed PCUI Cognito User Pool. Leave blank to create a new one.
Type: String
Expand Down Expand Up @@ -47,6 +59,12 @@ Metadata:
default: ParallelCluster UI
Parameters:
- PublicEcrImageUri
- Label:
default: (Optional) Private PCUI
Parameters:
- VpcEndpointId
- LambdaSubnetIds
- LambdaSecurityGroupIds
- Label:
default: (Optional) External PCUI Cognito
Parameters:
Expand Down Expand Up @@ -82,6 +100,7 @@ Conditions:
Fn::And:
- !Not [!Equals [!Ref ImageBuilderVpcId, ""]]
- !Not [!Equals [!Ref ImageBuilderSubnetId, ""]]
IsPrivate: !Not [!Equals [!Ref VpcEndpointId, ""]]
HasDefaultInfrastructure: !Equals [!Ref InfrastructureBucket, '']
UseExistingCognito:
!And
Expand Down Expand Up @@ -133,6 +152,7 @@ Resources:
ApiDefinitionS3Uri: !Sub s3://${AWS::Region}-aws-parallelcluster/parallelcluster/${Version}/api/ParallelCluster.openapi.yaml
CreateApiUserRole: False
EnableIamAdminAccess: True
VpcEndpointId: !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ]
ImageBuilderSubnetId: !If
- UseNonDockerizedPCAPI
- !Ref AWS::NoValue
Expand Down Expand Up @@ -162,12 +182,18 @@ Resources:
Value: !FindInMap [ ParallelClusterUI, Constants, Version ]
TracingConfig:
Mode: Active
VpcConfig:
Fn::If:
- IsPrivate
- SubnetIds: !Ref LambdaSubnetIds
SecurityGroupIds: !Ref LambdaSecurityGroupIds
- !Ref AWS::NoValue
Environment:
Variables:
API_BASE_URL: !GetAtt [ ParallelClusterApi, Outputs.ParallelClusterApiInvokeUrl ]
API_VERSION: !Ref Version
SITE_URL: !Sub
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/pcui
- Api: !Ref ApiGateway
AUTH_PATH: !If [ UseExistingCognito, !Ref UserPoolAuthDomain, !GetAtt [ Cognito, Outputs.UserPoolAuthDomain ]]
SECRET_ID: !GetAtt UserPoolClientSecret.SecretName
Expand All @@ -185,52 +211,121 @@ Resources:
- [!Select [2, !Split ['/', !Ref EcrImage]], !Select [3, !Split ['/', !Ref EcrImage]]]

ApiGateway:
Type: AWS::ApiGatewayV2::Api
Type: AWS::ApiGateway::RestApi
Properties:
Name: ParallelClusterUI
Description: ParallelClusterUI Lambda Proxy
ProtocolType: HTTP
Policy:
Fn::If:
- IsPrivate
- Version: "2012-10-17"
Statement:
- Effect: "Deny"
Principal: "*"
Action: "execute-api:Invoke"
Resource: "execute-api:/*"
Condition:
StringNotEquals:
aws:sourceVpce: !Ref VpcEndpointId
- Effect: "Allow"
Principal: "*"
Action: "execute-api:Invoke"
Resource: "execute-api:/*"
- Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: "*"
Action: "execute-api:Invoke"
Resource: "execute-api:/*"
EndpointConfiguration:
Types:
- !If [ IsPrivate, PRIVATE, REGIONAL ]
VpcEndpointIds:
- !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ]
Tags:
'parallelcluster:ui:version': !FindInMap [ParallelClusterUI, Constants, Version]

ApiGatewayRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref ApiGateway
RouteKey: $default
Target: !Sub
- 'integrations/${IntegrationId}'
- { IntegrationId: !Ref ApiGatewayIntegration }

ApiGatewayIntegration:
Type: AWS::ApiGatewayV2::Integration
- Key: 'parallelcluster:ui:version'
Value: !FindInMap [ParallelClusterUI, Constants, Version]

ApiGatewayProxyResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApiGateway
ParentId: !GetAtt ApiGateway.RootResourceId
PathPart: '{proxy+}'

ApiGatewayMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApiGateway
ResourceId: !Ref ApiGatewayProxyResource
HttpMethod: ANY
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub
- arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFunction-${StackIdSuffix}/invocations
- { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }

ApiGatewayRootMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApiGateway
ResourceId: !GetAtt ApiGateway.RootResourceId
HttpMethod: ANY
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub
- arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFunction-${StackIdSuffix}/invocations
- { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }

ApiGatewayLogRole:
Type: AWS::IAM::Role
Properties:
ApiId: !Ref ApiGateway
Description: 'ANY integration'
IntegrationType: AWS_PROXY
IntegrationUri: !Sub
- arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFunction-${StackIdSuffix}/invocations
- { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
PayloadFormatVersion: 2.0
TimeoutInMillis: 30000
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: apigateway.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'

ApiGatewayAccessLog:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 90

ApiGatewayAccount:
Type: AWS::ApiGateway::Account
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayLogRole.Arn

ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn: ApiGatewayMethod
Properties:
RestApiId: !Ref ApiGateway

ApiGatewayStage:
Type: AWS::ApiGatewayV2::Stage
Type: AWS::ApiGateway::Stage
DependsOn: ApiGatewayAccount
Properties:
AccessLogSettings:
AccessLogSetting:
DestinationArn: !GetAtt ApiGatewayAccessLog.Arn
Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","path":"$context.path", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }'
ApiId: !Ref ApiGateway
StageName: $default
AutoDeploy: True
DefaultRouteSettings:
ThrottlingBurstLimit: 50
ThrottlingRateLimit: 100
RestApiId: !Ref ApiGateway
DeploymentId: !Ref ApiGatewayDeployment
StageName: pcui
MethodSettings:
- ResourcePath: '/*'
HttpMethod: '*'
ThrottlingBurstLimit: 50
ThrottlingRateLimit: 100

CognitoAppClient:
Type: AWS::Cognito::UserPoolClient
Expand All @@ -246,7 +341,7 @@ Resources:
- ALLOW_REFRESH_TOKEN_AUTH
CallbackURLs:
- !Sub
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/login
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/pcui/login
- Api: !Ref ApiGateway
SupportedIdentityProviders:
- COGNITO
Expand Down Expand Up @@ -707,6 +802,11 @@ Resources:
- ec2:DescribeInstanceTypes
- ec2:DescribeSubnets
- ec2:DescribeKeyPairs
- ec2:DescribeNetworkInterfaces
- ec2:CreateNetworkInterface
- ec2:DeleteNetworkInterface
- ec2:DescribeInstances
- ec2:AttachNetworkInterface
Resource:
- '*'
Effect: Allow
Expand Down Expand Up @@ -822,7 +922,7 @@ Outputs:
Export:
Name: !Sub ${AWS::StackName}-ParallelClusterUISite
Value: !Sub
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}
- https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/pcui
- Api: !Ref ApiGateway
AppClientId:
Description: The id of the Cognito app client
Expand Down

0 comments on commit 90b8c3c

Please sign in to comment.