Skip to content

Commit

Permalink
Merge pull request #209 from jianzs/feat-website-resource
Browse files Browse the repository at this point in the history
feat: add new Website resource type
  • Loading branch information
jianzs authored May 12, 2024
2 parents fd1d893 + 93a0d4b commit 89b4f84
Show file tree
Hide file tree
Showing 35 changed files with 959 additions and 201 deletions.
11 changes: 11 additions & 0 deletions .changeset/cyan-tables-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@plutolang/pulumi-adapter": patch
---

feat(adapter): add `projectRoot` to Pulumi config for path resolution

This commit adds the `projectRoot` setting to the Pulumi configuration by default. This feature improves the accuracy of relative path resolution for resource creation, like a Website. With the `projectRoot` available, the infra SDK can correctly resolve paths given by the user relative to the project's base directory. For instance, creating a Website resource with a path parameter relative to the project root is now possible as demonstrated:

```typescript
const website = new Website("./public");
```
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
33 changes: 33 additions & 0 deletions .changeset/spicy-ads-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@plutolang/pyright-deducer": patch
"@plutolang/static-deducer": patch
---

feat(deducer): allow using direct captured properties as arguments in infra API

This change introduces the ability to use direct captured properties as arguments in infrastructure API calls. For instance, the code below is now considered valid:

```python
from pluto_client import Website, Router

router = Router("router")
website = Website(path="path/to/website", name="website")

website.addEnv("ROUTER", router.url())
```

In this example, `router.url()` is a direct captured property which the website utilizes to establish a connection to the backend service.

The goal is for the infrastructure API to accept both direct captured properties and variables assigned with these properties, as demonstrated here:

```python
from pluto_client import Website, Router

router = Router("router")
website = Website(path="path/to/website", name="website")

router_url = router.url()
website.addEnv("ROUTER", router_url)
```

Currently, the API only accepts direct captured properties as arguments. Future updates will include support for variables that store the return values of these properties.
7 changes: 7 additions & 0 deletions .changeset/three-foxes-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@plutolang/pluto-infra": patch
---

chore(sdk): upgrade @pulumi/aws to support Python 3.12 in Lambda

