From b13651ebf26ec4daf2cd34fc26899e3c27c85938 Mon Sep 17 00:00:00 2001 From: Markus Tacker Date: Tue, 23 Jul 2019 14:27:04 +0200 Subject: [PATCH] feat: make device UI own project --- cdk/apps/ContinuousDeployment.ts | 15 +- cdk/cloudformation-cd.ts | 16 +- cdk/resources/WebAppCD.ts | 204 ++++++++++++++++++ cdk/resources/WebAppHosting.ts | 72 +++++++ cdk/stacks/Bifravst.ts | 105 ++++----- cdk/stacks/ContinuousDeployment.ts | 168 +++------------ continuous-deployment-app.yml | 17 -- continuous-deployment-device-ui-app.yml | 17 ++ continuous-deployment-web-app.yml | 17 ++ data/device-ui.html | 69 ------ package.json | 6 +- scripts/cloudformation/stackOutput.ts | 18 ++ .../stackOutputToCRAEnvironment.ts | 18 +- scripts/connect.ts | 42 ++-- scripts/device/portForDevice.ts | 2 +- scripts/device/ui-server.ts | 40 ++-- 16 files changed, 489 insertions(+), 337 deletions(-) create mode 100644 cdk/resources/WebAppCD.ts create mode 100644 cdk/resources/WebAppHosting.ts delete mode 100644 continuous-deployment-app.yml create mode 100644 continuous-deployment-device-ui-app.yml create mode 100644 continuous-deployment-web-app.yml delete mode 100644 data/device-ui.html create mode 100644 scripts/cloudformation/stackOutput.ts diff --git a/cdk/apps/ContinuousDeployment.ts b/cdk/apps/ContinuousDeployment.ts index 83120420..870e2238 100644 --- a/cdk/apps/ContinuousDeployment.ts +++ b/cdk/apps/ContinuousDeployment.ts @@ -5,10 +5,17 @@ export class ContinuousDeploymentApp extends CloudFormation.App { public constructor(props: { stackId: string bifravstStackId: string - owner: string - repo: string - branch: string - app: { + bifravstAWS: { + owner: string + repo: string + branch: string + } + webApp: { + owner: string + repo: string + branch: string + } + deviceUI: { owner: string repo: string branch: string diff --git a/cdk/cloudformation-cd.ts b/cdk/cloudformation-cd.ts index 654119ff..ce2a9f9a 100644 --- a/cdk/cloudformation-cd.ts +++ b/cdk/cloudformation-cd.ts @@ -12,10 +12,16 @@ const pjson = JSON.parse( new ContinuousDeploymentApp({ stackId: `${STACK_ID}-continuous-deployment`, bifravstStackId: STACK_ID, - ...extractRepoAndOwner(pjson.repository.url), - branch: pjson.deploy.branch || 'saga', - app: { - ...extractRepoAndOwner(pjson.deploy.app.repository), - branch: pjson.deploy.app.branch || 'saga', + bifravstAWS: { + ...extractRepoAndOwner(pjson.repository.url), + branch: pjson.deploy.branch || 'saga', + }, + webApp: { + ...extractRepoAndOwner(pjson.deploy.webApp.repository), + branch: pjson.deploy.webApp.branch || 'saga', + }, + deviceUI: { + ...extractRepoAndOwner(pjson.deploy.deviceUI.repository), + branch: pjson.deploy.deviceUI.branch || 'saga', }, }).synth() diff --git a/cdk/resources/WebAppCD.ts b/cdk/resources/WebAppCD.ts new file mode 100644 index 00000000..cb929960 --- /dev/null +++ b/cdk/resources/WebAppCD.ts @@ -0,0 +1,204 @@ +import * as CloudFormation from '@aws-cdk/core' +import * as IAM from '@aws-cdk/aws-iam' +import * as CodeBuild from '@aws-cdk/aws-codebuild' +import * as CodePipeline from '@aws-cdk/aws-codepipeline' +import * as SSM from '@aws-cdk/aws-ssm' +import * as S3 from '@aws-cdk/aws-s3' + +/** + * This sets up the continuous delivery for a web-app + */ +export class WebAppCD extends CloudFormation.Construct { + public constructor( + parent: CloudFormation.Stack, + id: string, + properties: { + bifravstAWS: { + owner: string + repo: string + branch: string + } + webApp: { + owner: string + repo: string + branch: string + } + bifravstStackId: string + githubToken: SSM.IStringParameter + buildSpec: string + }, + ) { + super(parent, id) + + const { + bifravstStackId, + bifravstAWS, + webApp, + githubToken, + buildSpec, + } = properties + + const codeBuildRole = new IAM.Role(this, 'CodeBuildRole', { + assumedBy: new IAM.ServicePrincipal('codebuild.amazonaws.com'), + inlinePolicies: { + rootPermissions: new IAM.PolicyDocument({ + statements: [ + new IAM.PolicyStatement({ + resources: ['*'], + actions: ['*'], + }), + ], + }), + }, + }) + + const project = new CodeBuild.CfnProject(this, 'CodeBuildProject', { + name: id, + source: { + type: 'CODEPIPELINE', + buildSpec, + }, + serviceRole: codeBuildRole.roleArn, + artifacts: { + type: 'CODEPIPELINE', + }, + environment: { + type: 'LINUX_CONTAINER', + computeType: 'BUILD_GENERAL1_LARGE', + image: 'aws/codebuild/standard:2.0', + environmentVariables: [ + { + name: 'STACK_ID', + value: bifravstStackId, + }, + ], + }, + }) + project.node.addDependency(codeBuildRole) + + const bucket = new S3.Bucket(this, 'bucket', { + removalPolicy: CloudFormation.RemovalPolicy.DESTROY, + }) + + const pipelineRole = new IAM.Role(this, 'CodePipelineRole', { + assumedBy: new IAM.ServicePrincipal('codepipeline.amazonaws.com'), + inlinePolicies: { + controlCodeBuild: new IAM.PolicyDocument({ + statements: [ + new IAM.PolicyStatement({ + resources: [project.attrArn], + actions: ['codebuild:*'], + }), + ], + }), + writeToCDBucket: new IAM.PolicyDocument({ + statements: [ + new IAM.PolicyStatement({ + resources: [bucket.bucketArn, `${bucket.bucketArn}/*`], + actions: ['s3:*'], + }), + ], + }), + }, + }) + + const pipeline = new CodePipeline.CfnPipeline(this, 'CodePipeline', { + roleArn: pipelineRole.roleArn, + artifactStore: { + type: 'S3', + location: bucket.bucketName, + }, + name: id, + stages: [ + { + name: 'Source', + actions: [ + { + name: 'BifravstAWSSourceCode', + actionTypeId: { + category: 'Source', + owner: 'ThirdParty', + version: '1', + provider: 'GitHub', + }, + outputArtifacts: [ + { + name: 'BifravstAWS', + }, + ], + configuration: { + Branch: bifravstAWS.branch, + Owner: bifravstAWS.owner, + Repo: bifravstAWS.repo, + OAuthToken: githubToken.stringValue, + }, + }, + { + name: 'WebAppSourceCode', + actionTypeId: { + category: 'Source', + owner: 'ThirdParty', + version: '1', + provider: 'GitHub', + }, + outputArtifacts: [ + { + name: 'WebApp', + }, + ], + configuration: { + Branch: webApp.branch, + Owner: webApp.owner, + Repo: webApp.repo, + OAuthToken: githubToken.stringValue, + }, + }, + ], + }, + { + name: 'Deploy', + actions: [ + { + name: 'DeployWebApp', + inputArtifacts: [{ name: 'BifravstAWS' }, { name: 'WebApp' }], + actionTypeId: { + category: 'Build', + owner: 'AWS', + version: '1', + provider: 'CodeBuild', + }, + configuration: { + ProjectName: project.name, + PrimarySource: 'BifravstAWS', + }, + outputArtifacts: [ + { + name: 'BuildId', + }, + ], + }, + ], + }, + ], + }) + pipeline.node.addDependency(pipelineRole) + + new CodePipeline.CfnWebhook(this, 'webhook', { + name: `${id}-InvokePipelineFromGitHubChange`, + targetPipeline: id, + targetPipelineVersion: 1, + targetAction: 'Source', + filters: [ + { + jsonPath: '$.ref', + matchEquals: `refs/heads/${webApp.branch}`, + }, + ], + authentication: 'GITHUB_HMAC', + authenticationConfiguration: { + secretToken: githubToken.stringValue, + }, + registerWithThirdParty: false, + }) + } +} diff --git a/cdk/resources/WebAppHosting.ts b/cdk/resources/WebAppHosting.ts new file mode 100644 index 00000000..f82e7df4 --- /dev/null +++ b/cdk/resources/WebAppHosting.ts @@ -0,0 +1,72 @@ +import * as CloudFormation from '@aws-cdk/core' +import * as CloudFront from '@aws-cdk/aws-cloudfront' +import * as S3 from '@aws-cdk/aws-s3' + +/** + * This sets up the web hosting for a web app + */ +export class WebAppHosting extends CloudFormation.Resource { + public readonly bucket: S3.IBucket + public readonly distribution: CloudFront.CfnDistribution + + public constructor(parent: CloudFormation.Stack, id: string) { + super(parent, id) + + this.bucket = new S3.Bucket(this, 'bucket', { + publicReadAccess: true, + cors: [ + { + allowedHeaders: ['*'], + allowedMethods: [S3.HttpMethods.GET], + allowedOrigins: ['*'], + exposedHeaders: ['Date'], + maxAge: 3600, + }, + ], + removalPolicy: CloudFormation.RemovalPolicy.DESTROY, + websiteIndexDocument: 'index.html', + websiteErrorDocument: 'error.html', + }) + + this.distribution = new CloudFront.CfnDistribution( + this, + 'websiteDistribution', + { + distributionConfig: { + enabled: true, + priceClass: 'PriceClass_100', + defaultRootObject: 'index.html', + defaultCacheBehavior: { + allowedMethods: ['HEAD', 'GET', 'OPTIONS'], + cachedMethods: ['HEAD', 'GET'], + compress: true, + forwardedValues: { + queryString: true, + headers: [ + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Origin', + ], + }, + smoothStreaming: false, + targetOriginId: 'S3', + viewerProtocolPolicy: 'redirect-to-https', + }, + ipv6Enabled: true, + viewerCertificate: { + cloudFrontDefaultCertificate: true, + }, + origins: [ + { + domainName: `${this.bucket.bucketName}.s3-website.${parent.region}.amazonaws.com`, + id: 'S3', + customOriginConfig: { + originProtocolPolicy: 'http-only', + }, + }, + ], + }, + }, + ) + } +} diff --git a/cdk/stacks/Bifravst.ts b/cdk/stacks/Bifravst.ts index 0d37cccf..9408331b 100644 --- a/cdk/stacks/Bifravst.ts +++ b/cdk/stacks/Bifravst.ts @@ -5,12 +5,12 @@ import { } from '@aws-cdk/aws-cloudformation' import * as Cognito from '@aws-cdk/aws-cognito' import * as Lambda from '@aws-cdk/aws-lambda' -import * as CloudFront from '@aws-cdk/aws-cloudfront' import * as IAM from '@aws-cdk/aws-iam' import * as S3 from '@aws-cdk/aws-s3' import * as Iot from '@aws-cdk/aws-iot' import { BifravstLambdas } from '../cloudformation' import { LayeredLambdas } from '@nrfcloud/package-layered-lambdas' +import { WebAppHosting } from '../resources/WebAppHosting' export class BifravstStack extends CloudFormation.Stack { public constructor( @@ -123,76 +123,37 @@ export class BifravstStack extends CloudFormation.Stack { exportName: `${this.stackName}:userPoolClientId`, }) - const websiteBucket = new S3.Bucket(this, 'websitBucket', { - publicReadAccess: true, - cors: [ - { - allowedHeaders: ['*'], - allowedMethods: [S3.HttpMethods.GET], - allowedOrigins: ['*'], - exposedHeaders: ['Date'], - maxAge: 3600, - }, - ], - removalPolicy: CloudFormation.RemovalPolicy.DESTROY, - websiteIndexDocument: 'index.html', - websiteErrorDocument: 'error.html', + const webAppHosting = new WebAppHosting(this, 'webAppHosting') + new CloudFormation.CfnOutput(this, 'webAppBucketName', { + value: webAppHosting.bucket.bucketName, + exportName: `${this.stackName}:webAppBucketName`, }) - new CloudFormation.CfnOutput(this, 'websiteBucketName', { - value: websiteBucket.bucketName, - exportName: `${this.stackName}:websiteBucketName`, + new CloudFormation.CfnOutput(this, 'cloudFrontDistributionIdWebApp', { + value: webAppHosting.distribution.ref, + exportName: `${this.stackName}:cloudFrontDistributionIdWebApp`, }) - const distribution = new CloudFront.CfnDistribution( - this, - 'websiteDistribution', - { - distributionConfig: { - enabled: true, - priceClass: 'PriceClass_100', - defaultRootObject: 'index.html', - defaultCacheBehavior: { - allowedMethods: ['HEAD', 'GET', 'OPTIONS'], - cachedMethods: ['HEAD', 'GET'], - compress: true, - forwardedValues: { - queryString: true, - headers: [ - 'Access-Control-Request-Headers', - 'Access-Control-Request-Method', - 'Origin', - ], - }, - smoothStreaming: false, - targetOriginId: 'S3', - viewerProtocolPolicy: 'redirect-to-https', - }, - ipv6Enabled: true, - viewerCertificate: { - cloudFrontDefaultCertificate: true, - }, - origins: [ - { - domainName: `${websiteBucket.bucketName}.s3-website.${this.region}.amazonaws.com`, - id: 'S3', - customOriginConfig: { - originProtocolPolicy: 'http-only', - }, - }, - ], - }, - }, - ) + new CloudFormation.CfnOutput(this, 'webAppDomainName', { + value: webAppHosting.distribution.attrDomainName, + exportName: `${this.stackName}:webAppDomainName`, + }) - new CloudFormation.CfnOutput(this, 'cloudfrontDistributionId', { - value: distribution.ref, - exportName: `${this.stackName}:cloudFrontDistributionId`, + const deviceUIHosting = new WebAppHosting(this, 'deviceUIHosting') + + new CloudFormation.CfnOutput(this, 'deviceUiBucketName', { + value: deviceUIHosting.bucket.bucketName, + exportName: `${this.stackName}:deviceUi`, + }) + + new CloudFormation.CfnOutput(this, 'cloudFrontDistributionIdDeviceUi', { + value: deviceUIHosting.distribution.ref, + exportName: `${this.stackName}:cloudFrontDistributionIdDeviceUi`, }) - new CloudFormation.CfnOutput(this, 'websiteDomainName', { - value: distribution.attrDomainName, - exportName: `${this.stackName}:websiteDomainName`, + new CloudFormation.CfnOutput(this, 'deviceUiDomainName', { + value: deviceUIHosting.distribution.attrDomainName, + exportName: `${this.stackName}:deviceUiDomainName`, }) const iotJitpRole = new IAM.Role(this, 'iotJitpRole', { @@ -300,3 +261,19 @@ export class BifravstStack extends CloudFormation.Stack { }) } } + +export type StackOutputs = { + mqttEndpoint: string + userPoolId: string + identityPoolId: string + userPoolClientId: string + webAppBucketName: string + cloudFrontDistributionIdWebApp: string + webAppDomainName: string + deviceUiBucketName: string + cloudFrontDistributionIdDeviceUi: string + deviceUiDomainName: string + jitpRoleArn: string + thingPolicyArn: string + thingGroupName: string +} diff --git a/cdk/stacks/ContinuousDeployment.ts b/cdk/stacks/ContinuousDeployment.ts index d619d6d0..cad47bde 100644 --- a/cdk/stacks/ContinuousDeployment.ts +++ b/cdk/stacks/ContinuousDeployment.ts @@ -4,6 +4,7 @@ import * as CodeBuild from '@aws-cdk/aws-codebuild' import * as CodePipeline from '@aws-cdk/aws-codepipeline' import * as SSM from '@aws-cdk/aws-ssm' import * as S3 from '@aws-cdk/aws-s3' +import { WebAppCD } from '../resources/WebAppCD' /** * This is the CloudFormation stack sets up the continuous deployment of the project. @@ -14,10 +15,17 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { id: string, properties: { bifravstStackId: string - owner: string - repo: string - branch: string - app: { + bifravstAWS: { + owner: string + repo: string + branch: string + } + webApp: { + owner: string + repo: string + branch: string + } + deviceUI: { owner: string repo: string branch: string @@ -26,7 +34,7 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { ) { super(parent, id) - const { owner, repo, branch, app, bifravstStackId } = properties + const { bifravstAWS, deviceUI, webApp, bifravstStackId } = properties const codeBuildRole = new IAM.Role(this, 'CodeBuildRole', { assumedBy: new IAM.ServicePrincipal('codebuild.amazonaws.com'), @@ -73,39 +81,13 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { }) project.node.addDependency(codeBuildRole) - const appProject = new CodeBuild.CfnProject(this, 'AppCodeBuildProject', { - name: `${id}-app`, - description: - 'This project sets up the continuous deployment of the Bifravst app', - source: { - type: 'CODEPIPELINE', - buildSpec: 'continuous-deployment-app.yml', - }, - serviceRole: codeBuildRole.roleArn, - artifacts: { - type: 'CODEPIPELINE', - }, - environment: { - type: 'LINUX_CONTAINER', - computeType: 'BUILD_GENERAL1_LARGE', - image: 'aws/codebuild/standard:2.0', - environmentVariables: [ - { - name: 'STACK_ID', - value: bifravstStackId, - }, - ], - }, - }) - project.node.addDependency(codeBuildRole) - const codePipelineRole = new IAM.Role(this, 'CodePipelineRole', { assumedBy: new IAM.ServicePrincipal('codepipeline.amazonaws.com'), inlinePolicies: { controlCodeBuild: new IAM.PolicyDocument({ statements: [ new IAM.PolicyStatement({ - resources: [project.attrArn, appProject.attrArn], + resources: [project.attrArn], actions: ['codebuild:*'], }), ], @@ -155,9 +137,9 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { }, ], configuration: { - Branch: branch, - Owner: owner, - Repo: repo, + Branch: bifravstAWS.branch, + Owner: bifravstAWS.owner, + Repo: bifravstAWS.repo, OAuthToken: githubToken.stringValue, }, }, @@ -198,7 +180,7 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { filters: [ { jsonPath: '$.ref', - matchEquals: `refs/heads/${branch}`, + matchEquals: `refs/heads/${bifravstAWS.branch}`, }, ], authentication: 'GITHUB_HMAC', @@ -208,108 +190,22 @@ export class ContinuousDeploymentStack extends CloudFormation.Stack { registerWithThirdParty: false, }) - // App CD - - const appPipeline = new CodePipeline.CfnPipeline(this, 'AppCodePipeline', { - roleArn: codePipelineRole.roleArn, - artifactStore: { - type: 'S3', - location: bucket.bucketName, - }, - name: `${id}-app`, - stages: [ - { - name: 'Source', - actions: [ - { - name: 'BifravstAWSSourceCode', - actionTypeId: { - category: 'Source', - owner: 'ThirdParty', - version: '1', - provider: 'GitHub', - }, - outputArtifacts: [ - { - name: 'BifravstAWS', - }, - ], - configuration: { - Branch: branch, - Owner: owner, - Repo: repo, - OAuthToken: githubToken.stringValue, - }, - }, - { - name: 'BifravstAppSourceCode', - actionTypeId: { - category: 'Source', - owner: 'ThirdParty', - version: '1', - provider: 'GitHub', - }, - outputArtifacts: [ - { - name: 'BifravstApp', - }, - ], - configuration: { - Branch: app.branch, - Owner: app.owner, - Repo: app.repo, - OAuthToken: githubToken.stringValue, - }, - }, - ], - }, - { - name: 'Deploy', - actions: [ - { - name: 'DeployBifravstApp', - inputArtifacts: [ - { name: 'BifravstAWS' }, - { name: 'BifravstApp' }, - ], - actionTypeId: { - category: 'Build', - owner: 'AWS', - version: '1', - provider: 'CodeBuild', - }, - configuration: { - ProjectName: appProject.name, - PrimarySource: 'BifravstAWS', - }, - outputArtifacts: [ - { - name: 'BuildId', - }, - ], - }, - ], - }, - ], + // Sets up the continuous deployment for the web app + new WebAppCD(this, 'webAppCD', { + bifravstAWS, + webApp, + githubToken, + bifravstStackId, + buildSpec: 'continuous-deployment-web-app.yml', }) - appPipeline.node.addDependency(codePipelineRole) - new CodePipeline.CfnWebhook(this, 'appWebhook', { - name: `${id}-app-InvokePipelineFromGitHubChange`, - targetPipeline: `${id}-app`, - targetPipelineVersion: 1, - targetAction: 'Source', - filters: [ - { - jsonPath: '$.ref', - matchEquals: `refs/heads/${app.branch}`, - }, - ], - authentication: 'GITHUB_HMAC', - authenticationConfiguration: { - secretToken: githubToken.stringValue, - }, - registerWithThirdParty: false, + // Sets up the continuous deployment for the device UI + new WebAppCD(this, 'deviceUICD', { + bifravstAWS, + webApp: deviceUI, + githubToken, + bifravstStackId, + buildSpec: 'continuous-deployment-device-ui-app.yml', }) } } diff --git a/continuous-deployment-app.yml b/continuous-deployment-app.yml deleted file mode 100644 index 50f8d1d2..00000000 --- a/continuous-deployment-app.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 0.2 -phases: - install: - runtime-versions: - nodejs: 10 - commands: - - npm install -g npm@ - - npm ci --no-audit - - npx tsc - build: - commands: - - node dist/scripts/print-react-app-configuration.js > $CODEBUILD_SRC_DIR_BifravstApp/.env.production.local - - cat $CODEBUILD_SRC_DIR_BifravstApp/.env.production.local - - export $(cat $CODEBUILD_SRC_DIR_BifravstApp/.env.production.local | xargs) - - cd $CODEBUILD_SRC_DIR_BifravstApp/; npm ci --no-audit; npm run build; - - aws s3 cp $CODEBUILD_SRC_DIR_BifravstApp/build s3://$REACT_APP_WEBSITE_BUCKET_NAME --recursive --metadata-directive REPLACE --cache-control 'public,max-age=600' --expires '' - - aws cloudfront create-invalidation --distribution-id $REACT_APP_CLOUDFRONT_DISTRIBUTION_ID --paths /,/index.html diff --git a/continuous-deployment-device-ui-app.yml b/continuous-deployment-device-ui-app.yml new file mode 100644 index 00000000..1077ba57 --- /dev/null +++ b/continuous-deployment-device-ui-app.yml @@ -0,0 +1,17 @@ +version: 0.2 +phases: + install: + runtime-versions: + nodejs: 10 + commands: + - npm install -g npm@ + - npm ci --no-audit + - npx tsc + build: + commands: + - node dist/scripts/print-react-app-configuration.js > $CODEBUILD_SRC_DIR_WebApp/.env.production.local + - cat $CODEBUILD_SRC_DIR_WebApp/.env.production.local + - export $(cat $CODEBUILD_SRC_DIR_WebApp/.env.production.local | xargs) + - cd $CODEBUILD_SRC_DIR_WebApp/; npm ci --no-audit; npm run build; + - aws s3 cp $CODEBUILD_SRC_DIR_WebApp/build s3://$REACT_APP_DEVICE_UI_BUCKET_NAME --recursive --metadata-directive REPLACE --cache-control 'public,max-age=600' --expires '' + - aws cloudfront create-invalidation --distribution-id $REACT_APP_CLOUDFRONT_DISTRIBUTION_ID_DEVICE_UI --paths /,/index.html diff --git a/continuous-deployment-web-app.yml b/continuous-deployment-web-app.yml new file mode 100644 index 00000000..669f233a --- /dev/null +++ b/continuous-deployment-web-app.yml @@ -0,0 +1,17 @@ +version: 0.2 +phases: + install: + runtime-versions: + nodejs: 10 + commands: + - npm install -g npm@ + - npm ci --no-audit + - npx tsc + build: + commands: + - node dist/scripts/print-react-app-configuration.js > $CODEBUILD_SRC_DIR_WebApp/.env.production.local + - cat $CODEBUILD_SRC_DIR_WebApp/.env.production.local + - export $(cat $CODEBUILD_SRC_DIR_WebApp/.env.production.local | xargs) + - cd $CODEBUILD_SRC_DIR_WebApp/; npm ci --no-audit; npm run build; + - aws s3 cp $CODEBUILD_SRC_DIR_WebApp/build s3://$REACT_APP_WEB_APP_BUCKET_NAME --recursive --metadata-directive REPLACE --cache-control 'public,max-age=600' --expires '' + - aws cloudfront create-invalidation --distribution-id $REACT_APP_CLOUDFRONT_DISTRIBUTION_ID_WEB_APP --paths /,/index.html diff --git a/data/device-ui.html b/data/device-ui.html deleted file mode 100644 index 1385876c..00000000 --- a/data/device-ui.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Device UI - - - - - - - -
- - - diff --git a/package.json b/package.json index 5dd456da..f0340123 100644 --- a/package.json +++ b/package.json @@ -91,9 +91,13 @@ }, "deploy": { "branch": "saga", - "app": { + "webApp": { "repository": "https://github.com/bifravst/app.git", "branch": "saga" + }, + "deviceUI": { + "repository": "https://github.com/bifravst/device-ui.git", + "branch": "saga" } }, "jest": { diff --git a/scripts/cloudformation/stackOutput.ts b/scripts/cloudformation/stackOutput.ts new file mode 100644 index 00000000..4e566932 --- /dev/null +++ b/scripts/cloudformation/stackOutput.ts @@ -0,0 +1,18 @@ +import { CloudFormation } from 'aws-sdk' +import { toObject } from './toObject' + +/** + * Prints the stack outputs as create-react-app environment variables + */ +export const stackOutput = async (args: { + stackId: string + region?: string +}): Promise => { + const { region, stackId } = args + const cf = new CloudFormation({ region }) + const { Stacks } = await cf.describeStacks({ StackName: stackId }).promise() + if (!Stacks || !Stacks.length || !Stacks[0].Outputs) { + throw new Error(`Stack ${stackId} not found.`) + } + return toObject(Stacks[0].Outputs) as T +} diff --git a/scripts/cloudformation/stackOutputToCRAEnvironment.ts b/scripts/cloudformation/stackOutputToCRAEnvironment.ts index 5dc47cb8..3abe7595 100644 --- a/scripts/cloudformation/stackOutputToCRAEnvironment.ts +++ b/scripts/cloudformation/stackOutputToCRAEnvironment.ts @@ -1,6 +1,5 @@ -import { CloudFormation } from 'aws-sdk' import { objectToEnv } from './objectToEnv' -import { toObject } from './toObject' +import { stackOutput } from './stackOutput' /** * Prints the stack outputs as create-react-app environment variables @@ -8,15 +7,8 @@ import { toObject } from './toObject' export const stackOutputToCRAEnvironment = async (args: { stackId: string region?: string -}) => { - const { region, stackId } = args - const cf = new CloudFormation({ region }) - const { Stacks } = await cf.describeStacks({ StackName: stackId }).promise() - if (!Stacks || !Stacks.length || !Stacks[0].Outputs) { - throw new Error(`Stack ${stackId} not found.`) - } - return objectToEnv({ - ...toObject(Stacks[0].Outputs), - region, +}) => + objectToEnv({ + ...(await stackOutput(args)), + region: args.region, }) -} diff --git a/scripts/connect.ts b/scripts/connect.ts index d5d74a77..836e0237 100644 --- a/scripts/connect.ts +++ b/scripts/connect.ts @@ -4,6 +4,11 @@ import { thingShadow } from 'aws-iot-device-sdk' import { deviceFileLocations } from './jitp/deviceFileLocations' import chalk from 'chalk' import { uiServer } from './device/ui-server' +import { stackOutput } from './cloudformation/stackOutput' +import { StackOutputs } from '../cdk/stacks/Bifravst' + +const stackId = process.env.STACK_ID || 'bifravst' +const region = process.env.AWS_DEFAULT_REGION /** * Connect to the AWS IoT broker using a generated device certificate @@ -13,22 +18,31 @@ const main = async (args: { deviceId: string }) => { if (!clientId || !clientId.length) { throw new Error('Must provide a device id!') } - console.log(chalk.magenta('Fetching IoT endpoint address ...')) - const { endpointAddress } = await new Iot({ - region: process.env.AWS_DEFAULT_REGION, - }) - .describeEndpoint({ endpointType: 'iot:Data-ATS' }) - .promise() - if (!endpointAddress) { - throw new Error(`Failed to resolved AWS IoT endpoint`) - } - - console.log( - chalk.blue(`IoT broker hostname: ${chalk.yellow(endpointAddress)}`), - ) console.log(chalk.blue(`Device ID: ${chalk.yellow(clientId)}`)) + const [endpointAddress, deviceUiUrl] = await Promise.all([ + (async () => { + console.log(chalk.magenta('Fetching IoT endpoint address ...')) + const { endpointAddress } = await new Iot({ + region, + }) + .describeEndpoint({ endpointType: 'iot:Data-ATS' }) + .promise() + if (!endpointAddress) { + throw new Error(`Failed to resolved AWS IoT endpoint`) + } + console.log( + chalk.blue(`IoT broker hostname: ${chalk.yellow(endpointAddress)}`), + ) + return endpointAddress + })(), + stackOutput({ + region, + stackId, + }).then(({ deviceUiDomainName }) => `https://${deviceUiDomainName}`), + ]) + const certsDir = path.resolve(process.cwd(), 'certificates') const deviceFiles = deviceFileLocations(certsDir, clientId) @@ -49,7 +63,6 @@ const main = async (args: { deviceId: string }) => { clientId, host: endpointAddress, region: endpointAddress.split('.')[2], - debug: true, }) connection.on('connect', async () => { @@ -58,6 +71,7 @@ const main = async (args: { deviceId: string }) => { connection.register(clientId, {}, async () => { await uiServer({ + deviceUiUrl, deviceId: clientId, onUpdate: update => { console.log({ clientId, state: { state: { reported: update } } }) diff --git a/scripts/device/portForDevice.ts b/scripts/device/portForDevice.ts index 01e891db..2b579c5b 100644 --- a/scripts/device/portForDevice.ts +++ b/scripts/device/portForDevice.ts @@ -7,5 +7,5 @@ export const portForDevice = ({ deviceId }: { deviceId: string }): number => { hash = (hash << 5) - hash + deviceId.charCodeAt(i) hash |= 0 // Convert to 32bit integer } - return 1024 + Math.round((hash / Math.pow(2, 31)) * (65535 - 1024)) + return 1024 + Math.round((Math.abs(hash) / Math.pow(2, 31)) * (65535 - 1024)) } diff --git a/scripts/device/ui-server.ts b/scripts/device/ui-server.ts index 3008eb73..5f98b14f 100644 --- a/scripts/device/ui-server.ts +++ b/scripts/device/ui-server.ts @@ -1,29 +1,33 @@ -import * as path from 'path' import * as http from 'http' -import { promises as fs } from 'fs' import chalk from 'chalk' import { portForDevice } from './portForDevice' export const uiServer = async (args: { deviceId: string + deviceUiUrl: string onUpdate: (update: object) => void }) => { const port = portForDevice({ deviceId: args.deviceId }) - const uiPage = await fs.readFile( - path.resolve(process.cwd(), 'data', 'device-ui.html'), - 'utf-8', - ) const requestHandler: http.RequestListener = async (request, response) => { + if (request.method === 'OPTIONS') { + response.writeHead(200, { + 'Access-Control-Allow-Methods': 'GET,OPTIONS,POST', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Origin': '*', + }) + response.end() + return + } let body = '' switch (request.url) { - case '/': - case '/index.html': + case '/id': response.writeHead(200, { - 'Content-Length': Buffer.byteLength(uiPage), - 'Content-Type': 'text/html', + 'Content-Length': args.deviceId.length, + 'Content-Type': 'text/plain', + 'Access-Control-Allow-Origin': '*', }) - response.end(uiPage) + response.end(args.deviceId) break case '/update': request.on('data', chunk => { @@ -31,9 +35,12 @@ export const uiServer = async (args: { }) request.on('end', () => { try { + console.log(body) const update = JSON.parse(body) args.onUpdate(update) - response.statusCode = 202 + response.writeHead(202, { + 'Access-Control-Allow-Origin': '*', + }) response.end() } catch (err) { console.log(err) @@ -41,11 +48,15 @@ export const uiServer = async (args: { response.writeHead(400, { 'Content-Length': Buffer.byteLength(errData), 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', }) response.end(errData) } }) break + case '/subscribe': + // FIXME: Add websockets + break default: response.statusCode = 404 response.end() @@ -56,8 +67,11 @@ export const uiServer = async (args: { server.listen(port, () => { console.log( + chalk.cyan(`To control this device open your browser on:`), chalk.green( - `To control this device open your browser on http://localhost:${port}`, + `${args.deviceUiUrl}?endpoint=${encodeURIComponent( + `http://localhost:${port}`, + )}`, ), ) })