puls-dev 0.3.4 → 0.3.6
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 +165 -54
- package/dist/bin/install-shell.d.ts +2 -0
- package/dist/bin/install-shell.js +136 -0
- package/dist/bin/puls.js +32 -10
- package/dist/core/checker.js +74 -0
- package/dist/core/decorators.js +17 -1
- package/dist/core/resource.d.ts +35 -0
- package/dist/core/resource.js +57 -1
- package/dist/core/stack.d.ts +11 -0
- package/dist/core/stack.js +88 -1
- package/dist/index.d.ts +1 -0
- package/dist/providers/aws/api.js +3 -0
- package/dist/providers/aws/ec2.d.ts +5 -0
- package/dist/providers/aws/ec2.js +7 -0
- package/dist/providers/aws/lambda.d.ts +5 -0
- package/dist/providers/aws/lambda.js +24 -0
- package/dist/providers/aws/list.js +15 -3
- package/dist/providers/aws/rds.d.ts +9 -0
- package/dist/providers/aws/rds.js +19 -0
- package/dist/providers/do/database.d.ts +9 -0
- package/dist/providers/do/database.js +19 -0
- package/dist/providers/do/domain.js +1 -1
- package/dist/providers/do/droplet.d.ts +5 -0
- package/dist/providers/do/droplet.js +10 -0
- package/dist/providers/do/list.js +25 -2
- package/dist/providers/do/load_balancer.d.ts +5 -0
- package/dist/providers/do/load_balancer.js +7 -0
- package/dist/providers/do/vpc.d.ts +5 -0
- package/dist/providers/do/vpc.js +8 -0
- package/dist/providers/firebase/functions.d.ts +9 -0
- package/dist/providers/firebase/functions.js +28 -0
- package/dist/providers/firebase/list.js +34 -2
- package/dist/providers/gcp/api.js +6 -0
- package/dist/providers/gcp/cloudrun.d.ts +13 -0
- package/dist/providers/gcp/cloudrun.js +30 -0
- package/dist/providers/gcp/cloudsql.d.ts +9 -0
- package/dist/providers/gcp/cloudsql.js +20 -0
- package/dist/providers/gcp/list.js +12 -2
- package/dist/providers/gcp/vm.d.ts +5 -0
- package/dist/providers/gcp/vm.js +8 -0
- package/dist/providers/proxmox/list.js +8 -1
- package/dist/providers/proxmox/vm.d.ts +13 -0
- package/dist/providers/proxmox/vm.js +16 -0
- package/dist/types/diff.d.ts +17 -0
- package/dist/types/diff.js +1 -0
- package/dist/types/inventory.d.ts +65 -0
- package/package.json +2 -2
package/dist/core/resource.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import type { FieldDiff } from "../types/diff.js";
|
|
1
2
|
export declare abstract class BaseBuilder {
|
|
2
3
|
name: string;
|
|
3
4
|
protected isProtected: boolean;
|
|
4
5
|
protected localDryRun: boolean | null;
|
|
5
6
|
protected discoveryPromise: Promise<any>;
|
|
6
7
|
protected sidecars: BaseBuilder[];
|
|
8
|
+
protected _adoptedId: string | null;
|
|
7
9
|
/** @internal */
|
|
8
10
|
_deployPromise: Promise<any>;
|
|
9
11
|
/** @internal */
|
|
12
|
+
_resolveDiscovery(): Promise<any>;
|
|
13
|
+
/** @internal */
|
|
10
14
|
_destroyPromise?: Promise<any>;
|
|
11
15
|
/** @internal */
|
|
12
16
|
_dependencies: BaseBuilder[];
|
|
@@ -17,6 +21,37 @@ export declare abstract class BaseBuilder {
|
|
|
17
21
|
constructor(name: string);
|
|
18
22
|
dependsOn(resource: BaseBuilder): this;
|
|
19
23
|
protect(): this;
|
|
24
|
+
/**
|
|
25
|
+
* Adopt an existing cloud resource by its provider ID, bringing it under
|
|
26
|
+
* Puls management without recreating it. If name-based discovery finds the
|
|
27
|
+
* resource, that result wins; adoptId only kicks in when discovery returns
|
|
28
|
+
* null (i.e. the resource was created outside this stack or with a different
|
|
29
|
+
* naming convention).
|
|
30
|
+
*
|
|
31
|
+
* Outputs that depend on live API response fields (e.g. `out.host`) won't be
|
|
32
|
+
* resolved automatically — chain `.adoptOutput(key, value)` for each one you
|
|
33
|
+
* need for cross-stack wiring.
|
|
34
|
+
*/
|
|
35
|
+
adoptId(id: string): this;
|
|
36
|
+
/**
|
|
37
|
+
* Pre-resolve a named output on this builder. Use alongside `adoptId` to
|
|
38
|
+
* supply known connection details (host, port, uri, etc.) so downstream
|
|
39
|
+
* resources can reference them before this builder deploys.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* db = DO.Database("prod-db")
|
|
43
|
+
* .adoptId("abc123")
|
|
44
|
+
* .adoptOutput("host", "db.internal.example.com")
|
|
45
|
+
* .adoptOutput("uri", "postgres://...");
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Returns field-level differences between declared intent and live cloud state.
|
|
49
|
+
* Called by `Stack.diff()` for each resource that exists. Override in provider
|
|
50
|
+
* builders to surface meaningful drift fields. The default returns an empty array
|
|
51
|
+
* (no field-level diff available).
|
|
52
|
+
*/
|
|
53
|
+
getDiff(_existing: any): FieldDiff[];
|
|
54
|
+
adoptOutput(key: string, value: any): this;
|
|
20
55
|
dryRun(enabled?: boolean): this;
|
|
21
56
|
beforeDeploy(callback: () => Promise<void> | void): this;
|
|
22
57
|
afterDeploy(callback: (result: any) => Promise<void> | void): this;
|
package/dist/core/resource.js
CHANGED
|
@@ -5,9 +5,14 @@ export class BaseBuilder {
|
|
|
5
5
|
localDryRun = null;
|
|
6
6
|
discoveryPromise;
|
|
7
7
|
sidecars = [];
|
|
8
|
+
_adoptedId = null;
|
|
8
9
|
/** @internal */
|
|
9
10
|
_deployPromise;
|
|
10
11
|
/** @internal */
|
|
12
|
+
async _resolveDiscovery() {
|
|
13
|
+
return this.discoveryPromise;
|
|
14
|
+
}
|
|
15
|
+
/** @internal */
|
|
11
16
|
_destroyPromise;
|
|
12
17
|
/** @internal */
|
|
13
18
|
_dependencies = [];
|
|
@@ -26,6 +31,56 @@ export class BaseBuilder {
|
|
|
26
31
|
this.isProtected = true;
|
|
27
32
|
return this;
|
|
28
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Adopt an existing cloud resource by its provider ID, bringing it under
|
|
36
|
+
* Puls management without recreating it. If name-based discovery finds the
|
|
37
|
+
* resource, that result wins; adoptId only kicks in when discovery returns
|
|
38
|
+
* null (i.e. the resource was created outside this stack or with a different
|
|
39
|
+
* naming convention).
|
|
40
|
+
*
|
|
41
|
+
* Outputs that depend on live API response fields (e.g. `out.host`) won't be
|
|
42
|
+
* resolved automatically — chain `.adoptOutput(key, value)` for each one you
|
|
43
|
+
* need for cross-stack wiring.
|
|
44
|
+
*/
|
|
45
|
+
adoptId(id) {
|
|
46
|
+
this._adoptedId = id;
|
|
47
|
+
const original = this.discoveryPromise;
|
|
48
|
+
this.discoveryPromise = original.then((found) => {
|
|
49
|
+
if (!found) {
|
|
50
|
+
this.out?.id?.resolve?.(id);
|
|
51
|
+
return { id, status: "adopted", _adopted: true };
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
});
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pre-resolve a named output on this builder. Use alongside `adoptId` to
|
|
59
|
+
* supply known connection details (host, port, uri, etc.) so downstream
|
|
60
|
+
* resources can reference them before this builder deploys.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* db = DO.Database("prod-db")
|
|
64
|
+
* .adoptId("abc123")
|
|
65
|
+
* .adoptOutput("host", "db.internal.example.com")
|
|
66
|
+
* .adoptOutput("uri", "postgres://...");
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Returns field-level differences between declared intent and live cloud state.
|
|
70
|
+
* Called by `Stack.diff()` for each resource that exists. Override in provider
|
|
71
|
+
* builders to surface meaningful drift fields. The default returns an empty array
|
|
72
|
+
* (no field-level diff available).
|
|
73
|
+
*/
|
|
74
|
+
getDiff(_existing) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
adoptOutput(key, value) {
|
|
78
|
+
const out = this.out;
|
|
79
|
+
if (typeof out?.[key]?.resolve === "function") {
|
|
80
|
+
out[key].resolve(value);
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
29
84
|
dryRun(enabled = true) {
|
|
30
85
|
this.localDryRun = enabled;
|
|
31
86
|
return this;
|
|
@@ -123,7 +178,8 @@ export class BaseBuilder {
|
|
|
123
178
|
}
|
|
124
179
|
async destroy() {
|
|
125
180
|
const dryRun = this.isDryRunActive();
|
|
126
|
-
|
|
181
|
+
const adoptedSuffix = this._adoptedId ? ` [adopted id=${this._adoptedId}]` : "";
|
|
182
|
+
console.log(`\n🗑️ Destroying "${this.name}"${adoptedSuffix}...`);
|
|
127
183
|
console.log(` ✅ [${dryRun ? "PLAN" : "OK"}] Resource "${this.name}" marked for destruction.`);
|
|
128
184
|
await this.destroySidecars();
|
|
129
185
|
return { destroyed: this.name };
|
package/dist/core/stack.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
+
import type { StackDiff } from "../types/diff.js";
|
|
2
3
|
export declare abstract class Stack {
|
|
3
4
|
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
4
5
|
static _register(cls: Function, instance: Stack, region?: string): void;
|
|
@@ -16,6 +17,16 @@ export declare abstract class Stack {
|
|
|
16
17
|
* }
|
|
17
18
|
*/
|
|
18
19
|
static from<T extends Stack>(cls: new (...args: any[]) => T, region?: string): T;
|
|
20
|
+
/**
|
|
21
|
+
* Compares every declared resource against its live cloud state without
|
|
22
|
+
* making any API writes. Returns a structured `StackDiff` and prints a
|
|
23
|
+
* formatted report to the console.
|
|
24
|
+
*
|
|
25
|
+
* Field-level drift is surfaced for providers that implement `getDiff()`.
|
|
26
|
+
* Resources with no `getDiff()` override show only existence status
|
|
27
|
+
* (missing / in-sync / adopted).
|
|
28
|
+
*/
|
|
29
|
+
diff(): Promise<StackDiff>;
|
|
19
30
|
deploy(): Promise<Record<string, any>>;
|
|
20
31
|
destroy(): Promise<Record<string, any>>;
|
|
21
32
|
}
|
package/dist/core/stack.js
CHANGED
|
@@ -107,6 +107,45 @@ function printOutputs(stackName, outputs) {
|
|
|
107
107
|
}
|
|
108
108
|
console.log(` └${line}┘`);
|
|
109
109
|
}
|
|
110
|
+
function printDiff(diff) {
|
|
111
|
+
console.log(`\n🔍 Diff: ${diff.stackName}`);
|
|
112
|
+
const propWidth = Math.max(...diff.resources.map((r) => r.prop.length), 4);
|
|
113
|
+
const nameWidth = Math.max(...diff.resources.map((r) => r.resource.length), 8);
|
|
114
|
+
for (const r of diff.resources) {
|
|
115
|
+
const prop = r.prop.padEnd(propWidth);
|
|
116
|
+
const name = r.resource.padEnd(nameWidth);
|
|
117
|
+
if (r.status === "in-sync") {
|
|
118
|
+
console.log(` ${prop} ${name} ✅ in-sync`);
|
|
119
|
+
}
|
|
120
|
+
else if (r.status === "adopted") {
|
|
121
|
+
console.log(` ${prop} ${name} 🔗 adopted`);
|
|
122
|
+
}
|
|
123
|
+
else if (r.status === "missing") {
|
|
124
|
+
console.log(` ${prop} ${name} ❌ missing (will create)`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(` ${prop} ${name} ⚠️ drift`);
|
|
128
|
+
const fieldWidth = Math.max(...r.changes.map((c) => String(c.field).length), 8);
|
|
129
|
+
for (const c of r.changes) {
|
|
130
|
+
const field = String(c.field).padEnd(fieldWidth);
|
|
131
|
+
console.log(` └─ ${field} ${String(c.declared)} → ${c.live}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const driftCount = diff.resources.filter((r) => r.status === "drift").length;
|
|
136
|
+
const missingCount = diff.resources.filter((r) => r.status === "missing").length;
|
|
137
|
+
if (driftCount === 0 && missingCount === 0) {
|
|
138
|
+
console.log(`\n ✅ All ${diff.resources.length} resources are in sync.`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const parts = [];
|
|
142
|
+
if (driftCount > 0)
|
|
143
|
+
parts.push(`${driftCount} drifted`);
|
|
144
|
+
if (missingCount > 0)
|
|
145
|
+
parts.push(`${missingCount} missing`);
|
|
146
|
+
console.log(`\n ⚠️ ${parts.join(", ")} out of ${diff.resources.length} resources.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
110
149
|
export class Stack {
|
|
111
150
|
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
112
151
|
static _register(cls, instance, region) {
|
|
@@ -135,6 +174,54 @@ export class Stack {
|
|
|
135
174
|
throw new Error(`Stack "${cls.name}" ${region ? `for region "${region}" ` : ""}is not registered. Make sure it is decorated with @Deploy and its module is imported before referencing it.`);
|
|
136
175
|
return instance;
|
|
137
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Compares every declared resource against its live cloud state without
|
|
179
|
+
* making any API writes. Returns a structured `StackDiff` and prints a
|
|
180
|
+
* formatted report to the console.
|
|
181
|
+
*
|
|
182
|
+
* Field-level drift is surfaced for providers that implement `getDiff()`.
|
|
183
|
+
* Resources with no `getDiff()` override show only existence status
|
|
184
|
+
* (missing / in-sync / adopted).
|
|
185
|
+
*/
|
|
186
|
+
async diff() {
|
|
187
|
+
const props = Object.getOwnPropertyNames(this);
|
|
188
|
+
const entries = [];
|
|
189
|
+
for (const prop of props) {
|
|
190
|
+
const val = this[prop];
|
|
191
|
+
if (val instanceof BaseBuilder) {
|
|
192
|
+
entries.push({ prop, resource: val });
|
|
193
|
+
}
|
|
194
|
+
else if (Array.isArray(val)) {
|
|
195
|
+
for (const item of val) {
|
|
196
|
+
if (item instanceof BaseBuilder) {
|
|
197
|
+
entries.push({ prop, resource: item });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const resources = [];
|
|
203
|
+
for (const { prop, resource } of entries) {
|
|
204
|
+
const existing = await resource._resolveDiscovery();
|
|
205
|
+
let status;
|
|
206
|
+
let changes = resource.getDiff(existing ?? {});
|
|
207
|
+
if (!existing) {
|
|
208
|
+
status = "missing";
|
|
209
|
+
changes = [];
|
|
210
|
+
}
|
|
211
|
+
else if (existing._adopted === true) {
|
|
212
|
+
status = "adopted";
|
|
213
|
+
changes = [];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
status = changes.length > 0 ? "drift" : "in-sync";
|
|
217
|
+
}
|
|
218
|
+
resources.push({ prop, resource: resource.name, status, changes });
|
|
219
|
+
}
|
|
220
|
+
const hasDrift = resources.some((r) => r.status === "drift" || r.status === "missing");
|
|
221
|
+
const result = { stackName: this.constructor.name, resources, hasDrift };
|
|
222
|
+
printDiff(result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
138
225
|
async deploy() {
|
|
139
226
|
const controller = new AbortController();
|
|
140
227
|
const hosts = [];
|
|
@@ -193,7 +280,7 @@ export class Stack {
|
|
|
193
280
|
resource._deployPromise = (async () => {
|
|
194
281
|
try {
|
|
195
282
|
// Yield so the map() loop finishes assigning all _deployPromise values before
|
|
196
|
-
// any task checks its dependencies
|
|
283
|
+
// any task checks its dependencies - a dependency that appears later in the list
|
|
197
284
|
// would otherwise have an undefined _deployPromise and be silently skipped.
|
|
198
285
|
await Promise.resolve();
|
|
199
286
|
if (controller.signal.aborted) {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export * from "./core/resource.js";
|
|
|
5
5
|
export { Secret, clearResolvedSecrets } from "./core/secret.js";
|
|
6
6
|
export { Output } from "./core/output.js";
|
|
7
7
|
export * as INVENTORY_TYPES from "./types/inventory.js";
|
|
8
|
+
export type { FieldDiff, ResourceDiff, StackDiff, ResourceStatus } from "./types/diff.js";
|
|
8
9
|
export { SLACK, DISCORD } from "./core/hooks.js";
|
|
@@ -60,6 +60,9 @@ function createAwsOfflineMock(command) {
|
|
|
60
60
|
get(target, prop) {
|
|
61
61
|
if (prop === "then")
|
|
62
62
|
return undefined;
|
|
63
|
+
// Pagination tokens must be undefined so discovery loops exit cleanly
|
|
64
|
+
if (prop === "NextToken" || prop === "NextMarker" || prop === "Marker" || prop === "ContinuationToken")
|
|
65
|
+
return undefined;
|
|
63
66
|
if (prop === "CertificateArn")
|
|
64
67
|
return "arn:aws:acm:us-east-1:123456789012:certificate/mock-cert-uuid";
|
|
65
68
|
if (prop === "HostedZoneId")
|
|
@@ -29,6 +29,11 @@ export declare class EC2VMBuilder extends BaseBuilder {
|
|
|
29
29
|
sshPrivateKey(path: string): this;
|
|
30
30
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
31
31
|
forceConfigCheck(): this;
|
|
32
|
+
getDiff(existing: any): {
|
|
33
|
+
field: string;
|
|
34
|
+
declared: string;
|
|
35
|
+
live: any;
|
|
36
|
+
}[];
|
|
32
37
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
33
38
|
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
34
39
|
private discoverVM;
|
|
@@ -67,6 +67,13 @@ export class EC2VMBuilder extends BaseBuilder {
|
|
|
67
67
|
this._forceConfigCheck = true;
|
|
68
68
|
return this;
|
|
69
69
|
}
|
|
70
|
+
getDiff(existing) {
|
|
71
|
+
const diffs = [];
|
|
72
|
+
if (existing.InstanceType !== this._instanceType) {
|
|
73
|
+
diffs.push({ field: "instanceType", declared: this._instanceType, live: existing.InstanceType });
|
|
74
|
+
}
|
|
75
|
+
return diffs;
|
|
76
|
+
}
|
|
70
77
|
async checkPort(ip, port) {
|
|
71
78
|
return checkPort(ip, port);
|
|
72
79
|
}
|
|
@@ -20,6 +20,11 @@ export declare class LambdaBuilder extends BaseBuilder {
|
|
|
20
20
|
timeout(seconds: number): this;
|
|
21
21
|
role(arnOrBuilder: string | IAMRoleBuilder): this;
|
|
22
22
|
env(vars: Record<string, string | SecretsBuilder>): this;
|
|
23
|
+
getDiff(existing: any): {
|
|
24
|
+
field: string;
|
|
25
|
+
declared: string;
|
|
26
|
+
live: any;
|
|
27
|
+
}[];
|
|
23
28
|
private ensureRole;
|
|
24
29
|
private buildZip;
|
|
25
30
|
deploy(): Promise<{
|
|
@@ -78,6 +78,30 @@ export class LambdaBuilder extends BaseBuilder {
|
|
|
78
78
|
this._env = { ...this._env, ...vars };
|
|
79
79
|
return this;
|
|
80
80
|
}
|
|
81
|
+
getDiff(existing) {
|
|
82
|
+
const diffs = [];
|
|
83
|
+
if (existing.Runtime !== this._runtime) {
|
|
84
|
+
diffs.push({ field: "runtime", declared: this._runtime, live: existing.Runtime });
|
|
85
|
+
}
|
|
86
|
+
if (existing.Handler !== this._handler) {
|
|
87
|
+
diffs.push({ field: "handler", declared: this._handler, live: existing.Handler });
|
|
88
|
+
}
|
|
89
|
+
if (existing.MemorySize !== this._memory) {
|
|
90
|
+
diffs.push({ field: "memory", declared: `${this._memory} MB`, live: `${existing.MemorySize} MB` });
|
|
91
|
+
}
|
|
92
|
+
if (existing.Timeout !== this._timeout) {
|
|
93
|
+
diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${existing.Timeout}s` });
|
|
94
|
+
}
|
|
95
|
+
const liveEnv = existing.Environment?.Variables ?? {};
|
|
96
|
+
const declaredKeys = Object.keys(this._env);
|
|
97
|
+
const liveKeys = Object.keys(liveEnv);
|
|
98
|
+
const allKeys = new Set([...declaredKeys, ...liveKeys]);
|
|
99
|
+
const envDrift = [...allKeys].filter((k) => String(this._env[k]) !== String(liveEnv[k]));
|
|
100
|
+
if (envDrift.length > 0) {
|
|
101
|
+
diffs.push({ field: "env", declared: `${declaredKeys.length} vars`, live: `${liveKeys.length} vars (${envDrift.length} changed)` });
|
|
102
|
+
}
|
|
103
|
+
return diffs;
|
|
104
|
+
}
|
|
81
105
|
async ensureRole() {
|
|
82
106
|
if (this._roleBuilder) {
|
|
83
107
|
return await this._roleBuilder.out.arn.get();
|
|
@@ -3,16 +3,18 @@ import { ListBucketsCommand } from '@aws-sdk/client-s3';
|
|
|
3
3
|
import { ListFunctionsCommand } from '@aws-sdk/client-lambda';
|
|
4
4
|
import { DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
5
5
|
import { ListHostedZonesCommand } from '@aws-sdk/client-route-53';
|
|
6
|
-
import {
|
|
6
|
+
import { DescribeInstancesCommand } from '@aws-sdk/client-ec2';
|
|
7
|
+
import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client, getEC2Client } from './api.js';
|
|
7
8
|
import { Config } from '../../core/config.js';
|
|
8
9
|
export async function listAwsResources() {
|
|
9
10
|
const region = Config.get().providers.aws.region;
|
|
10
|
-
const [cfResult, s3Result, lambdaResult, rdsResult, r53Result] = await Promise.all([
|
|
11
|
+
const [cfResult, s3Result, lambdaResult, rdsResult, r53Result, ec2Result] = await Promise.all([
|
|
11
12
|
getCFClient().send(new ListDistributionsCommand({})),
|
|
12
13
|
getS3Client().send(new ListBucketsCommand({})),
|
|
13
14
|
getLambdaClient().send(new ListFunctionsCommand({ MaxItems: 50 })),
|
|
14
15
|
getRDSClient().send(new DescribeDBInstancesCommand({})),
|
|
15
16
|
getR53Client().send(new ListHostedZonesCommand({})),
|
|
17
|
+
getEC2Client().send(new DescribeInstancesCommand({ MaxResults: 200 })),
|
|
16
18
|
]);
|
|
17
19
|
const distributions = (cfResult.DistributionList?.Items ?? []).map((d) => ({
|
|
18
20
|
id: d.Id,
|
|
@@ -40,5 +42,15 @@ export async function listAwsResources() {
|
|
|
40
42
|
id: z.Id.replace('/hostedzone/', ''),
|
|
41
43
|
recordCount: z.ResourceRecordSetCount ?? 0,
|
|
42
44
|
}));
|
|
43
|
-
|
|
45
|
+
const ec2Instances = (ec2Result.Reservations ?? [])
|
|
46
|
+
.flatMap((r) => r.Instances ?? [])
|
|
47
|
+
.filter((i) => i.State?.Name !== 'terminated')
|
|
48
|
+
.map((i) => ({
|
|
49
|
+
id: i.InstanceId,
|
|
50
|
+
name: i.Tags?.find((t) => t.Key === 'Name')?.Value ?? i.InstanceId,
|
|
51
|
+
type: i.InstanceType ?? 'unknown',
|
|
52
|
+
state: i.State?.Name ?? 'unknown',
|
|
53
|
+
publicIp: i.PublicIpAddress,
|
|
54
|
+
}));
|
|
55
|
+
return { region, distributions, buckets, lambdas, rdsInstances, hostedZones, ec2Instances };
|
|
44
56
|
}
|
|
@@ -24,6 +24,15 @@ export declare class RDSBuilder extends BaseBuilder {
|
|
|
24
24
|
subnets(ids: string[]): this;
|
|
25
25
|
securityGroups(ids: string[]): this;
|
|
26
26
|
publicAccess(enabled?: boolean): this;
|
|
27
|
+
getDiff(existing: any): ({
|
|
28
|
+
field: string;
|
|
29
|
+
declared: string;
|
|
30
|
+
live: any;
|
|
31
|
+
} | {
|
|
32
|
+
field: string;
|
|
33
|
+
declared: boolean;
|
|
34
|
+
live: any;
|
|
35
|
+
})[];
|
|
27
36
|
database(name: string): this;
|
|
28
37
|
credentials(username: string, password: string): this;
|
|
29
38
|
private discoverInstance;
|
|
@@ -54,6 +54,25 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
54
54
|
this._publicAccess = enabled;
|
|
55
55
|
return this;
|
|
56
56
|
}
|
|
57
|
+
getDiff(existing) {
|
|
58
|
+
const diffs = [];
|
|
59
|
+
if (existing.Engine !== this._engine) {
|
|
60
|
+
diffs.push({ field: "engine", declared: this._engine, live: existing.Engine });
|
|
61
|
+
}
|
|
62
|
+
if (existing.EngineVersion !== this._engineVersion) {
|
|
63
|
+
diffs.push({ field: "engineVersion", declared: this._engineVersion, live: existing.EngineVersion });
|
|
64
|
+
}
|
|
65
|
+
if (existing.DBInstanceClass !== this._instanceClass) {
|
|
66
|
+
diffs.push({ field: "instanceClass", declared: this._instanceClass, live: existing.DBInstanceClass });
|
|
67
|
+
}
|
|
68
|
+
if (existing.AllocatedStorage !== this._storage) {
|
|
69
|
+
diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${existing.AllocatedStorage} GB` });
|
|
70
|
+
}
|
|
71
|
+
if (existing.PubliclyAccessible !== this._publicAccess) {
|
|
72
|
+
diffs.push({ field: "publicAccess", declared: this._publicAccess, live: existing.PubliclyAccessible });
|
|
73
|
+
}
|
|
74
|
+
return diffs;
|
|
75
|
+
}
|
|
57
76
|
database(name) {
|
|
58
77
|
this._dbName = name;
|
|
59
78
|
return this;
|
|
@@ -26,6 +26,15 @@ export declare class DatabaseBuilder extends BaseBuilder {
|
|
|
26
26
|
allowIp(cidr: string): this;
|
|
27
27
|
allowDroplet(dropletId: string): this;
|
|
28
28
|
allowTag(tagName: string): this;
|
|
29
|
+
getDiff(existing: any): ({
|
|
30
|
+
field: string;
|
|
31
|
+
declared: string;
|
|
32
|
+
live: any;
|
|
33
|
+
} | {
|
|
34
|
+
field: string;
|
|
35
|
+
declared: number;
|
|
36
|
+
live: any;
|
|
37
|
+
})[];
|
|
29
38
|
private discoverCluster;
|
|
30
39
|
deploy(): Promise<{
|
|
31
40
|
name: string;
|
|
@@ -58,6 +58,25 @@ export class DatabaseBuilder extends BaseBuilder {
|
|
|
58
58
|
this._firewallRules.push({ type: "tag", value: tagName });
|
|
59
59
|
return this;
|
|
60
60
|
}
|
|
61
|
+
getDiff(existing) {
|
|
62
|
+
const diffs = [];
|
|
63
|
+
if (existing.engine !== this._engine) {
|
|
64
|
+
diffs.push({ field: "engine", declared: this._engine, live: existing.engine });
|
|
65
|
+
}
|
|
66
|
+
if (existing.version !== this._version) {
|
|
67
|
+
diffs.push({ field: "version", declared: this._version, live: existing.version });
|
|
68
|
+
}
|
|
69
|
+
if (existing.size !== this._size) {
|
|
70
|
+
diffs.push({ field: "size", declared: this._size, live: existing.size });
|
|
71
|
+
}
|
|
72
|
+
if (existing.region !== this._region) {
|
|
73
|
+
diffs.push({ field: "region", declared: this._region, live: existing.region });
|
|
74
|
+
}
|
|
75
|
+
if (existing.num_nodes !== this._nodes) {
|
|
76
|
+
diffs.push({ field: "nodes", declared: this._nodes, live: existing.num_nodes });
|
|
77
|
+
}
|
|
78
|
+
return diffs;
|
|
79
|
+
}
|
|
61
80
|
async discoverCluster(name) {
|
|
62
81
|
try {
|
|
63
82
|
const api = getDoApi();
|
|
@@ -104,7 +104,7 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
104
104
|
if (existing) {
|
|
105
105
|
try {
|
|
106
106
|
const res = await api.get(`/domains/${this.domainName}/records?per_page=200`);
|
|
107
|
-
existingRecords = res.domain_records;
|
|
107
|
+
existingRecords = res.domain_records ?? [];
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
110
|
existingRecords = [];
|
|
@@ -30,6 +30,11 @@ export declare class DropletBuilder extends BaseBuilder {
|
|
|
30
30
|
vpc(uuid: string | Output<string>): this;
|
|
31
31
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
32
32
|
forceConfigCheck(): this;
|
|
33
|
+
getDiff(existing: any): {
|
|
34
|
+
field: string;
|
|
35
|
+
declared: any;
|
|
36
|
+
live: any;
|
|
37
|
+
}[];
|
|
33
38
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
34
39
|
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
35
40
|
private resolveOrRegisterSshKey;
|
|
@@ -101,6 +101,16 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
101
101
|
this._forceConfigCheck = true;
|
|
102
102
|
return this;
|
|
103
103
|
}
|
|
104
|
+
getDiff(existing) {
|
|
105
|
+
const diffs = [];
|
|
106
|
+
if (existing.size_slug !== this.config.size) {
|
|
107
|
+
diffs.push({ field: "size", declared: this.config.size, live: existing.size_slug });
|
|
108
|
+
}
|
|
109
|
+
if (existing.region?.slug !== this.config.region) {
|
|
110
|
+
diffs.push({ field: "region", declared: this.config.region, live: existing.region?.slug });
|
|
111
|
+
}
|
|
112
|
+
return diffs;
|
|
113
|
+
}
|
|
104
114
|
async checkPort(ip, port) {
|
|
105
115
|
return checkPort(ip, port);
|
|
106
116
|
}
|
|
@@ -20,11 +20,14 @@ function priceForSlug(slug) {
|
|
|
20
20
|
}
|
|
21
21
|
export async function listDoResources() {
|
|
22
22
|
const api = getDoApi();
|
|
23
|
-
const [dropletsData, firewallsData, lbData, domainsData] = await Promise.all([
|
|
23
|
+
const [dropletsData, firewallsData, lbData, domainsData, dbData, appsData, vpcsData] = await Promise.all([
|
|
24
24
|
api.get('/droplets?per_page=200'),
|
|
25
25
|
api.get('/firewalls?per_page=200'),
|
|
26
26
|
api.get('/load_balancers?per_page=200'),
|
|
27
27
|
api.get('/domains?per_page=200'),
|
|
28
|
+
api.get('/databases?per_page=200'),
|
|
29
|
+
api.get('/apps?per_page=200'),
|
|
30
|
+
api.get('/vpcs?per_page=200'),
|
|
28
31
|
]);
|
|
29
32
|
const droplets = dropletsData.droplets.map((d) => {
|
|
30
33
|
const pub = (d.networks?.v4 ?? []).find((n) => n.type === 'public');
|
|
@@ -54,6 +57,26 @@ export async function listDoResources() {
|
|
|
54
57
|
name: d.name,
|
|
55
58
|
ttl: d.ttl,
|
|
56
59
|
}));
|
|
60
|
+
const databases = (dbData.databases ?? []).map((d) => ({
|
|
61
|
+
id: d.id,
|
|
62
|
+
name: d.name,
|
|
63
|
+
engine: `${d.engine} ${d.version ?? ''}`.trim(),
|
|
64
|
+
region: d.region ?? '',
|
|
65
|
+
status: d.status ?? '',
|
|
66
|
+
nodeCount: d.num_nodes ?? 1,
|
|
67
|
+
}));
|
|
68
|
+
const apps = (appsData.apps ?? []).map((a) => ({
|
|
69
|
+
id: a.id,
|
|
70
|
+
name: a.spec?.name ?? a.id,
|
|
71
|
+
liveUrl: a.live_url ?? '',
|
|
72
|
+
status: a.active_deployment?.phase ?? 'unknown',
|
|
73
|
+
}));
|
|
74
|
+
const vpcs = (vpcsData.vpcs ?? []).map((v) => ({
|
|
75
|
+
id: v.id,
|
|
76
|
+
name: v.name,
|
|
77
|
+
region: v.region ?? '',
|
|
78
|
+
ipRange: v.ip_range ?? '',
|
|
79
|
+
}));
|
|
57
80
|
const totalMonthlyCost = droplets.reduce((sum, d) => sum + d.monthlyCost, 0);
|
|
58
|
-
return { droplets, firewalls, loadBalancers, domains, totalMonthlyCost };
|
|
81
|
+
return { droplets, firewalls, loadBalancers, domains, databases, apps, vpcs, totalMonthlyCost };
|
|
59
82
|
}
|
|
@@ -41,6 +41,11 @@ export declare class LoadBalancerBuilder extends BaseBuilder {
|
|
|
41
41
|
stickySession(type: 'cookies' | 'none', cookieName?: string, cookieTtlSeconds?: number): this;
|
|
42
42
|
private resolveDropletIds;
|
|
43
43
|
private resolveCertificateId;
|
|
44
|
+
getDiff(existing: any): {
|
|
45
|
+
field: string;
|
|
46
|
+
declared: string;
|
|
47
|
+
live: any;
|
|
48
|
+
}[];
|
|
44
49
|
deploy(): Promise<any>;
|
|
45
50
|
destroy(): Promise<any>;
|
|
46
51
|
}
|
|
@@ -96,6 +96,13 @@ export class LoadBalancerBuilder extends BaseBuilder {
|
|
|
96
96
|
}
|
|
97
97
|
return match.id;
|
|
98
98
|
}
|
|
99
|
+
getDiff(existing) {
|
|
100
|
+
const diffs = [];
|
|
101
|
+
if (existing.region?.slug !== this._region) {
|
|
102
|
+
diffs.push({ field: "region", declared: this._region, live: existing.region?.slug });
|
|
103
|
+
}
|
|
104
|
+
return diffs;
|
|
105
|
+
}
|
|
99
106
|
async deploy() {
|
|
100
107
|
const dryRun = this.isDryRunActive();
|
|
101
108
|
const existing = await this.discoveryPromise;
|
|
@@ -12,6 +12,11 @@ export declare class VPCBuilder extends BaseBuilder {
|
|
|
12
12
|
region(r: string): this;
|
|
13
13
|
ipRange(cidr: string): this;
|
|
14
14
|
description(text: string): this;
|
|
15
|
+
getDiff(existing: any): {
|
|
16
|
+
field: string;
|
|
17
|
+
declared: string;
|
|
18
|
+
live: any;
|
|
19
|
+
}[];
|
|
15
20
|
private discoverVpc;
|
|
16
21
|
deploy(): Promise<{
|
|
17
22
|
name: string;
|
package/dist/providers/do/vpc.js
CHANGED
|
@@ -26,6 +26,14 @@ export class VPCBuilder extends BaseBuilder {
|
|
|
26
26
|
this._description = text;
|
|
27
27
|
return this;
|
|
28
28
|
}
|
|
29
|
+
getDiff(existing) {
|
|
30
|
+
const diffs = [];
|
|
31
|
+
// region and ip_range are immutable after creation
|
|
32
|
+
if (this._description !== undefined && existing.description !== this._description) {
|
|
33
|
+
diffs.push({ field: "description", declared: this._description, live: existing.description });
|
|
34
|
+
}
|
|
35
|
+
return diffs;
|
|
36
|
+
}
|
|
29
37
|
async discoverVpc(name) {
|
|
30
38
|
try {
|
|
31
39
|
const api = getDoApi();
|
|
@@ -29,6 +29,15 @@ export declare class FirebaseFunctionsBuilder extends BaseBuilder {
|
|
|
29
29
|
maxInstances(n: number): this;
|
|
30
30
|
minInstances(n: number): this;
|
|
31
31
|
env(vars: Record<string, string>): this;
|
|
32
|
+
getDiff(existing: any): ({
|
|
33
|
+
field: string;
|
|
34
|
+
declared: string;
|
|
35
|
+
live: any;
|
|
36
|
+
} | {
|
|
37
|
+
field: string;
|
|
38
|
+
declared: number;
|
|
39
|
+
live: any;
|
|
40
|
+
})[];
|
|
32
41
|
private fnPath;
|
|
33
42
|
private getExisting;
|
|
34
43
|
private zipSource;
|