Skip to content

Commit

Permalink
feat(sdk): add Website resource type
Browse files Browse the repository at this point in the history
  • Loading branch information
jianzs committed May 11, 2024
1 parent fd1d893 commit e58e6d2
Show file tree
Hide file tree
Showing 20 changed files with 456 additions and 32 deletions.
6 changes: 6 additions & 0 deletions .changeset/red-geese-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@plutolang/pluto-infra": patch
"@plutolang/pluto": patch
---

feat(sdk): add Website resource type
6 changes: 4 additions & 2 deletions packages/pluto-infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@
"@pulumi/docker": "4.4.3",
"@pulumi/kubernetes": "4.3.0",
"@pulumi/pulumi": "3.88.0",
"fs-extra": "^11.1.1"
"fs-extra": "^11.1.1",
"glob": "^10.3.10",
"mime-types": "^2.1.35"
},
"devDependencies": {
"@aws-sdk/core": "^3.431.0",
"@types/aws-lambda": "^8.10.131",
"@types/express": "^4.17.20",
"@types/fs-extra": "^11.0.4",
"@types/mime-types": "^2.1.4",
"@types/node": "^20.8.4",
"@vitest/coverage-v8": "^0.34.6",
"cloudevents": "^8.0.0",
"express": "^4.18.2",
"glob": "^10.3.10",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
}
Expand Down
70 changes: 68 additions & 2 deletions packages/pluto-infra/src/aws/bucket.s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export enum S3Ops {
export class S3Bucket extends pulumi.ComponentResource implements IResourceInfra {
public readonly id: string;

public readonly bucket: aws.s3.Bucket;
public readonly bucket: aws.s3.BucketV2;

constructor(name: string, opts?: BucketOptions) {
super("pluto:bucket:aws/S3", name, opts);
this.id = genResourceId(Bucket.fqn, name);

const bucket = new aws.s3.Bucket(
const bucket = new aws.s3.BucketV2(
genAwsResourceName(this.id),
{
bucket: genAwsResourceName(this.id),
Expand All @@ -32,6 +32,72 @@ export class S3Bucket extends pulumi.ComponentResource implements IResourceInfra
this.bucket = bucket;
}

/**
* Enable static website hosting for the bucket. Returns the website endpoint.
*/
public configWebsite(indexDocument: string, errorDocument?: string) {
const config = new aws.s3.BucketWebsiteConfigurationV2(
genAwsResourceName(this.id),
{
bucket: this.bucket.bucket,
indexDocument: {
suffix: indexDocument,
},
errorDocument: errorDocument
? {
key: errorDocument,
}
: undefined,
},
{
parent: this,
}
);
return config.websiteEndpoint.apply((endpoint) => `http://${endpoint}`);
}

public setPublic() {
// AWS S3 disable public access by default.
const bucketPublicAccessBlock = new aws.s3.BucketPublicAccessBlock(
genAwsResourceName(this.id),
{
bucket: this.bucket.bucket,
blockPublicPolicy: false,
blockPublicAcls: false,
restrictPublicBuckets: false,
ignorePublicAcls: false,
},
{
parent: this,
}
);

// Allow public access to the objects in the bucket.
new aws.s3.BucketPolicy(
genAwsResourceName(this.id),
{
bucket: this.bucket.bucket,
policy: this.bucket.bucket.apply((bucketName) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: "*",
Action: ["s3:GetObject"],
Resource: [`arn:aws:s3:::${bucketName}/*`],
},
],
})
),
},
{
parent: this,
dependsOn: [bucketPublicAccessBlock],
}
);
}

public grantPermission(op: string): Permission {
const actions: string[] = [];
switch (op) {
Expand Down
4 changes: 2 additions & 2 deletions packages/pluto-infra/src/aws/function.lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ export class Lambda extends pulumi.ComponentResource implements IResourceInfra,
const lambdaAssetName = genAwsResourceName(this.id, Date.now().toString());
function upload(): pulumi.Output<string> {
const lambdaZip = new pulumi.asset.FileArchive(workdir);
const object = new aws.s3.BucketObject(lambdaAssetName, {
bucket: Lambda.lambdaAssetsBucket!.bucket,
const object = new aws.s3.BucketObjectv2(lambdaAssetName, {
bucket: Lambda.lambdaAssetsBucket!.bucket.bucket,
source: lambdaZip,
});
return object.key;
Expand Down
1 change: 1 addition & 0 deletions packages/pluto-infra/src/aws/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { CloudWatchSchedule } from "./schedule.cloudwatch";
export { AwsTester } from "./tester";
export { SageMaker } from "./sagemaker";
export { S3Bucket } from "./bucket.s3";
export { Website } from "./website.s3";
2 changes: 1 addition & 1 deletion packages/pluto-infra/src/aws/router.apigateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class ApiGatewayRouter
{
public readonly id: string;

public _url: pulumi.Output<string> = pulumi.interpolate`unkonwn`;
public _url: pulumi.Output<string>;

private apiGateway: Api;
private routes: Route[];
Expand Down
102 changes: 102 additions & 0 deletions packages/pluto-infra/src/aws/website.s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { glob } from "glob";
import * as path from "path";
import * as fs from "fs-extra";
import * as aws from "@pulumi/aws";
import * as mime from "mime-types";
import * as pulumi from "@pulumi/pulumi";
import { IResourceInfra } from "@plutolang/base";
import { genResourceId } from "@plutolang/base/utils";
import { Website as WebsiteProto, WebsiteOptions } from "@plutolang/pluto";
import { S3Bucket } from "./bucket.s3";

export class Website extends pulumi.ComponentResource implements IResourceInfra {
public readonly id: string;

private readonly envs: { [key: string]: pulumi.Output<string> | string } = {};
private readonly websiteDir: string;

private readonly websiteBucket: S3Bucket;

private readonly websiteEndpoint: pulumi.Output<string>;

// eslint-disable-next-line
public outputs?: pulumi.Output<any>;

constructor(websiteRoot: string, name?: string, options?: WebsiteOptions) {
name = name ?? "default";
super("pluto:website:aws/Website", name, options);
this.id = genResourceId(WebsiteProto.fqn, name);

const projectRoot = new pulumi.Config("pluto").require("projectRoot");
this.websiteDir = path.resolve(projectRoot, websiteRoot);
if (!fs.existsSync(this.websiteDir)) {
throw new Error(`The path ${this.websiteDir} does not exist.`);
}

this.websiteBucket = new S3Bucket(this.id);
this.websiteBucket.setPublic();
this.websiteEndpoint = this.websiteBucket.configWebsite("index.html");

this.outputs = this.websiteEndpoint;
}

public addEnv(key: string, value: pulumi.Output<string> | string) {
this.envs[key] = value;
}

public url(): string {
return this.websiteEndpoint as any;
}

public grantPermission(op: string, resource?: IResourceInfra) {
op;
resource;
throw new Error("Method should be called.");
}

public postProcess(): void {
function dumpPlutoJs(filepath: string, envs: { [key: string]: string }) {
const content = PLUTO_JS_TEMPALETE.replace("{placeholder}", JSON.stringify(envs, null, 2));
fs.writeFileSync(filepath, content);
}

function uploadFileToS3(bucket: S3Bucket, dirpath: string) {
glob.sync(`${dirpath}/**/*`).forEach((file) => {
const lambdaAssetName = file.replace(new RegExp(`${dirpath}/?`, "g"), "");
const mimeType = mime.lookup(file) || undefined;
new aws.s3.BucketObjectv2(lambdaAssetName, {
bucket: bucket.bucket.bucket,
key: lambdaAssetName,
contentType: mimeType,
source: new pulumi.asset.FileAsset(file),
});
});
}

pulumi.output(this.envs).apply((envs) => {
const filepath = path.join(this.websiteDir, "pluto.js");
// Developers may have previously constructed a `pluto.js` file to facilitate debugging
// throughout the development process. Therefore, it's essential to back up the original
// content of `pluto.js` and ensure it's restored after deployment.
const originalPlutoJs = fs.existsSync(filepath)
? fs.readFileSync(filepath, "utf8")
: undefined;

try {
dumpPlutoJs(filepath, envs);
uploadFileToS3(this.websiteBucket, this.websiteDir);
// Remove the generated `pluto.js` file after deployment.
fs.removeSync(filepath);
} finally {
// Restore original pluto.js content.
if (originalPlutoJs) {
fs.writeFileSync(filepath, originalPlutoJs);
}
}
});
}
}

const PLUTO_JS_TEMPALETE = `
window.plutoEnv = {placeholder}
`;
1 change: 1 addition & 0 deletions packages/pluto-infra/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { Schedule } from "./schedule";
export { Tester } from "./tester";
export { SageMaker } from "./sagemaker.aws";
export { Bucket } from "./bucket";
export { Website } from "./website";
50 changes: 50 additions & 0 deletions packages/pluto-infra/src/website.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ProvisionType, PlatformType, utils, IResourceInfra } from "@plutolang/base";
import { IWebsiteInfra, WebsiteOptions } from "@plutolang/pluto";
import { ImplClassMap } from "./utils";

type IWebsiteInfraImpl = IWebsiteInfra & IResourceInfra;

// Construct a type for a class constructor. The key point is that the parameters of the constructor
// must be consistent with the client class of this resource type. Use this type to ensure that
// all implementation classes have the correct and same constructor signature.
type WebsiteInfraImplClass = new (
path: string,
name?: string,
options?: WebsiteOptions
) => IWebsiteInfraImpl;

// Construct a map that contains all the implementation classes for this resource type.
// The final selection will be determined at runtime, and the class will be imported lazily.
const implClassMap = new ImplClassMap<IWebsiteInfraImpl, WebsiteInfraImplClass>(
"@plutolang/pluto.Website",
{
[ProvisionType.Pulumi]: {
[PlatformType.AWS]: async () => (await import("./aws")).Website,
},
}
);

/**
* This is a factory class that provides an interface to create instances of this resource type
* based on the target platform and provisioning engine.
*/
export abstract class Website {
/**
* Asynchronously creates an instance of the Website infrastructure class. The parameters of this function
* must be consistent with the constructor of both the client class and infrastructure class associated
* with this resource type.
*/
public static async createInstance(
path: string,
name?: string,
options?: WebsiteOptions
): Promise<IWebsiteInfraImpl> {
return implClassMap.createInstanceOrThrow(
utils.currentPlatformType(),
utils.currentEngineType(),
path,
name,
options
);
}
}
5 changes: 5 additions & 0 deletions packages/pluto-py/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pluto-client

## 0.0.10

feat(sdk): add Website resource type
3 changes: 3 additions & 0 deletions packages/pluto-py/pluto_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .router import Router, RouterOptions, HttpRequest, HttpResponse
from .bucket import Bucket, BucketOptions
from .schedule import Schedule, ScheduleOptions
from .website import Website, WebsiteOptions

__all__ = [
"Queue",
Expand All @@ -21,4 +22,6 @@
"BucketOptions",
"Schedule",
"ScheduleOptions",
"Website",
"WebsiteOptions",
]
1 change: 1 addition & 0 deletions packages/pluto-py/pluto_client/clients/shared/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .router import RouterClient
from .website import WebsiteClient
17 changes: 17 additions & 0 deletions packages/pluto-py/pluto_client/clients/shared/website.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Optional
from pluto_base.utils import gen_resource_id, get_env_val_for_property
from ...website import IWebsiteClient, Website, WebsiteOptions


class WebsiteClient(IWebsiteClient):
def __init__(
self,
path: str,
name: Optional[str] = None,
opts: Optional[WebsiteOptions] = None,
):
name = name or "default"
self.__id = gen_resource_id(Website.fqn, name)

def url(self) -> str:
return get_env_val_for_property(self.__id, "url")
Loading

0 comments on commit e58e6d2

Please sign in to comment.