/
sdk-architecture.md
131 lines (99 loc) · 7.79 KB
/
sdk-architecture.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# SDK Architecture
The Wing SDK is the standard library for the Wing language.
The core of the SDK are its APIs for creating cloud resources.
By using the SDK to specify the desired state of cloud application including resources like API gateways, queues, storage buckets, and so on, the SDK can synthesize an artifact for deploying that application to the cloud.
Today the SDK supports either synthesizing a collection of Terraform configuration, or synthesizing a simulator file that the SDK's `Simulator` API can use to simulate the app within Node.js.
## Constructs
The SDK resources are written using the Constructs Programming Model.
[constructs](https://github.com/aws/constructs) are building blocks that can be composed together to represent more complex desired state, most commonly for cloud applications.
constructs serves as the low-level foundation of several other infrastructure-as-code frameworks, such as the [AWS CDK](https://github.com/aws/aws-cdk), [cdk8s](https://github.com/cdk8s-team/cdk8s), and [cdktf](https://github.com/hashicorp/terraform-cdk).
Conceptually, constructs are ordinary classes that additionally have a unique **scope** (parent construct) and **id**.
By adding constructs as children of other constructs, they can form in-memory trees, where each construct is uniquely addressible based on the "path" of nodes from the tree root.
When a collection of constructs all implement a method like `toTerraform()`, then it is possible to traverse the construct tree and aggregate the result of calling the method on each construct in order to synthesize a result or artifact.
## Polycons
In order to model resources that are implemented differently for each cloud provider, the SDK also uses [polycons](https://github.com/winglang/polycons), a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) framework designed to work with constructs.
Using polycons, the SDK resources are structured as follows:
* Each resource has a polycon class defined in the `cloud` namespace with the API that is shared across all cloud implementations (e.g. `cloud.Bucket`).
In order to work with polycons, any shared properties and methods expected to exist on all classes must be defined on a base clase like `cloud.BucketBase`, and then we have `cloud.Bucket` extending `cloud.BucketBase`.
Each polycon also has a unique polycon type name needed for polycons to perform dependency injection on them.
* Each cloud target can implement a polycon by defining a class that extends the polycon base class (e.g. `tfaws.Bucket` extends `cloud.BucketBase`).
* Each cloud target defines a [polycon factory](https://github.com/winglang/polycons/blob/main/API.md#ipolyconfactory-) that defines the concrete mapping from polycon type names to polycon implementations.
* Each cloud target has a unique `App` construct that specifies logic for synthesizing a one or more types of constructs.
It also registers the cloud target's polycon factory to that node on the construct tree.
Through polycons, when a user writes `new cloud.Bucket()` within the scope of an AWS `App`, the constructor of `cloud.Bucket` will automatically look up the polycon factory associated with the construct tree, and call the factory's `resolve` method to produce the class instance specific to that clodu target (`new tfaws.Bucket()`), and return that back to the caller.
Each `App` class has an automatically registered polycon factory, but it's possible to pass a custom factory in `new App(...)` that builds on top of (or overrides) the original factory to support more polycons, or different resolution behavior.
## Inflights
Inflights are Wing's distributed computing primitive.
They are isolated code blocks which can be packaged and executed on compute platforms in the cloud (such as containers, CI/CD pipelines or FaaS).
When a resource wants to use an inflight in an API, it is represented in the SDK using the `Inflight` class.
An `Inflight` is modeled as an object containing:
* a snippet of code (inline string or file)
* an entrypoint (name of a function to call)
* a map of "captured" data and resources
For example, given the following Wing code:
```wing
let queue = new cloud.Queue();
let greeting = "Hello, world!";
new cloud.Function((event: str) ~> {
print(greeting);
queue.push(event);
});
```
... the responsibility of the Wing compiler is to transform it into JavaScript like this:
```ts
const queue = new sdk.cloud.Queue(this, "Queue");
const handler = new sdk.core.Inflight({
code: sdk.core.NodeJsCode.fromInline(
`async function $proc($cap, event) {
await $cap.logger.print($cap.greeting);
await $cap.queue.push(event);
}`
),
entrypoint: "$proc",
captures: {
logger: {
resource: sdk.cloud.Logger.of(this),
methods: ["print"],
},
queue: {
resource: queue,
methods: ["push"],
},
greeting: {
value: "Hello, world!",
}
},
});
new sdk.cloud.Function(this, "Function", handler);
```
The inflight's `captures` field currently serve two purposes.
The first purpose is to provide references to resources so that the CDK code can "glue" the resources together.
This can include setting up permissions for the compute resource to perform operations on the referenced resource during runtime.
The `methods` field associated with captured resources specifies what operations need to be used on the resource, so that least privilege permissions can be granted to the resource running the inflight code.
The second purpose is signal to the SDK that there is data that needs to be bundled with the user code.
For example, when a `cloud.Queue` is captured by an inflight in an AWS application, the user's inflight code needs to be bundled with an inflight "client" that can perform `push()` by calling the AWS SDK.
The next section explains how the `Inflight` class is used to produce a JavaScript bundle during the app's synthesis.
This JavaScript bundle will inject the appropriate code for `$cap`, and the code bundle will be included as a Terraform asset when the app is synthesized.
## Bundling
Currently, the SDK handles the process of bundling inflight user code with dependencies such as inflight resource clients and constants.
For simplicity, this discussion will focus on the example of a `cloud.Function` accepting an inflight handler.
The bundling process starts in the constructor of the class implementing the `cloud.Function` polycon -- for example, `tfaws.Function`.
The first step is that the function needs to obtain "clients" for all of the captured resources (modeled as `Record<string, Code>`).
To do this, it iterates over each entry in `captures`.
If the capture is a plain value or collection type, it just stringifies it.
If the capture is a resource, it inverts the control back to the captured resource by calling the resource's `_bind` method.
For example, if a `tfaws.Function` named `func` captures a `tfaws.Bucket` named `bucket`, it calls `bucket._bind(func, metadata)` where `metadata` is any extra information like the resource methods.
`_bind` will update `func`'s AWS IAM policy so that it can perform operations on the bucket, and it will return a `Code` object that contains a `BucketClient` class with methods like `get` and `put`.
After we repeat this process for all captures, we will have a map from capture names to `Code` objects.
The second step is to call esbuild.
Each `Inflight` has a `bundle` method that combines the user's code with the collection of "clients", in a template that looks something like:
```
const $cap = {
<capture1>: <capture1-client>,
<capture2>: <capture2-client>,
...
};
<user code with named entrypoint>
exports.handler = function(event) { entrypoint($cap, event) };
```
Today the SDK currently expects the user code to always have an object containing captures as its first argument, and an event as a second argument, but this may change in the future.