From 1f6ec47919da2900fc022f3507900bcc810f1ded Mon Sep 17 00:00:00 2001 From: emileten Date: Tue, 22 Aug 2023 16:58:34 +0200 Subject: [PATCH 1/3] feat: add STAC browser option --- README.md | 3 ++ lib/index.ts | 3 +- lib/stac-browser/index.ts | 71 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 lib/stac-browser/index.ts diff --git a/README.md b/README.md index dd2609f..875742e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ A STAC API implementation using [stac-fastapi](https://github.com/stac-utils/sta ### [pgSTAC Titiler API](https://developmentseed.org/eoapi-cdk/#titilerpgstacapilambda-) A complete dynamic tiling API using [titiler-pgstac](https://github.com/stac-utils/titiler-pgstac) to create dynamic mosaics of assets based on [STAC Search queries](https://github.com/radiantearth/stac-api-spec/tree/master/item-search). Packaged as a complete runtime for deployment with API Gateway and Lambda and fully integrated with the pgSTAC Database construct. +### [STAC browser](https://developmentseed.org/eoapi-cdk/#stacbrowser-) +A CDK construct to host a static [Radiant Earth STAC browser](https://github.com/radiantearth/stac-browser) on S3. + ### [STAC Ingestor](https://developmentseed.org/eoapi-cdk/#stacingestor-) An API for large scale STAC data ingestion and validation into a pgSTAC instance. diff --git a/lib/index.ts b/lib/index.ts index 1dac133..6cfceb8 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,4 +3,5 @@ export * from "./bootstrapper"; export * from "./database"; export * from "./ingestor-api"; export * from "./stac-api"; -export * from "./titiler-pgstac-api"; \ No newline at end of file +export * from "./titiler-pgstac-api"; +export * from "./stac-browser"; \ No newline at end of file diff --git a/lib/stac-browser/index.ts b/lib/stac-browser/index.ts new file mode 100644 index 0000000..af66b26 --- /dev/null +++ b/lib/stac-browser/index.ts @@ -0,0 +1,71 @@ +import { Stack, aws_s3 as s3, aws_s3_deployment as s3_deployment} from "aws-cdk-lib"; +import { RemovalPolicy, CfnOutput } from "aws-cdk-lib"; +import { PolicyStatement, ServicePrincipal, Effect } from "aws-cdk-lib/aws-iam"; + +import { Construct } from "constructs"; + + +export class StacBrowser extends Construct { + + public bucket: s3.Bucket; + public bucketDeployment: s3_deployment.BucketDeployment; + + constructor(scope: Construct, id: string, props: StacBrowserProps) { + super(scope, id); + + this.bucket = new s3.Bucket(this, 'Bucket', { + accessControl: s3.BucketAccessControl.PRIVATE, + removalPolicy: RemovalPolicy.DESTROY, + }) + + this.bucket.addToResourcePolicy(new PolicyStatement({ + sid: 'AllowCloudFrontServicePrincipal', + effect: Effect.ALLOW, + actions: ['s3:GetObject'], + principals: [new ServicePrincipal('cloudfront.amazonaws.com')], + resources: [this.bucket.arnForObjects('*')], + conditions: { + 'StringEquals': { + 'aws:SourceArn': props.cloudFrontDistributionArn + } + } + })); + + + this.bucketDeployment = new s3_deployment.BucketDeployment(this, 'BucketDeployment', { + destinationBucket: this.bucket, + sources: [s3_deployment.Source.asset(props.stacBrowserDistPath)] + }); + + new CfnOutput(this, "bucket-name", { + exportName: `${Stack.of(this).stackName}-bucket-name`, + value: this.bucket.bucketName, + }); + + } +} + +export interface StacBrowserProps { + + /** + * Location of the directory in the local filesystem that contains the STAC browser compiled code. + */ + readonly stacBrowserDistPath: string; + + + /** + * The ARN of the cloudfront distribution that will be added to the bucket policy with read access. + * + * @default - No cloudfront distribution ARN. The bucket policy will not be modified. + */ + readonly cloudFrontDistributionArn?: string; + + /** + * The name of the index document (e.g. "index.html") for the website. Enables static website + * hosting for this bucket. + * + * @default - No index document. + */ + readonly websiteIndexDocument?: string; + +} From f7599468ac6b86d47b21866dc1e8a88ef913dfe4 Mon Sep 17 00:00:00 2001 From: emileten Date: Wed, 23 Aug 2023 13:01:27 +0200 Subject: [PATCH 2/3] automate the build process within the construct, parameterize stac catalog url and radiant earth repo tag --- lib/stac-browser/index.ts | 50 +++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/stac-browser/index.ts b/lib/stac-browser/index.ts index af66b26..d7d20d9 100644 --- a/lib/stac-browser/index.ts +++ b/lib/stac-browser/index.ts @@ -3,7 +3,8 @@ import { RemovalPolicy, CfnOutput } from "aws-cdk-lib"; import { PolicyStatement, ServicePrincipal, Effect } from "aws-cdk-lib/aws-iam"; import { Construct } from "constructs"; - +import { execSync } from "child_process"; +import * as fs from 'fs'; export class StacBrowser extends Construct { @@ -13,6 +14,8 @@ export class StacBrowser extends Construct { constructor(scope: Construct, id: string, props: StacBrowserProps) { super(scope, id); + const buildPath = this.buildApp(props.stacCatalogUrl, props.githubRepoTag); + this.bucket = new s3.Bucket(this, 'Bucket', { accessControl: s3.BucketAccessControl.PRIVATE, removalPolicy: RemovalPolicy.DESTROY, @@ -34,7 +37,7 @@ export class StacBrowser extends Construct { this.bucketDeployment = new s3_deployment.BucketDeployment(this, 'BucketDeployment', { destinationBucket: this.bucket, - sources: [s3_deployment.Source.asset(props.stacBrowserDistPath)] + sources: [s3_deployment.Source.asset(buildPath)] }); new CfnOutput(this, "bucket-name", { @@ -43,14 +46,53 @@ export class StacBrowser extends Construct { }); } + + private buildApp(stacCatalogUrl: string, githubRepoTag: string): string { + + // Define where to clone and build + const cloneDirectory = './stac-browser'; + const githubRepoUrl = 'https://github.com/radiantearth/stac-browser.git'; + + // if `cloneDirectory` exists, delete it + if (fs.existsSync(cloneDirectory)) { + console.log(`${cloneDirectory} already exists, deleting...`) + execSync(`rm -rf ${cloneDirectory}`); + } + + // Clone the repo + console.log(`Cloning ${githubRepoUrl} into ${cloneDirectory}`) + execSync(`git clone ${githubRepoUrl} ${cloneDirectory}`); + + // Check out the desired version + console.log(`Checking out version ${githubRepoTag}`) + execSync(`git checkout tags/${githubRepoTag}`, { cwd: cloneDirectory }); + + // Install the dependencies and build the application + console.log(`Installing dependencies`) + execSync('npm install', { cwd: cloneDirectory }); + + // Build the app with catalogUrl + console.log(`Building app with catalogUrl=${stacCatalogUrl} into ${cloneDirectory}`) + execSync(`npm run build -- --catalogUrl=${stacCatalogUrl}`, { cwd: cloneDirectory }); + + return './stac-browser/dist' + + } + + } export interface StacBrowserProps { /** - * Location of the directory in the local filesystem that contains the STAC browser compiled code. + * STAC catalog URL */ - readonly stacBrowserDistPath: string; + readonly stacCatalogUrl: string; + + /** + * Tag of the radiant earth stac-browser repo to use to build the app. + */ + readonly githubRepoTag: string; /** From c3f0f727d0b49578d38bf5e349061448d5f1908c Mon Sep 17 00:00:00 2001 From: emileten Date: Tue, 29 Aug 2023 17:04:35 +0900 Subject: [PATCH 3/3] option to use existing bucket, option to choose clone directory, avoid deleting anything and raise an error if using an existing directory does not work --- lib/stac-browser/index.ts | 100 ++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/lib/stac-browser/index.ts b/lib/stac-browser/index.ts index d7d20d9..8bda784 100644 --- a/lib/stac-browser/index.ts +++ b/lib/stac-browser/index.ts @@ -6,35 +6,46 @@ import { Construct } from "constructs"; import { execSync } from "child_process"; import * as fs from 'fs'; +const DEFAULT_CLONE_DIRECTORY = './stac-browser'; + export class StacBrowser extends Construct { - public bucket: s3.Bucket; + public bucket: s3.IBucket; public bucketDeployment: s3_deployment.BucketDeployment; constructor(scope: Construct, id: string, props: StacBrowserProps) { super(scope, id); - const buildPath = this.buildApp(props.stacCatalogUrl, props.githubRepoTag); + const buildPath = this.buildApp(props.stacCatalogUrl, props.githubRepoTag, props.cloneDirectory || DEFAULT_CLONE_DIRECTORY); - this.bucket = new s3.Bucket(this, 'Bucket', { - accessControl: s3.BucketAccessControl.PRIVATE, - removalPolicy: RemovalPolicy.DESTROY, + // import a bucket from props.bucketArn if defined, otherwise create a new bucket + if (props.bucketArn) { + this.bucket = s3.Bucket.fromBucketArn(this, 'Bucket', props.bucketArn); + } else { + this.bucket = new s3.Bucket(this, 'Bucket', { + accessControl: s3.BucketAccessControl.PRIVATE, + removalPolicy: RemovalPolicy.DESTROY, + websiteIndexDocument: props.websiteIndexDocument }) - - this.bucket.addToResourcePolicy(new PolicyStatement({ - sid: 'AllowCloudFrontServicePrincipal', - effect: Effect.ALLOW, - actions: ['s3:GetObject'], - principals: [new ServicePrincipal('cloudfront.amazonaws.com')], - resources: [this.bucket.arnForObjects('*')], - conditions: { - 'StringEquals': { - 'aws:SourceArn': props.cloudFrontDistributionArn + } + + // if props.cloudFrontDistributionArn is defined and props.bucketArn is not defined, add a bucket policy to allow read access from the cloudfront distribution + if (props.cloudFrontDistributionArn && !props.bucketArn) { + this.bucket.addToResourcePolicy(new PolicyStatement({ + sid: 'AllowCloudFrontServicePrincipal', + effect: Effect.ALLOW, + actions: ['s3:GetObject'], + principals: [new ServicePrincipal('cloudfront.amazonaws.com')], + resources: [this.bucket.arnForObjects('*')], + conditions: { + 'StringEquals': { + 'aws:SourceArn': props.cloudFrontDistributionArn + } } - } - })); + })); + } - + // add the compiled code to the bucket as a bucket deployment this.bucketDeployment = new s3_deployment.BucketDeployment(this, 'BucketDeployment', { destinationBucket: this.bucket, sources: [s3_deployment.Source.asset(buildPath)] @@ -47,25 +58,35 @@ export class StacBrowser extends Construct { } - private buildApp(stacCatalogUrl: string, githubRepoTag: string): string { + private buildApp(stacCatalogUrl: string, githubRepoTag: string, cloneDirectory: string): string { // Define where to clone and build - const cloneDirectory = './stac-browser'; const githubRepoUrl = 'https://github.com/radiantearth/stac-browser.git'; - // if `cloneDirectory` exists, delete it - if (fs.existsSync(cloneDirectory)) { - console.log(`${cloneDirectory} already exists, deleting...`) - execSync(`rm -rf ${cloneDirectory}`); + + // Maybe the repo already exists in cloneDirectory. Try checking out the desired version and if it fails, delete and reclone. + try { + console.log(`Checking if a valid cloned repo exists with version ${githubRepoTag}...`) + execSync(`git checkout tags/${githubRepoTag}`, { cwd: cloneDirectory }); } + catch (error) { + + // if directory exists, raise an error + if (fs.existsSync(cloneDirectory)) { + throw new Error(`Directory ${cloneDirectory} already exists and is not a valid clone of ${githubRepoUrl}. Please delete this directory or specify a different cloneDirectory.`); + } - // Clone the repo - console.log(`Cloning ${githubRepoUrl} into ${cloneDirectory}`) - execSync(`git clone ${githubRepoUrl} ${cloneDirectory}`); + // else, we clone and check out the version. - // Check out the desired version - console.log(`Checking out version ${githubRepoTag}`) - execSync(`git checkout tags/${githubRepoTag}`, { cwd: cloneDirectory }); + // Clone the repo + console.log(`Cloning ${githubRepoUrl} into ${cloneDirectory}...`) + execSync(`git clone ${githubRepoUrl} ${cloneDirectory}`); + + // Check out the desired version + console.log(`Checking out version ${githubRepoTag}...`) + execSync(`git checkout tags/${githubRepoTag}`, { cwd: cloneDirectory }); + + } // Install the dependencies and build the application console.log(`Installing dependencies`) @@ -84,6 +105,15 @@ export class StacBrowser extends Construct { export interface StacBrowserProps { + /** + * Bucket ARN. If specified, the identity used to deploy the stack must have the appropriate permissions to create a deployment for this bucket. + * In addition, if specified, `cloudFrontDistributionArn` is ignored since the policy of an imported resource can't be modified. + * + * @default - No bucket ARN. A new bucket will be created. + */ + + readonly bucketArn?: string; + /** * STAC catalog URL */ @@ -97,7 +127,8 @@ export interface StacBrowserProps { /** * The ARN of the cloudfront distribution that will be added to the bucket policy with read access. - * + * If `bucketArn` is specified, this parameter is ignored since the policy of an imported bucket can't be modified. + * * @default - No cloudfront distribution ARN. The bucket policy will not be modified. */ readonly cloudFrontDistributionArn?: string; @@ -110,4 +141,11 @@ export interface StacBrowserProps { */ readonly websiteIndexDocument?: string; + /** + * Location in the filesystem where to compile the browser code. + * + * @default - DEFAULT_CLONE_DIRECTORY + */ + readonly cloneDirectory?: string; + }