@ocel/pulumi 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +59 -0
- package/dist/custom.d.ts +7 -0
- package/dist/custom.js +15 -0
- package/dist/grants.d.ts +18 -0
- package/dist/grants.js +22 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +18 -0
- package/dist/postgres.d.ts +21 -0
- package/dist/postgres.js +37 -0
- package/dist/resource.d.ts +109 -0
- package/dist/resource.js +114 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @ocel/pulumi
|
|
2
|
+
|
|
3
|
+
Publish resources your Pulumi program provisions as ocel bindings, so an ocel app can
|
|
4
|
+
reach them by name.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @ocel/pulumi
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`@pulumi/pulumi` and `ocel` are peer dependencies. `ocel` is resolved from the ocel
|
|
13
|
+
project — the directory holding `ocel.json` — not from the Pulumi program.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
`ocel.json` beside `Pulumi.yaml` in the same package is the supported layout.
|
|
18
|
+
Declare the binding in the Pulumi program:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import * as aws from "@pulumi/aws";
|
|
22
|
+
import { Config } from "@pulumi/pulumi";
|
|
23
|
+
import { bind } from "@ocel/pulumi";
|
|
24
|
+
|
|
25
|
+
const password = new Config().requireSecret("dbPassword");
|
|
26
|
+
|
|
27
|
+
const orders = new aws.rds.Instance("orders", {
|
|
28
|
+
engine: "postgres",
|
|
29
|
+
password,
|
|
30
|
+
/* … */
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
bind.postgres("orders", {
|
|
34
|
+
host: orders.address,
|
|
35
|
+
port: orders.port,
|
|
36
|
+
database: orders.dbName,
|
|
37
|
+
username: orders.username,
|
|
38
|
+
password,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Name it in `ocel.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"$schema": "https://ocel.dev/schema/0.0.0/ocel.schema.json",
|
|
47
|
+
"slug": "shop",
|
|
48
|
+
"provider": { "name": "aws" },
|
|
49
|
+
"bindings": ["orders"],
|
|
50
|
+
"apps": [{ "name": "api", "path": "." }]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then read it in the app:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { postgres } from "ocel/postgres";
|
|
58
|
+
|
|
59
|
+
export const orders = postgres("orders");
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`pulumi up` publishes the binding; `ocel deploy` hands it to the app.
|
|
63
|
+
|
|
64
|
+
## `bind.postgres(name, resource, opts?)`
|
|
65
|
+
|
|
66
|
+
`name` is the name the app binds to. `resource` is the postgres fields, each of them
|
|
67
|
+
an input this update resolves before the record is published, so a resource's outputs
|
|
68
|
+
are handed over as they are:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
bind.postgres("orders", {
|
|
72
|
+
host,
|
|
73
|
+
port,
|
|
74
|
+
database,
|
|
75
|
+
username,
|
|
76
|
+
password,
|
|
77
|
+
grants: [{ actions: ["rds-db:connect"], resources: [dbUserArn] }],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`opts` says where the binding lands:
|
|
82
|
+
|
|
83
|
+
| Option | Default | Meaning |
|
|
84
|
+
| ------------- | --------------------------- | ------------------------------------------------------------------------------------------ |
|
|
85
|
+
| `class` | `"production"` | The ocel class the binding is published to. |
|
|
86
|
+
| `environment` | none | One preview environment; `class: "preview"` only. Left off, the binding serves every preview. |
|
|
87
|
+
| `project` | the program's directory | The directory holding `ocel.json`. |
|
|
88
|
+
| `parent` | none | The Pulumi resource this binding hangs under. |
|
|
89
|
+
|
|
90
|
+
One call is one resource. Remove the call and the published binding goes with it. A name
|
|
91
|
+
belongs to whoever published it — the URN of the resource the call creates — so two
|
|
92
|
+
stacks publishing `orders` into one project is refused rather than silently handing
|
|
93
|
+
every app bound to that name another database.
|
|
94
|
+
|
|
95
|
+
## `bind.custom(name, { properties }, opts?)`
|
|
96
|
+
|
|
97
|
+
Publishes values ocel neither types nor delivers, for a transform to read:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
bind.custom("network", {
|
|
101
|
+
properties: {
|
|
102
|
+
subnetIds: vpc.privateSubnetIds,
|
|
103
|
+
securityGroupIds: [securityGroup.id],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A transform module in the ocel project reads them by name:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
export default defineTransform(({ bindings }) => ({
|
|
112
|
+
function: {
|
|
113
|
+
vpc: {
|
|
114
|
+
subnetIds: bindings.network.subnetIds,
|
|
115
|
+
securityGroupIds: bindings.network.securityGroupIds,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The properties are inserted verbatim — string, number, boolean, list or object — and
|
|
122
|
+
the surface being filled is what rejects a value of the wrong shape. `opts` is the same
|
|
123
|
+
as for `bind.postgres`.
|
|
124
|
+
|
|
125
|
+
No app reads a custom binding, so it takes no `grants`: nothing would attach them. Naming
|
|
126
|
+
one in `bindings` is refused for the same reason.
|
|
127
|
+
|
|
128
|
+
## Types
|
|
129
|
+
|
|
130
|
+
There is one function per ocel binding type an app resolves. A resource ocel cannot type is
|
|
131
|
+
not bindable by name — publish what a transform needs from it with `bind.custom` instead.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const source = "pulumi";
|
|
2
|
+
export interface Target {
|
|
3
|
+
project: string;
|
|
4
|
+
class: "production" | "preview";
|
|
5
|
+
environment?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runBindings(args: string[], target: Target, input?: string): void;
|
|
8
|
+
export declare function checkTarget(target: Target): void;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import nodeModule from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const source = "pulumi";
|
|
5
|
+
export function runBindings(args, target, input) {
|
|
6
|
+
checkTarget(target);
|
|
7
|
+
const [runtime, entry] = ocelCommand(target.project);
|
|
8
|
+
const result = childProcess.spawnSync(runtime, [entry, "bindings", ...args, ...flagsFor(target)], {
|
|
9
|
+
cwd: target.project,
|
|
10
|
+
input,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
});
|
|
13
|
+
if (result.error) {
|
|
14
|
+
throw result.error;
|
|
15
|
+
}
|
|
16
|
+
if (result.status !== 0) {
|
|
17
|
+
throw new Error(refusal(result.stderr ?? "", result.status));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function checkTarget(target) {
|
|
21
|
+
if (!target.project) {
|
|
22
|
+
throw new Error("an ocel project is required: it is the directory holding ocel.json, whose apps consume this binding, and it is never read from a Pulumi stack or project name");
|
|
23
|
+
}
|
|
24
|
+
if (target.class !== "production" && target.class !== "preview") {
|
|
25
|
+
throw new Error(`class ${JSON.stringify(target.class ?? null)} is neither "production" nor "preview": a binding is published to an ocel class, never to a Pulumi stack name`);
|
|
26
|
+
}
|
|
27
|
+
if (target.environment === classWideMarker) {
|
|
28
|
+
throw new Error(`${classWideMarker} is reserved: leave the environment off to publish to the whole class, which serves every preview including the ephemeral ones`);
|
|
29
|
+
}
|
|
30
|
+
if (target.environment && target.class !== "preview") {
|
|
31
|
+
throw new Error(`environment ${target.environment} is named alongside class ${target.class}: a binding is published to a class and, in preview, to one preview environment`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function flagsFor(target) {
|
|
35
|
+
const flags = target.class === "preview" ? ["--preview"] : [];
|
|
36
|
+
if (target.environment) {
|
|
37
|
+
flags.push("--environment", target.environment);
|
|
38
|
+
}
|
|
39
|
+
return flags;
|
|
40
|
+
}
|
|
41
|
+
const classWideMarker = "*";
|
|
42
|
+
function refusal(stderr, status) {
|
|
43
|
+
const said = stderr.trim();
|
|
44
|
+
if (!said) {
|
|
45
|
+
return `ocel bindings exited ${status ?? "on a signal"} without saying why`;
|
|
46
|
+
}
|
|
47
|
+
return said;
|
|
48
|
+
}
|
|
49
|
+
function ocelCommand(project) {
|
|
50
|
+
const require = nodeModule.createRequire(path.join(project, "ocel.json"));
|
|
51
|
+
let manifest;
|
|
52
|
+
try {
|
|
53
|
+
manifest = require.resolve("ocel/package.json");
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
throw new Error(`@ocel/pulumi runs the ocel CLI in ${project}, and ocel is not installed there. Add ocel to that project's dependencies.`, { cause });
|
|
57
|
+
}
|
|
58
|
+
return [process.execPath, path.join(path.dirname(manifest), "bin", "run.js")];
|
|
59
|
+
}
|
package/dist/custom.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One `common.bindings.v1.Binding` holding a custom record's open properties, ready for protobuf JSON. */
|
|
2
|
+
export interface CustomBinding {
|
|
3
|
+
name: string;
|
|
4
|
+
custom: Record<string, unknown>;
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function customBinding(name: string, properties: Record<string, unknown>): CustomBinding;
|
package/dist/custom.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { source } from "./cli.js";
|
|
2
|
+
export function customBinding(name, properties) {
|
|
3
|
+
if (!name) {
|
|
4
|
+
throw new Error("a binding is published under no name; the name is what a transform reads it by");
|
|
5
|
+
}
|
|
6
|
+
if (Object.keys(properties).length === 0) {
|
|
7
|
+
throw new Error(`custom binding ${name} carries no properties; a custom binding is the values a transform reads out of it, so an empty one has nothing to read`);
|
|
8
|
+
}
|
|
9
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
10
|
+
if (value === undefined) {
|
|
11
|
+
throw new Error(`custom binding ${name} carries ${key} as undefined; a property a transform names must have a value the deploy can insert`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { name, custom: properties, source };
|
|
15
|
+
}
|
package/dist/grants.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Input } from "@pulumi/pulumi";
|
|
2
|
+
/** A provider-native permission an app receives along with the binding's properties. */
|
|
3
|
+
export interface Grant {
|
|
4
|
+
actions: string[];
|
|
5
|
+
resources: string[];
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A grant as it is written in a Pulumi program, where the resource a permission
|
|
10
|
+
* names is an output of the resource being bound and is resolved before the
|
|
11
|
+
* record is published.
|
|
12
|
+
*/
|
|
13
|
+
export interface GrantInput {
|
|
14
|
+
actions: string[];
|
|
15
|
+
resources: Input<string>[];
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function scoped(name: string, grants: Grant[] | undefined): Grant[] | undefined;
|
package/dist/grants.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function scoped(name, grants) {
|
|
2
|
+
if (!grants || grants.length === 0) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
for (const grant of grants) {
|
|
6
|
+
if (grant.actions.length === 0 || grant.actions.some(unscopedAction)) {
|
|
7
|
+
throw new Error(`binding ${name} carries a grant over ${grant.actions.join(", ") || "no action"}: an action naming a whole service reaches past the resource the binding names`);
|
|
8
|
+
}
|
|
9
|
+
if (grant.resources.length === 0 || grant.resources.includes(wildcard)) {
|
|
10
|
+
throw new Error(`binding ${name} carries a grant over ${grant.resources.join(", ") || "no resource"}: an app receives permissions for the resource it binds and nothing else`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return grants;
|
|
14
|
+
}
|
|
15
|
+
const wildcard = "*";
|
|
16
|
+
function unscopedAction(action) {
|
|
17
|
+
const separator = action.indexOf(":");
|
|
18
|
+
if (separator < 0) {
|
|
19
|
+
return action === wildcard;
|
|
20
|
+
}
|
|
21
|
+
return action.slice(0, separator) === wildcard || action.slice(separator + 1) === wildcard;
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { custom, postgres } from "./resource.js";
|
|
2
|
+
export type { CustomBinding } from "./custom.js";
|
|
3
|
+
export type { Grant, GrantInput } from "./grants.js";
|
|
4
|
+
export type { PostgresBinding, PostgresProperties } from "./postgres.js";
|
|
5
|
+
export type { BindOptions, CustomProperties, DescribedPostgresResource, } from "./resource.js";
|
|
6
|
+
/**
|
|
7
|
+
* The ocel bindings this Pulumi program publishes, one call per binding.
|
|
8
|
+
*
|
|
9
|
+
* A binding is one resource an app reaches, published under a name of this
|
|
10
|
+
* program's choosing — `bind.postgres("pulumi-pg-orders", …)` here, and the app's
|
|
11
|
+
* config binds a declared resource to it with
|
|
12
|
+
* `"bindings": { "postgres": { "orders": "pulumi-pg-orders" } }`. Each
|
|
13
|
+
* call is one Pulumi resource, so removing the call removes the published
|
|
14
|
+
* record, and there is one function per ocel binding type an app resolves: a
|
|
15
|
+
* resource ocel cannot type is not bindable by name, and nothing here refuses
|
|
16
|
+
* at update what it could refuse by having no function to call.
|
|
17
|
+
*
|
|
18
|
+
* `custom` is the exception an app never sees: it publishes values ocel neither
|
|
19
|
+
* types nor delivers, read only by a transform module through
|
|
20
|
+
* `bindings.<name>.<property>`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const bind: {
|
|
23
|
+
postgres: typeof postgres;
|
|
24
|
+
custom: typeof custom;
|
|
25
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { custom, postgres } from "./resource.js";
|
|
2
|
+
/**
|
|
3
|
+
* The ocel bindings this Pulumi program publishes, one call per binding.
|
|
4
|
+
*
|
|
5
|
+
* A binding is one resource an app reaches, published under a name of this
|
|
6
|
+
* program's choosing — `bind.postgres("pulumi-pg-orders", …)` here, and the app's
|
|
7
|
+
* config binds a declared resource to it with
|
|
8
|
+
* `"bindings": { "postgres": { "orders": "pulumi-pg-orders" } }`. Each
|
|
9
|
+
* call is one Pulumi resource, so removing the call removes the published
|
|
10
|
+
* record, and there is one function per ocel binding type an app resolves: a
|
|
11
|
+
* resource ocel cannot type is not bindable by name, and nothing here refuses
|
|
12
|
+
* at update what it could refuse by having no function to call.
|
|
13
|
+
*
|
|
14
|
+
* `custom` is the exception an app never sees: it publishes values ocel neither
|
|
15
|
+
* types nor delivers, read only by a transform module through
|
|
16
|
+
* `bindings.<name>.<property>`.
|
|
17
|
+
*/
|
|
18
|
+
export const bind = { postgres, custom };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Grant } from "./grants.js";
|
|
2
|
+
/** The typed properties a postgres binding carries, as `common.bindings.v1.PostgresProperties`. */
|
|
3
|
+
export interface PostgresProperties {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
database: string;
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}
|
|
10
|
+
/** One `common.bindings.v1.Binding` holding postgres properties, ready for protobuf JSON. */
|
|
11
|
+
export interface PostgresBinding {
|
|
12
|
+
name: string;
|
|
13
|
+
postgres: PostgresProperties;
|
|
14
|
+
grants?: Grant[];
|
|
15
|
+
source: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DescribedPostgres {
|
|
18
|
+
properties: Record<string, unknown>;
|
|
19
|
+
grants?: Grant[];
|
|
20
|
+
}
|
|
21
|
+
export declare function postgresBinding(name: string, described: DescribedPostgres): PostgresBinding;
|
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { source } from "./cli.js";
|
|
2
|
+
import { scoped } from "./grants.js";
|
|
3
|
+
export function postgresBinding(name, described) {
|
|
4
|
+
if (!name) {
|
|
5
|
+
throw new Error("a binding is published under no name; the name is what a consuming app binds to");
|
|
6
|
+
}
|
|
7
|
+
const binding = {
|
|
8
|
+
name,
|
|
9
|
+
postgres: propertiesFor(name, described.properties),
|
|
10
|
+
source,
|
|
11
|
+
};
|
|
12
|
+
const grants = scoped(name, described.grants);
|
|
13
|
+
if (grants) {
|
|
14
|
+
binding.grants = grants;
|
|
15
|
+
}
|
|
16
|
+
return binding;
|
|
17
|
+
}
|
|
18
|
+
const textFields = ["host", "database", "username", "password"];
|
|
19
|
+
function propertiesFor(name, properties) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const field of textFields) {
|
|
22
|
+
const value = properties[field];
|
|
23
|
+
if (typeof value !== "string" || value === "") {
|
|
24
|
+
throw new Error(`postgres binding ${name} carries no ${field}; a postgres binding is its host, port, database, username and password, and an app resolving it reads every one`);
|
|
25
|
+
}
|
|
26
|
+
out[field] = value;
|
|
27
|
+
}
|
|
28
|
+
out.port = portFor(name, properties.port);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function portFor(name, value) {
|
|
32
|
+
const port = typeof value === "string" ? Number(value) : value;
|
|
33
|
+
if (typeof port !== "number" || !Number.isInteger(port) || port <= 0) {
|
|
34
|
+
throw new Error(`postgres binding ${name} carries port ${JSON.stringify(value ?? null)}, and a port is a whole number an app can connect to`);
|
|
35
|
+
}
|
|
36
|
+
return port;
|
|
37
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Input, type Resource } from "@pulumi/pulumi";
|
|
2
|
+
import { type Target } from "./cli.js";
|
|
3
|
+
import type { GrantInput } from "./grants.js";
|
|
4
|
+
import { type DescribedPostgres } from "./postgres.js";
|
|
5
|
+
/**
|
|
6
|
+
* A postgres resource described from what a Pulumi resource exposes.
|
|
7
|
+
*
|
|
8
|
+
* The fields are the ones `common.bindings.v1.PostgresProperties` declares, each of them
|
|
9
|
+
* an `Input`, so a resource's outputs are handed over as they are and resolved
|
|
10
|
+
* before the record is published. The grants are explicit: nothing is inferred
|
|
11
|
+
* about what an app may do with the resource.
|
|
12
|
+
*/
|
|
13
|
+
export interface DescribedPostgresResource {
|
|
14
|
+
host: Input<string>;
|
|
15
|
+
port: Input<number>;
|
|
16
|
+
database: Input<string>;
|
|
17
|
+
username: Input<string>;
|
|
18
|
+
password: Input<string>;
|
|
19
|
+
grants?: GrantInput[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A record only transforms read: the properties written out by hand, under a
|
|
23
|
+
* name a transform names.
|
|
24
|
+
*
|
|
25
|
+
* Ocel neither types nor interprets what a custom binding carries — it hands the
|
|
26
|
+
* values to a transform that fills a surface field with them, so nothing is
|
|
27
|
+
* delivered to an app and no grants are accepted.
|
|
28
|
+
*/
|
|
29
|
+
export type CustomProperties = Record<string, Input<unknown>>;
|
|
30
|
+
/**
|
|
31
|
+
* Where a binding lands: an ocel class, one preview environment, the project
|
|
32
|
+
* holding both, and the Pulumi resource this one hangs under.
|
|
33
|
+
*/
|
|
34
|
+
export interface BindOptions {
|
|
35
|
+
class?: "production" | "preview";
|
|
36
|
+
environment?: string;
|
|
37
|
+
project?: string;
|
|
38
|
+
parent?: Resource;
|
|
39
|
+
}
|
|
40
|
+
interface BindingInputs extends Target {
|
|
41
|
+
name: string;
|
|
42
|
+
owner: string;
|
|
43
|
+
}
|
|
44
|
+
interface PostgresInputs extends BindingInputs, DescribedPostgres {
|
|
45
|
+
}
|
|
46
|
+
interface CustomInputs extends BindingInputs {
|
|
47
|
+
properties: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
interface BindingState extends Target {
|
|
50
|
+
name: string;
|
|
51
|
+
owner: string;
|
|
52
|
+
digest: string;
|
|
53
|
+
}
|
|
54
|
+
export declare const postgresProvider: {
|
|
55
|
+
create(inputs: PostgresInputs): Promise<{
|
|
56
|
+
id: string;
|
|
57
|
+
outs: BindingState;
|
|
58
|
+
}>;
|
|
59
|
+
diff(_id: string, olds: BindingState, news: PostgresInputs): Promise<{
|
|
60
|
+
changes: boolean;
|
|
61
|
+
replaces: string[];
|
|
62
|
+
deleteBeforeReplace: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
update(_id: string, _olds: BindingState, news: PostgresInputs): Promise<{
|
|
65
|
+
outs: BindingState;
|
|
66
|
+
}>;
|
|
67
|
+
delete(_id: string, props: BindingState): Promise<void>;
|
|
68
|
+
};
|
|
69
|
+
export declare const customProvider: {
|
|
70
|
+
create(inputs: CustomInputs): Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
outs: BindingState;
|
|
73
|
+
}>;
|
|
74
|
+
diff(_id: string, olds: BindingState, news: CustomInputs): Promise<{
|
|
75
|
+
changes: boolean;
|
|
76
|
+
replaces: string[];
|
|
77
|
+
deleteBeforeReplace: boolean;
|
|
78
|
+
}>;
|
|
79
|
+
update(_id: string, _olds: BindingState, news: CustomInputs): Promise<{
|
|
80
|
+
outs: BindingState;
|
|
81
|
+
}>;
|
|
82
|
+
delete(_id: string, props: BindingState): Promise<void>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Publishes one Pulumi-defined resource as one ocel binding, as a side effect of
|
|
86
|
+
* this update.
|
|
87
|
+
*
|
|
88
|
+
* The name is this program's own, published for an app's config to bind a
|
|
89
|
+
* declared resource to — `postgres("pulumi-pg-orders", …)` here, and
|
|
90
|
+
* `"bindings": { "postgres": { "orders": "pulumi-pg-orders" } }` in `ocel.json`
|
|
91
|
+
* — and the resource is the postgres fields read off whatever provisioned it,
|
|
92
|
+
* each of them an output this update resolves.
|
|
93
|
+
* `class` defaults to production, `environment` names one preview environment,
|
|
94
|
+
* and `project` is the directory holding `ocel.json`, which is the
|
|
95
|
+
* directory Pulumi runs the program from unless it is given.
|
|
96
|
+
*/
|
|
97
|
+
export declare function postgres(name: string, resource: DescribedPostgresResource, opts?: BindOptions): void;
|
|
98
|
+
/**
|
|
99
|
+
* Publishes one set of values your own infrastructure holds as one ocel custom
|
|
100
|
+
* binding, as a side effect of this update.
|
|
101
|
+
*
|
|
102
|
+
* The name is the one a transform reads — `bind.custom("network", …)` here,
|
|
103
|
+
* `bindings.custom.network.subnetIds` in a transform module. `class` defaults to
|
|
104
|
+
* production, `environment` names one preview environment, and `project` is the
|
|
105
|
+
* directory holding `ocel.json`, which is the directory Pulumi runs the
|
|
106
|
+
* program from unless it is given.
|
|
107
|
+
*/
|
|
108
|
+
export declare function custom(name: string, properties: CustomProperties, opts?: BindOptions): void;
|
|
109
|
+
export {};
|
package/dist/resource.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { createUrn, dynamic } from "@pulumi/pulumi";
|
|
3
|
+
import { checkTarget, runBindings } from "./cli.js";
|
|
4
|
+
import { customBinding } from "./custom.js";
|
|
5
|
+
import { postgresBinding } from "./postgres.js";
|
|
6
|
+
function bindingProvider(recordFor, resolved) {
|
|
7
|
+
const digestOf = (inputs) => crypto
|
|
8
|
+
.createHash("sha256")
|
|
9
|
+
.update(JSON.stringify(recordFor(inputs)))
|
|
10
|
+
.digest("hex");
|
|
11
|
+
const stateOf = (inputs) => ({
|
|
12
|
+
name: inputs.name,
|
|
13
|
+
owner: inputs.owner,
|
|
14
|
+
project: inputs.project,
|
|
15
|
+
class: inputs.class,
|
|
16
|
+
environment: inputs.environment,
|
|
17
|
+
digest: digestOf(inputs),
|
|
18
|
+
});
|
|
19
|
+
const set = (inputs) => runBindings(["set", "--owner", inputs.owner], inputs, `${JSON.stringify(recordFor(inputs))}\n`);
|
|
20
|
+
return {
|
|
21
|
+
async create(inputs) {
|
|
22
|
+
set(inputs);
|
|
23
|
+
return { id: idFor(inputs), outs: stateOf(inputs) };
|
|
24
|
+
},
|
|
25
|
+
async diff(_id, olds, news) {
|
|
26
|
+
const replaces = replacesFor(olds, news);
|
|
27
|
+
return {
|
|
28
|
+
changes: replaces.length > 0 || !resolved(news) || olds.digest !== digestOf(news),
|
|
29
|
+
replaces,
|
|
30
|
+
deleteBeforeReplace: replaces.length > 0,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
async update(_id, _olds, news) {
|
|
34
|
+
set(news);
|
|
35
|
+
return { outs: stateOf(news) };
|
|
36
|
+
},
|
|
37
|
+
async delete(_id, props) {
|
|
38
|
+
runBindings(["rm", props.name], props);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const postgresProvider = bindingProvider((inputs) => postgresBinding(inputs.name, {
|
|
43
|
+
properties: inputs.properties,
|
|
44
|
+
grants: inputs.grants,
|
|
45
|
+
}), (inputs) => bindingFields.every((field) => inputs.properties[field] !== undefined));
|
|
46
|
+
export const customProvider = bindingProvider((inputs) => customBinding(inputs.name, inputs.properties), (inputs) => Object.values(inputs.properties).every((value) => value !== undefined));
|
|
47
|
+
/**
|
|
48
|
+
* Publishes one Pulumi-defined resource as one ocel binding, as a side effect of
|
|
49
|
+
* this update.
|
|
50
|
+
*
|
|
51
|
+
* The name is this program's own, published for an app's config to bind a
|
|
52
|
+
* declared resource to — `postgres("pulumi-pg-orders", …)` here, and
|
|
53
|
+
* `"bindings": { "postgres": { "orders": "pulumi-pg-orders" } }` in `ocel.json`
|
|
54
|
+
* — and the resource is the postgres fields read off whatever provisioned it,
|
|
55
|
+
* each of them an output this update resolves.
|
|
56
|
+
* `class` defaults to production, `environment` names one preview environment,
|
|
57
|
+
* and `project` is the directory holding `ocel.json`, which is the
|
|
58
|
+
* directory Pulumi runs the program from unless it is given.
|
|
59
|
+
*/
|
|
60
|
+
export function postgres(name, resource, opts) {
|
|
61
|
+
declare(postgresProvider, name, opts, describe(resource));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Publishes one set of values your own infrastructure holds as one ocel custom
|
|
65
|
+
* binding, as a side effect of this update.
|
|
66
|
+
*
|
|
67
|
+
* The name is the one a transform reads — `bind.custom("network", …)` here,
|
|
68
|
+
* `bindings.custom.network.subnetIds` in a transform module. `class` defaults to
|
|
69
|
+
* production, `environment` names one preview environment, and `project` is the
|
|
70
|
+
* directory holding `ocel.json`, which is the directory Pulumi runs the
|
|
71
|
+
* program from unless it is given.
|
|
72
|
+
*/
|
|
73
|
+
export function custom(name, properties, opts) {
|
|
74
|
+
declare(customProvider, name, opts, { properties });
|
|
75
|
+
}
|
|
76
|
+
const dynamicType = "pulumi-nodejs:dynamic:Resource";
|
|
77
|
+
function declare(provider, name, opts, described) {
|
|
78
|
+
const target = {
|
|
79
|
+
project: opts?.project ?? process.cwd(),
|
|
80
|
+
class: opts?.class ?? "production",
|
|
81
|
+
environment: opts?.environment,
|
|
82
|
+
};
|
|
83
|
+
checkTarget(target);
|
|
84
|
+
const logical = `ocel-binding-${name}`;
|
|
85
|
+
new BindingResource(provider, logical, {
|
|
86
|
+
...target,
|
|
87
|
+
name,
|
|
88
|
+
owner: createUrn(logical, dynamicType, opts?.parent),
|
|
89
|
+
...described,
|
|
90
|
+
}, { parent: opts?.parent });
|
|
91
|
+
}
|
|
92
|
+
class BindingResource extends dynamic.Resource {
|
|
93
|
+
}
|
|
94
|
+
function idFor(inputs) {
|
|
95
|
+
return [inputs.class, inputs.environment, inputs.name].filter(Boolean).join("/");
|
|
96
|
+
}
|
|
97
|
+
const identity = ["name", "owner", "project", "class", "environment"];
|
|
98
|
+
function replacesFor(olds, news) {
|
|
99
|
+
return identity.filter((field) => olds[field] !== news[field]);
|
|
100
|
+
}
|
|
101
|
+
const bindingFields = ["host", "port", "database", "username", "password"];
|
|
102
|
+
function describe(resource) {
|
|
103
|
+
return {
|
|
104
|
+
properties: pick(resource),
|
|
105
|
+
grants: resource.grants,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function pick(properties) {
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const field of bindingFields) {
|
|
111
|
+
out[field] = properties[field];
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ocel/pulumi",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Publishes Pulumi-defined resources as ocel bindings, one binding per call",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@pulumi/pulumi": "^3",
|
|
20
|
+
"ocel": "^0.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@pulumi/pulumi": "~3.252.0",
|
|
24
|
+
"@types/node": "^20",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vite": "^8.1.3",
|
|
27
|
+
"vitest": "^4.1.9",
|
|
28
|
+
"ocel": "^0.0.0"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/ocelhq/ocel.git",
|
|
34
|
+
"directory": "packages/ocel-pulumi"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|