puls-dev 0.3.3 → 0.3.5
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/dist/bin/install-shell.d.ts +2 -0
- package/dist/bin/install-shell.js +136 -0
- package/dist/bin/puls.d.ts +1 -0
- package/dist/bin/puls.js +145 -0
- package/dist/core/checker.js +74 -0
- package/dist/core/config.d.ts +3 -0
- package/dist/core/context.d.ts +1 -0
- package/dist/core/decorators.d.ts +1 -0
- package/dist/core/decorators.js +39 -5
- package/dist/core/output.js +8 -1
- package/dist/core/production.test.js +1 -0
- package/dist/core/resource.d.ts +35 -0
- package/dist/core/resource.js +57 -1
- package/dist/core/secret.d.ts +1 -0
- package/dist/core/secret.js +5 -0
- package/dist/core/stack.d.ts +11 -0
- package/dist/core/stack.js +141 -90
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- 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 +10 -0
- package/dist/providers/do/droplet.js +28 -3
- package/dist/providers/do/droplet.test.js +1 -1
- 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/template.d.ts +3 -0
- package/dist/providers/gcp/template.js +13 -1
- package/dist/providers/gcp/vm.d.ts +8 -0
- package/dist/providers/gcp/vm.js +22 -2
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +18 -3
- package/dist/providers/proxmox/base.d.ts +16 -0
- package/dist/providers/proxmox/base.js +121 -0
- package/dist/providers/proxmox/list.js +8 -1
- package/dist/providers/proxmox/template.d.ts +3 -10
- package/dist/providers/proxmox/template.js +51 -139
- package/dist/providers/proxmox/vm.d.ts +18 -10
- package/dist/providers/proxmox/vm.js +73 -152
- 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 +7 -22
|
@@ -24,6 +24,7 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
24
24
|
dropletId;
|
|
25
25
|
resolvedIp;
|
|
26
26
|
sshKeyPath;
|
|
27
|
+
_sshUser;
|
|
27
28
|
_provision = [];
|
|
28
29
|
_forceConfigCheck = false;
|
|
29
30
|
constructor(name) {
|
|
@@ -70,10 +71,24 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
70
71
|
this.config.size = size;
|
|
71
72
|
return this;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
sshKey(keyPath) {
|
|
74
75
|
this.sshKeyPath = keyPath.replace('~', homedir());
|
|
75
76
|
return this;
|
|
76
77
|
}
|
|
78
|
+
/** @deprecated Use `.sshKey()` instead */
|
|
79
|
+
sslKey(keyPath) {
|
|
80
|
+
return this.sshKey(keyPath);
|
|
81
|
+
}
|
|
82
|
+
sshUser(user) {
|
|
83
|
+
this._sshUser = user;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
resolveUser() {
|
|
87
|
+
return (this._sshUser ??
|
|
88
|
+
process.env.DO_SSH_USER ??
|
|
89
|
+
Config.get().providers.do?.sshUser ??
|
|
90
|
+
"root");
|
|
91
|
+
}
|
|
77
92
|
vpc(uuid) {
|
|
78
93
|
this.config.vpc_uuid = uuid;
|
|
79
94
|
return this;
|
|
@@ -86,12 +101,22 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
86
101
|
this._forceConfigCheck = true;
|
|
87
102
|
return this;
|
|
88
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
|
+
}
|
|
89
114
|
async checkPort(ip, port) {
|
|
90
115
|
return checkPort(ip, port);
|
|
91
116
|
}
|
|
92
117
|
async runProvisioner(ip, script) {
|
|
93
118
|
const keyPath = this.sshKeyPath ? this.sshKeyPath : undefined;
|
|
94
|
-
return runProvisioner(ip,
|
|
119
|
+
return runProvisioner(ip, this.resolveUser(), keyPath, script);
|
|
95
120
|
}
|
|
96
121
|
async resolveOrRegisterSshKey(api) {
|
|
97
122
|
const pubPath = this.sshKeyPath.replace(/\.pub$/, '') + '.pub';
|
|
@@ -258,7 +283,7 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
258
283
|
context.hosts.push({
|
|
259
284
|
name: this.name,
|
|
260
285
|
ip: activeIp,
|
|
261
|
-
user:
|
|
286
|
+
user: this.resolveUser(),
|
|
262
287
|
sshKey: this.sshKeyPath,
|
|
263
288
|
provider: "do"
|
|
264
289
|
});
|
|
@@ -106,7 +106,7 @@ describe('DropletBuilder Unit Tests', () => {
|
|
|
106
106
|
builder
|
|
107
107
|
.region('nyc3')
|
|
108
108
|
.size('s-1vcpu-1gb')
|
|
109
|
-
.
|
|
109
|
+
.sshKey('~/.ssh/id_rsa.pub');
|
|
110
110
|
const result = await builder.deploy();
|
|
111
111
|
assert.ok(result);
|
|
112
112
|
assert.strictEqual(result.region, 'nyc3');
|
|
@@ -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;
|
|
@@ -79,6 +79,34 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
|
|
|
79
79
|
this._env = vars;
|
|
80
80
|
return this;
|
|
81
81
|
}
|
|
82
|
+
getDiff(existing) {
|
|
83
|
+
const diffs = [];
|
|
84
|
+
const liveRuntime = existing.buildConfig?.runtime;
|
|
85
|
+
if (liveRuntime !== undefined && liveRuntime !== this._runtime) {
|
|
86
|
+
diffs.push({ field: "runtime", declared: this._runtime, live: liveRuntime });
|
|
87
|
+
}
|
|
88
|
+
const liveEntryPoint = existing.buildConfig?.entryPoint;
|
|
89
|
+
if (liveEntryPoint !== undefined && liveEntryPoint !== this._entryPoint) {
|
|
90
|
+
diffs.push({ field: "entryPoint", declared: this._entryPoint, live: liveEntryPoint });
|
|
91
|
+
}
|
|
92
|
+
const liveMemory = existing.serviceConfig?.availableMemory;
|
|
93
|
+
if (liveMemory !== undefined && liveMemory !== this._memory) {
|
|
94
|
+
diffs.push({ field: "memory", declared: this._memory, live: liveMemory });
|
|
95
|
+
}
|
|
96
|
+
const liveTimeout = existing.serviceConfig?.timeoutSeconds;
|
|
97
|
+
if (liveTimeout !== undefined && liveTimeout !== this._timeout) {
|
|
98
|
+
diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${liveTimeout}s` });
|
|
99
|
+
}
|
|
100
|
+
const liveMax = existing.serviceConfig?.maxInstanceCount;
|
|
101
|
+
if (liveMax !== undefined && liveMax !== this._maxInstances) {
|
|
102
|
+
diffs.push({ field: "maxInstances", declared: this._maxInstances, live: liveMax });
|
|
103
|
+
}
|
|
104
|
+
const liveMin = existing.serviceConfig?.minInstanceCount ?? 0;
|
|
105
|
+
if (liveMin !== this._minInstances) {
|
|
106
|
+
diffs.push({ field: "minInstances", declared: this._minInstances, live: liveMin });
|
|
107
|
+
}
|
|
108
|
+
return diffs;
|
|
109
|
+
}
|
|
82
110
|
fnPath() {
|
|
83
111
|
return `/projects/${getProjectId()}/locations/${this._region}/functions/${this.name}`;
|
|
84
112
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { getProjectId, hostingFetch, cloudFetch } from "./api.js";
|
|
2
2
|
export async function listFirebaseResources() {
|
|
3
3
|
const project = getProjectId();
|
|
4
|
-
const [hostRes, fnRes] = await Promise.all([
|
|
4
|
+
const [hostRes, fnRes, firestoreRes, storageRes, authRes, rcRes] = await Promise.all([
|
|
5
5
|
hostingFetch(`/projects/${project}/sites`).catch(() => ({})),
|
|
6
6
|
cloudFetch("https://cloudfunctions.googleapis.com/v2", `/projects/${project}/locations/-/functions`).catch(() => ({})),
|
|
7
|
+
cloudFetch("https://firestore.googleapis.com/v1", `/projects/${project}/databases`).catch(() => ({})),
|
|
8
|
+
cloudFetch("https://storage.googleapis.com/storage/v1", `/b?project=${project}`).catch(() => ({})),
|
|
9
|
+
cloudFetch("https://identitytoolkit.googleapis.com/admin/v2", `/projects/${project}/config`).catch(() => ({})),
|
|
10
|
+
cloudFetch("https://firebaseremoteconfig.googleapis.com/v1", `/projects/${project}/remoteConfig`).catch(() => ({})),
|
|
7
11
|
]);
|
|
8
12
|
// 1. Map Hosting Sites
|
|
9
13
|
const hostingSites = (hostRes.sites ?? []).map((s) => ({
|
|
@@ -21,5 +25,33 @@ export async function listFirebaseResources() {
|
|
|
21
25
|
runtime: f.buildConfig?.runtime ?? "unknown",
|
|
22
26
|
};
|
|
23
27
|
});
|
|
24
|
-
|
|
28
|
+
// 3. Map Firestore Databases
|
|
29
|
+
const firestoreDbs = (firestoreRes.databases ?? []).map((d) => ({
|
|
30
|
+
name: d.name.split("/").pop() ?? d.name,
|
|
31
|
+
type: d.type ?? "FIRESTORE_NATIVE",
|
|
32
|
+
state: d.state ?? "unknown",
|
|
33
|
+
}));
|
|
34
|
+
// 4. Map Storage Buckets (filter to project-owned buckets)
|
|
35
|
+
const storageBuckets = (storageRes.items ?? [])
|
|
36
|
+
.filter((b) => b.name?.includes(project))
|
|
37
|
+
.map((b) => ({
|
|
38
|
+
name: b.name,
|
|
39
|
+
location: b.location ?? "unknown",
|
|
40
|
+
}));
|
|
41
|
+
// 5. Map Auth Sign-in Providers from Identity Toolkit config
|
|
42
|
+
const signIn = authRes.signIn ?? {};
|
|
43
|
+
const authProviders = [
|
|
44
|
+
...(signIn.email?.enabled ? [{ providerId: "email/password" }] : []),
|
|
45
|
+
...(signIn.phoneNumber?.enabled ? [{ providerId: "phone" }] : []),
|
|
46
|
+
...(signIn.anonymous?.enabled ? [{ providerId: "anonymous" }] : []),
|
|
47
|
+
];
|
|
48
|
+
// 6. Map RemoteConfig
|
|
49
|
+
let remoteConfig;
|
|
50
|
+
if (rcRes.parameters !== undefined || rcRes.version) {
|
|
51
|
+
remoteConfig = {
|
|
52
|
+
parameterCount: Object.keys(rcRes.parameters ?? {}).length,
|
|
53
|
+
version: rcRes.version?.versionNumber ?? "unknown",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { hostingSites, functions, firestoreDbs, storageBuckets, authProviders, remoteConfig };
|
|
25
57
|
}
|
|
@@ -113,6 +113,12 @@ function createGcpOfflineMock(base, path, opts) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
if (path.includes("/instances")) {
|
|
116
|
+
// Specific instance GET (discovery path: /instances/{name}) - return not-found so
|
|
117
|
+
// builders plan creation rather than treating the mock VM as already existing
|
|
118
|
+
const afterInstances = path.split("/instances")[1] ?? "";
|
|
119
|
+
if (afterInstances.startsWith("/") && afterInstances.length > 1) {
|
|
120
|
+
throw new Error(`GCP API GET ${path} → 404: Not Found`);
|
|
121
|
+
}
|
|
116
122
|
return {
|
|
117
123
|
status: "RUNNING",
|
|
118
124
|
id: "mock-gcp-instance-id",
|
|
@@ -20,6 +20,19 @@ export declare class GCPCloudRunBuilder extends BaseBuilder {
|
|
|
20
20
|
env(vars: Record<string, string | GCPSecretBuilder>): this;
|
|
21
21
|
region(reg: string): this;
|
|
22
22
|
public(enabled?: boolean): this;
|
|
23
|
+
getDiff(existing: any): ({
|
|
24
|
+
field: string;
|
|
25
|
+
declared: string | undefined;
|
|
26
|
+
live: any;
|
|
27
|
+
} | {
|
|
28
|
+
field: string;
|
|
29
|
+
declared: number;
|
|
30
|
+
live: any;
|
|
31
|
+
} | {
|
|
32
|
+
field: string;
|
|
33
|
+
declared: boolean;
|
|
34
|
+
live: boolean;
|
|
35
|
+
})[];
|
|
23
36
|
private discoverService;
|
|
24
37
|
deploy(): Promise<{
|
|
25
38
|
serviceId: string;
|
|
@@ -60,6 +60,36 @@ export class GCPCloudRunBuilder extends BaseBuilder {
|
|
|
60
60
|
this._public = enabled;
|
|
61
61
|
return this;
|
|
62
62
|
}
|
|
63
|
+
getDiff(existing) {
|
|
64
|
+
const diffs = [];
|
|
65
|
+
const container = existing.template?.containers?.[0];
|
|
66
|
+
const scaling = existing.template?.scaling ?? {};
|
|
67
|
+
const targetIngress = this._public ? "INGRESS_TRAFFIC_ALL" : "INGRESS_TRAFFIC_INTERNAL_ONLY";
|
|
68
|
+
if (container?.image !== this._image) {
|
|
69
|
+
diffs.push({ field: "image", declared: this._image, live: container?.image });
|
|
70
|
+
}
|
|
71
|
+
if (container?.ports?.[0]?.containerPort !== this._port) {
|
|
72
|
+
diffs.push({ field: "port", declared: this._port, live: container?.ports?.[0]?.containerPort });
|
|
73
|
+
}
|
|
74
|
+
if (container?.resources?.limits?.cpu !== formatCpu(this._cpu)) {
|
|
75
|
+
diffs.push({ field: "cpu", declared: formatCpu(this._cpu), live: container?.resources?.limits?.cpu });
|
|
76
|
+
}
|
|
77
|
+
if (container?.resources?.limits?.memory !== formatMemory(this._memory)) {
|
|
78
|
+
diffs.push({ field: "memory", declared: formatMemory(this._memory), live: container?.resources?.limits?.memory });
|
|
79
|
+
}
|
|
80
|
+
const minDeclared = this._minInstances ?? 0;
|
|
81
|
+
const maxDeclared = this._maxInstances ?? 100;
|
|
82
|
+
if ((scaling.minInstanceCount ?? 0) !== minDeclared) {
|
|
83
|
+
diffs.push({ field: "minInstances", declared: minDeclared, live: scaling.minInstanceCount ?? 0 });
|
|
84
|
+
}
|
|
85
|
+
if ((scaling.maxInstanceCount ?? 100) !== maxDeclared) {
|
|
86
|
+
diffs.push({ field: "maxInstances", declared: maxDeclared, live: scaling.maxInstanceCount ?? 100 });
|
|
87
|
+
}
|
|
88
|
+
if (existing.ingress !== targetIngress) {
|
|
89
|
+
diffs.push({ field: "public", declared: this._public, live: existing.ingress === "INGRESS_TRAFFIC_ALL" });
|
|
90
|
+
}
|
|
91
|
+
return diffs;
|
|
92
|
+
}
|
|
63
93
|
async discoverService() {
|
|
64
94
|
try {
|
|
65
95
|
const project = getProjectId();
|
|
@@ -23,6 +23,15 @@ export declare class GCPCloudSQLBuilder extends BaseBuilder {
|
|
|
23
23
|
database(name: string): this;
|
|
24
24
|
publicAccess(enabled?: boolean): this;
|
|
25
25
|
region(reg: string): this;
|
|
26
|
+
getDiff(existing: any): ({
|
|
27
|
+
field: string;
|
|
28
|
+
declared: string;
|
|
29
|
+
live: any;
|
|
30
|
+
} | {
|
|
31
|
+
field: string;
|
|
32
|
+
declared: boolean;
|
|
33
|
+
live: boolean;
|
|
34
|
+
})[];
|
|
26
35
|
private discoverInstance;
|
|
27
36
|
private waitForOperation;
|
|
28
37
|
deploy(): Promise<{
|
|
@@ -64,6 +64,26 @@ export class GCPCloudSQLBuilder extends BaseBuilder {
|
|
|
64
64
|
this.discoveryPromise = this.discoverInstance();
|
|
65
65
|
return this;
|
|
66
66
|
}
|
|
67
|
+
getDiff(existing) {
|
|
68
|
+
const diffs = [];
|
|
69
|
+
const declaredDbVersion = `${this._engine.toUpperCase()}_${this._engineVersion}`;
|
|
70
|
+
if (existing.databaseVersion !== declaredDbVersion) {
|
|
71
|
+
diffs.push({ field: "databaseVersion", declared: declaredDbVersion, live: existing.databaseVersion });
|
|
72
|
+
}
|
|
73
|
+
const liveTier = existing.settings?.tier;
|
|
74
|
+
if (liveTier !== undefined && liveTier !== this._tier) {
|
|
75
|
+
diffs.push({ field: "tier", declared: this._tier, live: liveTier });
|
|
76
|
+
}
|
|
77
|
+
const liveDiskSize = existing.settings?.dataDiskSizeGb;
|
|
78
|
+
if (liveDiskSize !== undefined && Number(liveDiskSize) !== this._storage) {
|
|
79
|
+
diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${liveDiskSize} GB` });
|
|
80
|
+
}
|
|
81
|
+
const isPublic = (existing.settings?.ipConfiguration?.authorizedNetworks ?? []).length > 0;
|
|
82
|
+
if (isPublic !== this._publicAccess) {
|
|
83
|
+
diffs.push({ field: "publicAccess", declared: this._publicAccess, live: isPublic });
|
|
84
|
+
}
|
|
85
|
+
return diffs;
|
|
86
|
+
}
|
|
67
87
|
async discoverInstance() {
|
|
68
88
|
try {
|
|
69
89
|
const project = getProjectId();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { gcpFetch, getProjectId } from "./api.js";
|
|
2
2
|
export async function listGcpResources() {
|
|
3
3
|
const project = getProjectId();
|
|
4
|
-
const [vmRes, sqlRes, runRes, dnsRes] = await Promise.all([
|
|
4
|
+
const [vmRes, sqlRes, runRes, dnsRes, pubsubRes, secretsRes] = await Promise.all([
|
|
5
5
|
gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/aggregated/instances`).catch(() => ({})),
|
|
6
6
|
gcpFetch("https://sqladmin.googleapis.com", `/v1/projects/${project}/instances`).catch(() => ({})),
|
|
7
7
|
gcpFetch("https://run.googleapis.com", `/v2/projects/${project}/locations/-/services`).catch(() => ({})),
|
|
8
8
|
gcpFetch("https://dns.googleapis.com", `/dns/v1/projects/${project}/managedZones`).catch(() => ({})),
|
|
9
|
+
gcpFetch("https://pubsub.googleapis.com", `/v1/projects/${project}/topics`).catch(() => ({})),
|
|
10
|
+
gcpFetch("https://secretmanager.googleapis.com", `/v1/projects/${project}/secrets`).catch(() => ({})),
|
|
9
11
|
]);
|
|
10
12
|
// 1. Map VM Instances
|
|
11
13
|
const vms = [];
|
|
@@ -51,5 +53,13 @@ export async function listGcpResources() {
|
|
|
51
53
|
name: z.name,
|
|
52
54
|
dnsName: z.dnsName ?? "",
|
|
53
55
|
}));
|
|
54
|
-
|
|
56
|
+
// 5. Map Pub/Sub Topics
|
|
57
|
+
const pubSubTopics = (pubsubRes.topics ?? []).map((t) => ({
|
|
58
|
+
name: t.name.split("/").pop() ?? t.name,
|
|
59
|
+
}));
|
|
60
|
+
// 6. Map Secret Manager Secrets
|
|
61
|
+
const secrets = (secretsRes.secrets ?? []).map((s) => ({
|
|
62
|
+
name: s.name.split("/").pop() ?? s.name,
|
|
63
|
+
}));
|
|
64
|
+
return { vms, rdsInstances, distributions, hostedZones, pubSubTopics, secrets };
|
|
55
65
|
}
|
|
@@ -9,6 +9,7 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
|
|
|
9
9
|
private _zone;
|
|
10
10
|
private _network;
|
|
11
11
|
private _sshKeys;
|
|
12
|
+
private _sshUser?;
|
|
12
13
|
private _provision;
|
|
13
14
|
constructor(name: string);
|
|
14
15
|
baseImage(img: string): this;
|
|
@@ -16,6 +17,8 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
|
|
|
16
17
|
zone(z: string): this;
|
|
17
18
|
network(netPath: string): this;
|
|
18
19
|
sshKey(keys: string | string[]): this;
|
|
20
|
+
sshUser(user: string): this;
|
|
21
|
+
private resolveUser;
|
|
19
22
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
20
23
|
private discoverImage;
|
|
21
24
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
4
5
|
import { Output } from "../../core/output.js";
|
|
5
6
|
import { gcpFetch, getProjectId } from "./api.js";
|
|
6
7
|
import { checkPort, runProvisioner } from "../../core/provisioner.js";
|
|
@@ -15,6 +16,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
15
16
|
_zone = "us-central1-a";
|
|
16
17
|
_network = "global/networks/default";
|
|
17
18
|
_sshKeys = [];
|
|
19
|
+
_sshUser;
|
|
18
20
|
_provision = [];
|
|
19
21
|
constructor(name) {
|
|
20
22
|
super(name);
|
|
@@ -41,6 +43,16 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
41
43
|
this._sshKeys = keys;
|
|
42
44
|
return this;
|
|
43
45
|
}
|
|
46
|
+
sshUser(user) {
|
|
47
|
+
this._sshUser = user;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
resolveUser() {
|
|
51
|
+
return (this._sshUser ??
|
|
52
|
+
process.env.GCP_SSH_USER ??
|
|
53
|
+
Config.get().providers.gcp?.sshUser ??
|
|
54
|
+
"root");
|
|
55
|
+
}
|
|
44
56
|
provision(...playbookPaths) {
|
|
45
57
|
this._provision.push(...playbookPaths.flat());
|
|
46
58
|
return this;
|
|
@@ -66,7 +78,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
66
78
|
async runProvisioner(ip, script) {
|
|
67
79
|
const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
|
|
68
80
|
const keyPath = keysArray.find(k => !k.startsWith('ssh-') && !k.startsWith('ecdsa-') && !k.startsWith('sk-'));
|
|
69
|
-
return runProvisioner(ip,
|
|
81
|
+
return runProvisioner(ip, this.resolveUser(), keyPath, script);
|
|
70
82
|
}
|
|
71
83
|
async deploy() {
|
|
72
84
|
const dryRun = this.isDryRunActive();
|
|
@@ -12,6 +12,7 @@ export declare class GCPVMBuilder extends BaseBuilder {
|
|
|
12
12
|
private _zone;
|
|
13
13
|
private _network;
|
|
14
14
|
private _sshKeys;
|
|
15
|
+
private _sshUser?;
|
|
15
16
|
private _provision;
|
|
16
17
|
private _forceConfigCheck;
|
|
17
18
|
private resolvedInstanceId?;
|
|
@@ -23,8 +24,15 @@ export declare class GCPVMBuilder extends BaseBuilder {
|
|
|
23
24
|
zone(z: string): this;
|
|
24
25
|
network(netPath: string): this;
|
|
25
26
|
sshKey(keys: string | string[]): this;
|
|
27
|
+
sshUser(user: string): this;
|
|
28
|
+
private resolveUser;
|
|
26
29
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
27
30
|
forceConfigCheck(): this;
|
|
31
|
+
getDiff(existing: any): {
|
|
32
|
+
field: string;
|
|
33
|
+
declared: string;
|
|
34
|
+
live: any;
|
|
35
|
+
}[];
|
|
28
36
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
29
37
|
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
30
38
|
private discoverVM;
|
package/dist/providers/gcp/vm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
4
5
|
import { Output } from "../../core/output.js";
|
|
5
6
|
import { gcpFetch, getProjectId } from "./api.js";
|
|
6
7
|
import { checkPort, runProvisioner } from "../../core/provisioner.js";
|
|
@@ -17,6 +18,7 @@ export class GCPVMBuilder extends BaseBuilder {
|
|
|
17
18
|
_zone = "us-central1-a";
|
|
18
19
|
_network = "global/networks/default";
|
|
19
20
|
_sshKeys = [];
|
|
21
|
+
_sshUser;
|
|
20
22
|
_provision = [];
|
|
21
23
|
_forceConfigCheck = false;
|
|
22
24
|
resolvedInstanceId;
|
|
@@ -51,6 +53,16 @@ export class GCPVMBuilder extends BaseBuilder {
|
|
|
51
53
|
this._sshKeys = keys;
|
|
52
54
|
return this;
|
|
53
55
|
}
|
|
56
|
+
sshUser(user) {
|
|
57
|
+
this._sshUser = user;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
resolveUser() {
|
|
61
|
+
return (this._sshUser ??
|
|
62
|
+
process.env.GCP_SSH_USER ??
|
|
63
|
+
Config.get().providers.gcp?.sshUser ??
|
|
64
|
+
"root");
|
|
65
|
+
}
|
|
54
66
|
provision(...playbookPaths) {
|
|
55
67
|
this._provision.push(...playbookPaths.flat());
|
|
56
68
|
return this;
|
|
@@ -59,6 +71,14 @@ export class GCPVMBuilder extends BaseBuilder {
|
|
|
59
71
|
this._forceConfigCheck = true;
|
|
60
72
|
return this;
|
|
61
73
|
}
|
|
74
|
+
getDiff(existing) {
|
|
75
|
+
const diffs = [];
|
|
76
|
+
const liveMachineType = existing.machineType?.split("/").pop();
|
|
77
|
+
if (liveMachineType !== undefined && liveMachineType !== this._machineType) {
|
|
78
|
+
diffs.push({ field: "machineType", declared: this._machineType, live: liveMachineType });
|
|
79
|
+
}
|
|
80
|
+
return diffs;
|
|
81
|
+
}
|
|
62
82
|
async checkPort(ip, port) {
|
|
63
83
|
return checkPort(ip, port);
|
|
64
84
|
}
|
|
@@ -68,7 +88,7 @@ export class GCPVMBuilder extends BaseBuilder {
|
|
|
68
88
|
if (!keyPath) {
|
|
69
89
|
throw new Error(`[GCP VM:${this.name}] No SSH private key path found. Pass a file path via .sshKey() to run provisioning.`);
|
|
70
90
|
}
|
|
71
|
-
return runProvisioner(ip,
|
|
91
|
+
return runProvisioner(ip, this.resolveUser(), keyPath, script);
|
|
72
92
|
}
|
|
73
93
|
async discoverVM() {
|
|
74
94
|
try {
|
|
@@ -298,7 +318,7 @@ export class GCPVMBuilder extends BaseBuilder {
|
|
|
298
318
|
context.hosts.push({
|
|
299
319
|
name: this.name,
|
|
300
320
|
ip: activeIp,
|
|
301
|
-
user:
|
|
321
|
+
user: this.resolveUser(),
|
|
302
322
|
sshKey: keyPath,
|
|
303
323
|
provider: "gcp"
|
|
304
324
|
});
|
|
@@ -10,4 +10,5 @@ export declare class ProxmoxApiClient {
|
|
|
10
10
|
put<T>(path: string, body?: unknown): Promise<T>;
|
|
11
11
|
delete(path: string): Promise<void>;
|
|
12
12
|
}
|
|
13
|
+
export declare function withVmidAllocation<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
14
|
export declare function getPMClient(): ProxmoxApiClient;
|
|
@@ -75,9 +75,10 @@ export class ProxmoxApiClient {
|
|
|
75
75
|
return json.data ?? null;
|
|
76
76
|
}, {
|
|
77
77
|
retryable: (err) => {
|
|
78
|
-
|
|
78
|
+
// Match the 3-digit HTTP status that appears after ": " at the end of the path segment
|
|
79
|
+
const match = err.message.match(/:\s(\d{3})(?:\s|$)/);
|
|
79
80
|
const status = match ? parseInt(match[1], 10) : null;
|
|
80
|
-
return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
|
|
81
|
+
return status === 429 || (status !== null && status >= 500) || err.message.includes("ETIMEDOUT");
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
}
|
|
@@ -94,11 +95,25 @@ export class ProxmoxApiClient {
|
|
|
94
95
|
await this.request("DELETE", path);
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
let _vmidLock = Promise.resolve();
|
|
99
|
+
export async function withVmidAllocation(fn) {
|
|
100
|
+
let release;
|
|
101
|
+
const acquired = new Promise(r => { release = r; });
|
|
102
|
+
const prev = _vmidLock;
|
|
103
|
+
_vmidLock = prev.then(() => acquired);
|
|
104
|
+
await prev;
|
|
105
|
+
try {
|
|
106
|
+
return await fn();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
release();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
97
112
|
export function getPMClient() {
|
|
98
113
|
const cfg = Config.get().providers.proxmox;
|
|
99
114
|
if (!cfg?.url || !cfg?.user || !cfg?.tokenName || !cfg?.tokenSecret) {
|
|
100
115
|
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
101
|
-
return new ProxmoxApiClient("https://
|
|
116
|
+
return new ProxmoxApiClient("https://proxmox.invalid:8006", "mock-user@pve", "mock-token-name", "mock-token-secret", false);
|
|
102
117
|
}
|
|
103
118
|
throw new Error("Proxmox not configured. Set proxmox: { url, user, tokenName, tokenSecret } in @Deploy");
|
|
104
119
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import type { ProxmoxApiClient } from "./api.js";
|
|
3
|
+
export declare abstract class ProxmoxBaseBuilder extends BaseBuilder {
|
|
4
|
+
protected _sshKeys?: string | string[];
|
|
5
|
+
protected _sshUser?: string;
|
|
6
|
+
sshKey(keys: string | readonly string[]): this;
|
|
7
|
+
sshUser(user: string): this;
|
|
8
|
+
protected resolveUser(): string;
|
|
9
|
+
protected selectBestNode(pm: ProxmoxApiClient): Promise<string | undefined>;
|
|
10
|
+
protected waitForTask(node: string, upid: string, pm: ProxmoxApiClient): Promise<void>;
|
|
11
|
+
protected resolvePublicKeys(): string[];
|
|
12
|
+
protected sshKeyPath(): string;
|
|
13
|
+
protected checkCloudInit(ip: string): Promise<boolean>;
|
|
14
|
+
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
15
|
+
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
16
|
+
}
|