Upgraded `@pulumi/aws` version from 6.4.1 to 6.34.1 to ensure compatibility with the Python 3.12 runtime in AWS Lambda functions.
1 change: 1 addition & 0 deletions components/adapters/pulumi/src/pulumi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export class PulumiAdapter extends core.Adapter {
for (const key of Object.keys(envs)) process.env[key] = envs[key];

const pulumiConfig = await genPulumiConfig(this.stack);
pulumiConfig["pluto:projectRoot"] = { value: process.cwd() };
await pulumiStack.setAllConfig(pulumiConfig);
return pulumiStack;
}
Expand Down
48 changes: 44 additions & 4 deletions components/deducers/python-pyright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "pyright-internal/dist/parser/parseNodes";
import { core, arch } from "@plutolang/base";
import { genResourceId } from "@plutolang/base/utils";
import * as TextUtils from "./text-utils";
import * as TypeUtils from "./type-utils";
import * as TypeConsts from "./type-consts";
import * as ProgramUtils from "./program-utils";
Expand Down Expand Up @@ -269,6 +270,23 @@ export default class PyrightDeducer extends core.Deducer {
* extract parameters and the operation name.
*/
private buildRelationshipsFromInfraApis(infraCalls: CallNode[], sourceFile: SourceFile) {
const isFunctionArg = (node: ArgumentNode) => {
return (
TypeUtils.isLambdaNode(node.valueExpression) ||
TypeUtils.isFunctionVar(node.valueExpression, this.typeEvaluator!)
);
};

const isCapturedPropertyAccess = (node: ArgumentNode) => {
return (
node.valueExpression.nodeType === ParseNodeType.Call &&
this.sepcialNodeMap!.getNodeById(
node.valueExpression.id,
TypeConsts.IRESOURCE_CAPTURED_PROPS_FULL_NAME
)
);
};

for (let nodeIdx = 0; nodeIdx < infraCalls.length; nodeIdx++) {
const node = infraCalls[nodeIdx];

Expand All @@ -292,10 +310,7 @@ export default class PyrightDeducer extends core.Deducer {
getParameterName(node, idx, this.typeEvaluator!, /* isClassMember */ true) ??
"unknown";

if (
TypeUtils.isLambdaNode(argNode.valueExpression) ||
TypeUtils.isFunctionVar(argNode.valueExpression, this.typeEvaluator!)
) {
if (isFunctionArg(argNode)) {
// This argument is a function or lambda expression, we need to extract it to a closure
// and store it to a sperate directory.
const closureId = `${fromResource.name}_${nodeIdx}_${operation}_${idx}_${parameterName}`;
Expand All @@ -316,6 +331,31 @@ export default class PyrightDeducer extends core.Deducer {
type: "closure",
value: closure.id,
});
} else if (isCapturedPropertyAccess(argNode)) {
// This argument facilitates direct access to the captured property of a resource object.
// Since in IaC code, the variable name of the resource object is replaced with the
// resource object ID, we need to alter the accessor in this method call to the resource
// object ID.
const propertyAccessNode = argNode.valueExpression as CallNode;
const resNode = this.tracker!.getConstructNodeForApiCall(propertyAccessNode, sourceFile);
if (!resNode) {
throw new Error(
"No resource object found for the captured property access, " +
TextUtils.getTextOfNode(propertyAccessNode, sourceFile)
);
}

const toResource = this.nodeToResourceMap.get(resNode.id);
toResourceIds.push({ id: toResource!.id, type: "resource" });

const method = getMemberName(propertyAccessNode, this.typeEvaluator!);
const paramValue = `${toResource!.id}.${method}()`;
parameters.push({
index: idx,
name: parameterName,
type: "text",
value: paramValue,
});
} else {
// Otherwise, this argument should be composed of literals, and we can convert it into a
// JSON string.
Expand Down
102 changes: 68 additions & 34 deletions components/deducers/static/src/deducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import path from "path";
import assert from "assert";
import { arch, core, utils } from "@plutolang/base";
import {
ParameterInfo,
ResourceRelationshipInfo,
ResourceVariableInfo,
VisitResult,
concatVisitResult,
} from "./types";
import { IdWithType } from "@plutolang/base/arch";
import { FN_RESOURCE_TYPE_NAME } from "./constants";
import { visitVariableStatement } from "./visit-var-def";
import { visitExpression } from "./visit-expression";
Expand Down Expand Up @@ -156,12 +158,15 @@ function storeAllClosure(
parameters:
varInfo.resourceConstructInfo.parameters
?.map((param) => {
if (param.resourceName) {
// TODO: Check if this parameter is a closure, or a resource. This is used to
// generate the closure source code. If the parameter is a closure, we use the
// any type to fill.
if (param.type === "closure") {
// If the parameter is a closure, we use the any type to fill.
return "({} as any)";
}

if (param.type === "property") {
return `${param.resourceVarName}.${param.property}()`;
}

return param.expression?.getText() ?? "undefined";
})
.join(", ") ?? "",
Expand All @@ -185,6 +190,34 @@ function buildArchRef(
return resVarInfo.resourceName ?? varName;
}

function constructArchParameter(param: ParameterInfo): arch.Parameter {
const paramType = param.type === "closure" ? "closure" : "text";

let paramValue;
switch (param.type) {
case "closure":
paramValue = param.closureName;
break;
case "property": {
const resName = getResourceNameByVarName(param.resourceVarName);
const res = archResources.find((r) => r.name === resName);
assert(res !== undefined);
paramValue = `${res.id}.${param.property}()`;
break;
}
case "text":
paramValue = param.expression?.getText() ?? "undefined";
break;
}

return {
index: param.order,
name: param.name,
type: paramType,
value: paramValue,
};
}

const archClosures: arch.Closure[] = [];
const archResources: arch.Resource[] = [];
resVarInfos.forEach((varInfo) => {
Expand All @@ -200,17 +233,9 @@ function buildArchRef(
} else {
// Resource
resName = varInfo.resourceName;
assert(resName !== undefined, `The resource name is not defined.`);
assert(resName !== undefined, `The resource name ${resName} is not defined.`);

const resParams =
varInfo.resourceConstructInfo.parameters?.map((param): arch.Parameter => {
return {
index: param.order,
name: param.name,
type: param.resourceName ? "closure" : "text",
value: param.resourceName ?? param.expression?.getText() ?? "undefined",
};
}) ?? [];
const resParams = varInfo.resourceConstructInfo.parameters?.map(constructArchParameter) ?? [];

// TODO: remove this temporary solution, fetch full quilified name of the resource type from
// the user code.
Expand All @@ -222,33 +247,42 @@ function buildArchRef(
});

const archRelats: arch.Relationship[] = resRelatInfos.map((relatInfo): arch.Relationship => {
const fromRes =
const fromResource =
archResources.find((val) => val.name == getResourceNameByVarName(relatInfo.fromVarName)) ??
archClosures.find((val) => val.id == relatInfo.fromVarName);
const toRes =
archResources.find((val) => val.name == getResourceNameByVarName(relatInfo.toVarNames[0])) ??
archClosures.find((val) => val.id == relatInfo.toVarNames[0]);
assert(
fromRes !== undefined && toRes !== undefined,
`${relatInfo.fromVarName} --${relatInfo.operation}--> ${relatInfo.toVarNames[0]}`
);
assert(fromResource !== undefined);

const toResources: IdWithType[] = [];
for (const toVarName of relatInfo.toVarNames) {
const res = archResources.find((r) => r.name === getResourceNameByVarName(toVarName));
if (res) {
toResources.push({ id: res.id, type: "resource" });
continue;
}

const closure = archClosures.find((c) => c.id == toVarName);
if (closure) {
toResources.push({ id: closure.id, type: "closure" });
break;
}
}
for (const param of relatInfo.parameters) {
if (param.type === "property") {
const resName = getResourceNameByVarName(param.resourceVarName);
const res = archResources.find((r) => r.name === resName);
assert(res !== undefined, `The resource '${resName}' is not found.`);
toResources.push({ id: res.id, type: "resource" });
}
}

const fromType = fromRes instanceof arch.Closure ? "closure" : "resource";
const toType = toRes instanceof arch.Closure ? "closure" : "resource";
const fromType = fromResource instanceof arch.Closure ? "closure" : "resource";

const relatType = relatInfo.type;
const relatOp = relatInfo.operation;
const params = relatInfo.parameters.map((param): arch.Parameter => {
return {
index: param.order,
name: param.name,
type: param.resourceName ? "closure" : "text",
value: param.resourceName ?? param.expression?.getText() ?? "undefined",
};
});
const params = relatInfo.parameters.map(constructArchParameter);
return new arch.Relationship(
{ id: fromRes.id, type: fromType },
[{ id: toRes.id, type: toType }],
{ id: fromResource.id, type: fromType },
toResources,
relatType,
relatOp,
params
Expand Down
25 changes: 21 additions & 4 deletions components/deducers/static/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,28 @@ export interface ResourceConstructInfo {
locations: Location[];
}

export interface ParameterInfo {
name: string; // The parameter name in the function signature.
resourceName?: string;
expression: ts.Expression | undefined;
export type ParameterInfo = TextParameterInfo | ClosureParameterInfo | PropertyParameterInfo;

export interface BaseParameterInfo {
type: "text" | "closure" | "property";
name: string;
order: number;
expression?: ts.Expression;
}

interface TextParameterInfo extends BaseParameterInfo {
type: "text";
}

interface ClosureParameterInfo extends BaseParameterInfo {
type: "closure";
closureName: string;
}

interface PropertyParameterInfo extends BaseParameterInfo {
type: "property";
resourceVarName: string;
property: string;
}

export interface Location {
Expand Down
Loading

0 comments on commit 89b4f84

Please sign in to comment.