puls-dev 0.2.7 → 0.2.9
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/core/checker.js +71 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +11 -1
- package/dist/core/context.d.ts +14 -0
- package/dist/core/context.js +2 -0
- package/dist/core/decorators.d.ts +4 -0
- package/dist/core/decorators.js +56 -30
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +116 -0
- package/dist/core/hooks.test.d.ts +1 -0
- package/dist/core/hooks.test.js +194 -0
- package/dist/core/multiregion.test.d.ts +1 -0
- package/dist/core/multiregion.test.js +87 -0
- package/dist/core/output.d.ts +2 -0
- package/dist/core/output.js +9 -2
- package/dist/core/parallel.test.d.ts +1 -0
- package/dist/core/parallel.test.js +215 -0
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.js +140 -0
- package/dist/core/parser.test.d.ts +1 -0
- package/dist/core/parser.test.js +117 -0
- package/dist/core/production.test.d.ts +1 -0
- package/dist/core/production.test.js +189 -0
- package/dist/core/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +123 -0
- package/dist/core/resource.d.ts +23 -0
- package/dist/core/resource.js +54 -0
- package/dist/core/retry.d.ts +9 -0
- package/dist/core/retry.js +28 -0
- package/dist/core/retry.test.d.ts +1 -0
- package/dist/core/retry.test.js +66 -0
- package/dist/core/secret.d.ts +41 -0
- package/dist/core/secret.js +105 -0
- package/dist/core/secret.test.d.ts +1 -0
- package/dist/core/secret.test.js +166 -0
- package/dist/core/stack.d.ts +4 -3
- package/dist/core/stack.js +322 -48
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/providers/aws/api.js +97 -17
- package/dist/providers/aws/ec2.d.ts +51 -0
- package/dist/providers/aws/ec2.js +331 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +281 -0
- package/dist/providers/aws/index.d.ts +4 -0
- package/dist/providers/aws/index.js +4 -0
- package/dist/providers/aws/route53.d.ts +1 -0
- package/dist/providers/aws/route53.js +15 -2
- package/dist/providers/aws/route53.test.js +47 -0
- package/dist/providers/aws/template.d.ts +34 -0
- package/dist/providers/aws/template.js +252 -0
- package/dist/providers/aws/template.test.d.ts +1 -0
- package/dist/providers/aws/template.test.js +208 -0
- package/dist/providers/do/api.d.ts +3 -1
- package/dist/providers/do/api.js +126 -27
- package/dist/providers/do/app.d.ts +26 -0
- package/dist/providers/do/app.js +124 -0
- package/dist/providers/do/app.test.d.ts +1 -0
- package/dist/providers/do/app.test.js +268 -0
- package/dist/providers/do/database.d.ts +44 -0
- package/dist/providers/do/database.js +208 -0
- package/dist/providers/do/database.test.d.ts +1 -0
- package/dist/providers/do/database.test.js +293 -0
- package/dist/providers/do/domain.d.ts +2 -0
- package/dist/providers/do/domain.js +30 -0
- package/dist/providers/do/domain.test.js +49 -0
- package/dist/providers/do/droplet.d.ts +9 -0
- package/dist/providers/do/droplet.js +146 -8
- package/dist/providers/do/droplet.test.js +228 -1
- package/dist/providers/do/firewall.d.ts +2 -1
- package/dist/providers/do/firewall.js +23 -9
- package/dist/providers/do/firewall.test.js +54 -0
- package/dist/providers/do/index.d.ts +11 -0
- package/dist/providers/do/index.js +8 -0
- package/dist/providers/do/spaces.d.ts +27 -0
- package/dist/providers/do/spaces.js +142 -0
- package/dist/providers/do/spaces.test.d.ts +1 -0
- package/dist/providers/do/spaces.test.js +180 -0
- package/dist/providers/do/spaces_api.d.ts +2 -0
- package/dist/providers/do/spaces_api.js +20 -0
- package/dist/providers/do/vpc.d.ts +30 -0
- package/dist/providers/do/vpc.js +128 -0
- package/dist/providers/do/vpc.test.d.ts +1 -0
- package/dist/providers/do/vpc.test.js +258 -0
- package/dist/providers/firebase/api.js +92 -29
- package/dist/providers/firebase/list.d.ts +2 -0
- package/dist/providers/firebase/list.js +25 -0
- package/dist/providers/gcp/api.js +88 -14
- package/dist/providers/gcp/clouddns.d.ts +1 -0
- package/dist/providers/gcp/clouddns.js +15 -2
- package/dist/providers/gcp/clouddns.test.js +45 -0
- package/dist/providers/gcp/index.d.ts +5 -1
- package/dist/providers/gcp/index.js +5 -1
- package/dist/providers/gcp/list.d.ts +2 -0
- package/dist/providers/gcp/list.js +55 -0
- package/dist/providers/gcp/secrets.js +1 -1
- package/dist/providers/gcp/template.d.ts +32 -0
- package/dist/providers/gcp/template.js +252 -0
- package/dist/providers/gcp/template.test.d.ts +1 -0
- package/dist/providers/gcp/template.test.js +227 -0
- package/dist/providers/gcp/vm.d.ts +48 -0
- package/dist/providers/gcp/vm.js +375 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +72 -16
- package/dist/providers/proxmox/index.d.ts +2 -0
- package/dist/providers/proxmox/index.js +2 -0
- package/dist/providers/proxmox/template.d.ts +44 -0
- package/dist/providers/proxmox/template.js +349 -0
- package/dist/providers/proxmox/template.test.d.ts +1 -0
- package/dist/providers/proxmox/template.test.js +179 -0
- package/dist/providers/proxmox/vm.d.ts +7 -4
- package/dist/providers/proxmox/vm.js +57 -102
- package/dist/providers/proxmox/vm.test.js +77 -0
- package/dist/types/inventory.d.ts +44 -1
- package/package.json +3 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { GoogleAuth } from 'google-auth-library';
|
|
3
3
|
import { Config } from '../../core/config.js';
|
|
4
|
+
import { withRetry } from '../../core/retry.js';
|
|
5
|
+
import { resourceContextStorage } from '../../core/context.js';
|
|
4
6
|
function resolveFirebaseConfig() {
|
|
7
|
+
const isOffline = Config.isOfflineMode() || Config.isGlobalDryRun();
|
|
5
8
|
const cfg = Config.get().providers.firebase;
|
|
6
9
|
if (cfg?.serviceAccountPath)
|
|
7
10
|
return cfg;
|
|
@@ -11,12 +14,18 @@ function resolveFirebaseConfig() {
|
|
|
11
14
|
const sa = JSON.parse(fs.readFileSync(saPath, 'utf8'));
|
|
12
15
|
return { projectId: sa.project_id, serviceAccountPath: saPath };
|
|
13
16
|
}
|
|
17
|
+
if (isOffline) {
|
|
18
|
+
return { projectId: "mock-firebase-project", serviceAccountPath: "/mock/sa.json" };
|
|
19
|
+
}
|
|
14
20
|
throw new Error('Firebase not configured. Set FIREBASE_SA=/path/to/sa.json or use @Deploy({ firebase: "..." })');
|
|
15
21
|
}
|
|
16
22
|
export function getProjectId() {
|
|
17
23
|
return resolveFirebaseConfig().projectId;
|
|
18
24
|
}
|
|
19
25
|
export async function getFirebaseToken(scopes) {
|
|
26
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
27
|
+
return "mock-firebase-token";
|
|
28
|
+
}
|
|
20
29
|
const { serviceAccountPath } = resolveFirebaseConfig();
|
|
21
30
|
const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
|
|
22
31
|
const client = await auth.getClient();
|
|
@@ -25,38 +34,92 @@ export async function getFirebaseToken(scopes) {
|
|
|
25
34
|
}
|
|
26
35
|
const HOSTING_SCOPE = 'https://www.googleapis.com/auth/firebase.hosting';
|
|
27
36
|
const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
function createFirebaseOfflineMock(path, opts) {
|
|
38
|
+
if (path.includes("/versions")) {
|
|
39
|
+
return { name: `${path}/versions/mock-version-id`, status: "FINALIZED" };
|
|
40
|
+
}
|
|
41
|
+
if (path.includes("/releases")) {
|
|
42
|
+
return { name: `${path}/releases/mock-release-id` };
|
|
43
|
+
}
|
|
44
|
+
if (path.includes("/sites/")) {
|
|
45
|
+
return { name: "mock-site-name", defaultUrl: "https://mock-project.web.app" };
|
|
46
|
+
}
|
|
47
|
+
return new Proxy({}, {
|
|
48
|
+
get(target, prop) {
|
|
49
|
+
if (prop === "then")
|
|
50
|
+
return undefined;
|
|
51
|
+
if (prop === "name")
|
|
52
|
+
return "mock-firebase-name";
|
|
53
|
+
if (prop === "status")
|
|
54
|
+
return "FINALIZED";
|
|
55
|
+
if (prop === "id")
|
|
56
|
+
return "mock-firebase-id";
|
|
57
|
+
if (prop.endsWith("s"))
|
|
58
|
+
return [];
|
|
59
|
+
return `mock-fb-${prop.toLowerCase()}`;
|
|
60
|
+
}
|
|
38
61
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
}
|
|
63
|
+
export async function hostingFetch(path, opts = {}) {
|
|
64
|
+
const context = resourceContextStorage.getStore();
|
|
65
|
+
const abortSignal = context?.abortSignal;
|
|
66
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
67
|
+
return Promise.resolve(createFirebaseOfflineMock(path, opts));
|
|
42
68
|
}
|
|
43
|
-
const
|
|
44
|
-
return
|
|
69
|
+
const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
|
|
70
|
+
return withRetry(async () => {
|
|
71
|
+
const token = await getFirebaseToken([HOSTING_SCOPE]);
|
|
72
|
+
const base = 'https://firebasehosting.googleapis.com/v1beta1';
|
|
73
|
+
const res = await fetch(`${base}${path}`, {
|
|
74
|
+
...fetchOpts,
|
|
75
|
+
headers: {
|
|
76
|
+
'Authorization': `Bearer ${token}`,
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
...(fetchOpts.headers ?? {}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = await res.text();
|
|
83
|
+
throw new Error(`Firebase Hosting API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
84
|
+
}
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
return text ? JSON.parse(text) : null;
|
|
87
|
+
}, {
|
|
88
|
+
retryable: (err) => {
|
|
89
|
+
const match = err.message.match(/→ (\d+):/);
|
|
90
|
+
const status = match ? parseInt(match[1], 10) : null;
|
|
91
|
+
return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
45
94
|
}
|
|
46
95
|
export async function cloudFetch(base, path, opts = {}) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'Authorization': `Bearer ${token}`,
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
...(opts.headers ?? {}),
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) {
|
|
57
|
-
const body = await res.text();
|
|
58
|
-
throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
96
|
+
const context = resourceContextStorage.getStore();
|
|
97
|
+
const abortSignal = context?.abortSignal;
|
|
98
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
99
|
+
return Promise.resolve(createFirebaseOfflineMock(path, opts));
|
|
59
100
|
}
|
|
60
|
-
const
|
|
61
|
-
return
|
|
101
|
+
const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
|
|
102
|
+
return withRetry(async () => {
|
|
103
|
+
const token = await getFirebaseToken([CLOUD_SCOPE]);
|
|
104
|
+
const res = await fetch(`${base}${path}`, {
|
|
105
|
+
...fetchOpts,
|
|
106
|
+
headers: {
|
|
107
|
+
'Authorization': `Bearer ${token}`,
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
...(fetchOpts.headers ?? {}),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const body = await res.text();
|
|
114
|
+
throw new Error(`GCP API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
115
|
+
}
|
|
116
|
+
const text = await res.text();
|
|
117
|
+
return text ? JSON.parse(text) : null;
|
|
118
|
+
}, {
|
|
119
|
+
retryable: (err) => {
|
|
120
|
+
const match = err.message.match(/→ (\d+):/);
|
|
121
|
+
const status = match ? parseInt(match[1], 10) : null;
|
|
122
|
+
return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
62
125
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getProjectId, hostingFetch, cloudFetch } from "./api.js";
|
|
2
|
+
export async function listFirebaseResources() {
|
|
3
|
+
const project = getProjectId();
|
|
4
|
+
const [hostRes, fnRes] = await Promise.all([
|
|
5
|
+
hostingFetch(`/projects/${project}/sites`).catch(() => ({})),
|
|
6
|
+
cloudFetch("https://cloudfunctions.googleapis.com/v2", `/projects/${project}/locations/-/functions`).catch(() => ({})),
|
|
7
|
+
]);
|
|
8
|
+
// 1. Map Hosting Sites
|
|
9
|
+
const hostingSites = (hostRes.sites ?? []).map((s) => ({
|
|
10
|
+
site: s.name.split("/").pop() ?? "unknown",
|
|
11
|
+
}));
|
|
12
|
+
// 2. Map Cloud Functions
|
|
13
|
+
const functions = (fnRes.functions ?? []).map((f) => {
|
|
14
|
+
const parts = f.name.split("/");
|
|
15
|
+
const name = parts.pop() ?? "unknown";
|
|
16
|
+
const region = parts[parts.indexOf("locations") + 1] ?? "unknown";
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
region,
|
|
20
|
+
entryPoint: f.buildConfig?.entryPoint ?? "unknown",
|
|
21
|
+
runtime: f.buildConfig?.runtime ?? "unknown",
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return { hostingSites, functions };
|
|
25
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { GoogleAuth } from 'google-auth-library';
|
|
3
3
|
import { Config } from '../../core/config.js';
|
|
4
|
+
import { withRetry } from '../../core/retry.js';
|
|
5
|
+
import { resourceContextStorage } from '../../core/context.js';
|
|
4
6
|
export function resolveGCPConfig() {
|
|
7
|
+
const isOffline = Config.isOfflineMode() || Config.isGlobalDryRun();
|
|
5
8
|
// 1. Check Config.providers.gcp
|
|
6
9
|
const gcpCfg = Config.get().providers.gcp;
|
|
7
10
|
if (gcpCfg?.serviceAccountPath) {
|
|
@@ -72,6 +75,13 @@ export function resolveGCPConfig() {
|
|
|
72
75
|
// Continue to next fallback
|
|
73
76
|
}
|
|
74
77
|
}
|
|
78
|
+
if (isOffline) {
|
|
79
|
+
return {
|
|
80
|
+
projectId: "mock-gcp-project",
|
|
81
|
+
serviceAccountPath: "/mock/sa.json",
|
|
82
|
+
region: gcpCfg?.region ?? "us-central1"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
75
85
|
throw new Error('GCP credentials not configured. Please set GCP_SA or FIREBASE_SA env var, or configure providers.gcp or providers.firebase in Config.');
|
|
76
86
|
}
|
|
77
87
|
export function getProjectId() {
|
|
@@ -82,6 +92,9 @@ export function getRegion() {
|
|
|
82
92
|
return gcpCfg?.region ?? 'us-central1';
|
|
83
93
|
}
|
|
84
94
|
export async function getGCPToken(scopes) {
|
|
95
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
96
|
+
return "mock-gcp-token";
|
|
97
|
+
}
|
|
85
98
|
const { serviceAccountPath } = resolveGCPConfig();
|
|
86
99
|
const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
|
|
87
100
|
const client = await auth.getClient();
|
|
@@ -91,21 +104,82 @@ export async function getGCPToken(scopes) {
|
|
|
91
104
|
}
|
|
92
105
|
return token.token;
|
|
93
106
|
}
|
|
107
|
+
function createGcpOfflineMock(base, path, opts) {
|
|
108
|
+
if (path.includes("/secrets/")) {
|
|
109
|
+
return {
|
|
110
|
+
payload: {
|
|
111
|
+
data: Buffer.from("mock-gcp-secret-value").toString("base64")
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (path.includes("/instances")) {
|
|
116
|
+
return {
|
|
117
|
+
status: "RUNNING",
|
|
118
|
+
id: "mock-gcp-instance-id",
|
|
119
|
+
networkInterfaces: [
|
|
120
|
+
{
|
|
121
|
+
networkIP: "10.128.0.2",
|
|
122
|
+
accessConfigs: [
|
|
123
|
+
{
|
|
124
|
+
natIP: "34.56.78.90"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (path.includes("/global/networks")) {
|
|
132
|
+
return { status: "READY", name: "mock-network" };
|
|
133
|
+
}
|
|
134
|
+
if (path.includes("/subnetworks")) {
|
|
135
|
+
return { status: "READY", name: "mock-subnetwork" };
|
|
136
|
+
}
|
|
137
|
+
// Generic fallback proxy
|
|
138
|
+
return new Proxy({}, {
|
|
139
|
+
get(target, prop) {
|
|
140
|
+
if (prop === "then")
|
|
141
|
+
return undefined;
|
|
142
|
+
if (prop === "id")
|
|
143
|
+
return "mock-gcp-id-12345";
|
|
144
|
+
if (prop === "name")
|
|
145
|
+
return "mock-gcp-name";
|
|
146
|
+
if (prop === "status" || prop === "status")
|
|
147
|
+
return "RUNNING";
|
|
148
|
+
if (prop.endsWith("s"))
|
|
149
|
+
return [];
|
|
150
|
+
return `mock-gcp-${prop.toLowerCase()}`;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
94
154
|
const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
|
|
95
155
|
export async function gcpFetch(base, path, opts = {}) {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'Authorization': `Bearer ${token}`,
|
|
101
|
-
'Content-Type': 'application/json',
|
|
102
|
-
...(opts.headers ?? {}),
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
if (!res.ok) {
|
|
106
|
-
const body = await res.text();
|
|
107
|
-
throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
156
|
+
const context = resourceContextStorage.getStore();
|
|
157
|
+
const abortSignal = context?.abortSignal;
|
|
158
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
159
|
+
return Promise.resolve(createGcpOfflineMock(base, path, opts));
|
|
108
160
|
}
|
|
109
|
-
const
|
|
110
|
-
return
|
|
161
|
+
const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
|
|
162
|
+
return withRetry(async () => {
|
|
163
|
+
const token = await getGCPToken([CLOUD_SCOPE]);
|
|
164
|
+
const res = await fetch(`${base}${path}`, {
|
|
165
|
+
...fetchOpts,
|
|
166
|
+
headers: {
|
|
167
|
+
'Authorization': `Bearer ${token}`,
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
...(fetchOpts.headers ?? {}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const body = await res.text();
|
|
174
|
+
throw new Error(`GCP API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
175
|
+
}
|
|
176
|
+
const text = await res.text();
|
|
177
|
+
return text ? JSON.parse(text) : null;
|
|
178
|
+
}, {
|
|
179
|
+
retryable: (err) => {
|
|
180
|
+
const match = err.message.match(/→ (\d+):/);
|
|
181
|
+
const status = match ? parseInt(match[1], 10) : null;
|
|
182
|
+
return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
|
|
183
|
+
}
|
|
184
|
+
});
|
|
111
185
|
}
|
|
@@ -19,6 +19,7 @@ export declare class GCPCloudDNSZoneBuilder extends BaseBuilder {
|
|
|
19
19
|
private records;
|
|
20
20
|
constructor(zoneName: string);
|
|
21
21
|
private discoverZone;
|
|
22
|
+
record(filePath: string): this;
|
|
22
23
|
record(name: string, type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "PTR" | "SRV" | "CAA" | "SPF", value: string, ttl?: number): this;
|
|
23
24
|
pointer(name: string, target: BaseBuilder | Output<string> | string): this;
|
|
24
25
|
deploy(): Promise<{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseBuilder } from "../../core/resource.js";
|
|
2
2
|
import { Output } from "../../core/output.js";
|
|
3
3
|
import { gcpFetch, getProjectId } from "./api.js";
|
|
4
|
+
import { loadRecordsFromFile } from "../../core/parser.js";
|
|
4
5
|
const DNS_BASE = "https://dns.googleapis.com";
|
|
5
6
|
function cleanZoneId(domain) {
|
|
6
7
|
return domain
|
|
@@ -57,8 +58,20 @@ export class GCPCloudDNSZoneBuilder extends BaseBuilder {
|
|
|
57
58
|
throw e;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
record(
|
|
61
|
-
|
|
61
|
+
record(nameOrPath, type, value, ttl = 300) {
|
|
62
|
+
if (arguments.length === 1 && typeof nameOrPath === "string" && (nameOrPath.endsWith(".yaml") || nameOrPath.endsWith(".yml") || nameOrPath.endsWith(".json"))) {
|
|
63
|
+
const loaded = loadRecordsFromFile(nameOrPath);
|
|
64
|
+
for (const r of loaded) {
|
|
65
|
+
this.records.push({
|
|
66
|
+
name: r.name,
|
|
67
|
+
type: r.type,
|
|
68
|
+
value: r.value,
|
|
69
|
+
ttl: r.ttl ?? 300,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
this.records.push({ name: nameOrPath, type: type, value: value, ttl });
|
|
62
75
|
return this;
|
|
63
76
|
}
|
|
64
77
|
pointer(name, target) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { test, describe, beforeEach, afterEach, mock } from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import { GoogleAuth } from "google-auth-library";
|
|
4
6
|
import { GCPCloudDNSZoneBuilder } from "./clouddns.js";
|
|
5
7
|
import { Config } from "../../core/config.js";
|
|
@@ -256,4 +258,47 @@ describe("GCPCloudDNSZoneBuilder Unit Tests", () => {
|
|
|
256
258
|
const deleteCall = fetchCalls.find((c) => c.method === "DELETE" && c.url.endsWith("/managedZones/to-delete-com"));
|
|
257
259
|
assert.ok(deleteCall);
|
|
258
260
|
});
|
|
261
|
+
test("loads records from a configuration file (YAML) successfully", async () => {
|
|
262
|
+
// 1. Zone exists
|
|
263
|
+
mockResponses["GET /managedZones/file-zone-com"] = {
|
|
264
|
+
status: 200,
|
|
265
|
+
body: { name: "file-zone-com" },
|
|
266
|
+
};
|
|
267
|
+
mockResponses["GET /managedZones/file-zone-com/rrsets"] = { status: 200, body: { rrsets: [] } };
|
|
268
|
+
mockResponses["POST /managedZones/file-zone-com/changes"] = { status: 200, body: {} };
|
|
269
|
+
// 2. Mock YAML file creation
|
|
270
|
+
const tempYamlPath = path.resolve(process.cwd(), "temp-dns-records.yaml");
|
|
271
|
+
const yamlContent = `
|
|
272
|
+
- name: www
|
|
273
|
+
type: CNAME
|
|
274
|
+
value: lb.google.com
|
|
275
|
+
- name: mail
|
|
276
|
+
type: A
|
|
277
|
+
value: 1.2.3.4
|
|
278
|
+
ttl: 600
|
|
279
|
+
`;
|
|
280
|
+
fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
|
|
281
|
+
try {
|
|
282
|
+
const builder = new GCPCloudDNSZoneBuilder("file-zone.com")
|
|
283
|
+
.record("temp-dns-records.yaml")
|
|
284
|
+
.record("api", "A", "10.0.0.9", 120); // Hybrid programmatic record!
|
|
285
|
+
const result = await builder.deploy();
|
|
286
|
+
assert.strictEqual(result.records.length, 3);
|
|
287
|
+
const wwwRec = result.records.find((r) => r.name === "www.file-zone.com.");
|
|
288
|
+
assert.ok(wwwRec);
|
|
289
|
+
assert.strictEqual(wwwRec.type, "CNAME");
|
|
290
|
+
assert.deepStrictEqual(wwwRec.rrdatas, ["lb.google.com."]);
|
|
291
|
+
const mailRec = result.records.find((r) => r.name === "mail.file-zone.com.");
|
|
292
|
+
assert.ok(mailRec);
|
|
293
|
+
assert.strictEqual(mailRec.type, "A");
|
|
294
|
+
assert.strictEqual(mailRec.ttl, 600);
|
|
295
|
+
const apiRec = result.records.find((r) => r.name === "api.file-zone.com.");
|
|
296
|
+
assert.ok(apiRec);
|
|
297
|
+
assert.strictEqual(apiRec.ttl, 120);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
if (fs.existsSync(tempYamlPath))
|
|
301
|
+
fs.unlinkSync(tempYamlPath);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
259
304
|
});
|
|
@@ -4,7 +4,9 @@ import { GCPSecretBuilder, resolveGCPEnvVars } from './secrets.js';
|
|
|
4
4
|
import { GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder } from './pubsub.js';
|
|
5
5
|
import { GCPCloudDNSZoneBuilder } from './clouddns.js';
|
|
6
6
|
import { GCPServiceAccountBuilder, GCPIAMBindingBuilder } from './iam.js';
|
|
7
|
-
|
|
7
|
+
import { GCPVMBuilder } from './vm.js';
|
|
8
|
+
import { GCPTemplateBuilder } from './template.js';
|
|
9
|
+
export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder, GCPVMBuilder, GCPTemplateBuilder };
|
|
8
10
|
export declare const GCP: {
|
|
9
11
|
CloudRun: (serviceId: string) => GCPCloudRunBuilder;
|
|
10
12
|
CloudSQL: (instanceId: string) => GCPCloudSQLBuilder;
|
|
@@ -16,4 +18,6 @@ export declare const GCP: {
|
|
|
16
18
|
Topic: (topicId: string) => GCPPubSubTopicBuilder;
|
|
17
19
|
Subscription: (subscriptionId: string) => GCPPubSubSubscriptionBuilder;
|
|
18
20
|
};
|
|
21
|
+
VM: (instanceId: string) => GCPVMBuilder;
|
|
22
|
+
Template: (instanceId: string) => GCPTemplateBuilder;
|
|
19
23
|
};
|
|
@@ -4,7 +4,9 @@ import { GCPSecretBuilder, resolveGCPEnvVars } from './secrets.js';
|
|
|
4
4
|
import { GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder } from './pubsub.js';
|
|
5
5
|
import { GCPCloudDNSZoneBuilder } from './clouddns.js';
|
|
6
6
|
import { GCPServiceAccountBuilder, GCPIAMBindingBuilder } from './iam.js';
|
|
7
|
-
|
|
7
|
+
import { GCPVMBuilder } from './vm.js';
|
|
8
|
+
import { GCPTemplateBuilder } from './template.js';
|
|
9
|
+
export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder, GCPVMBuilder, GCPTemplateBuilder };
|
|
8
10
|
export const GCP = {
|
|
9
11
|
CloudRun: (serviceId) => new GCPCloudRunBuilder(serviceId),
|
|
10
12
|
CloudSQL: (instanceId) => new GCPCloudSQLBuilder(instanceId),
|
|
@@ -16,4 +18,6 @@ export const GCP = {
|
|
|
16
18
|
Topic: (topicId) => new GCPPubSubTopicBuilder(topicId),
|
|
17
19
|
Subscription: (subscriptionId) => new GCPPubSubSubscriptionBuilder(subscriptionId),
|
|
18
20
|
},
|
|
21
|
+
VM: (instanceId) => new GCPVMBuilder(instanceId),
|
|
22
|
+
Template: (instanceId) => new GCPTemplateBuilder(instanceId),
|
|
19
23
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { gcpFetch, getProjectId } from "./api.js";
|
|
2
|
+
export async function listGcpResources() {
|
|
3
|
+
const project = getProjectId();
|
|
4
|
+
const [vmRes, sqlRes, runRes, dnsRes] = await Promise.all([
|
|
5
|
+
gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/aggregated/instances`).catch(() => ({})),
|
|
6
|
+
gcpFetch("https://sqladmin.googleapis.com", `/v1/projects/${project}/instances`).catch(() => ({})),
|
|
7
|
+
gcpFetch("https://run.googleapis.com", `/v2/projects/${project}/locations/-/services`).catch(() => ({})),
|
|
8
|
+
gcpFetch("https://dns.googleapis.com", `/dns/v1/projects/${project}/managedZones`).catch(() => ({})),
|
|
9
|
+
]);
|
|
10
|
+
// 1. Map VM Instances
|
|
11
|
+
const vms = [];
|
|
12
|
+
if (vmRes.items) {
|
|
13
|
+
for (const [zoneKey, zoneData] of Object.entries(vmRes.items)) {
|
|
14
|
+
const data = zoneData;
|
|
15
|
+
if (data.instances) {
|
|
16
|
+
const zone = zoneKey.split("/").pop() ?? zoneKey;
|
|
17
|
+
for (const inst of data.instances) {
|
|
18
|
+
const machineType = inst.machineType?.split("/").pop() ?? "unknown";
|
|
19
|
+
const ip = inst.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP ?? "no-ip";
|
|
20
|
+
vms.push({
|
|
21
|
+
name: inst.name,
|
|
22
|
+
zone,
|
|
23
|
+
machineType,
|
|
24
|
+
status: inst.status ?? "unknown",
|
|
25
|
+
ip,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 2. Map Cloud SQL Instances
|
|
32
|
+
const rdsInstances = (sqlRes.items ?? []).map((i) => ({
|
|
33
|
+
name: i.name,
|
|
34
|
+
engine: i.databaseVersion ?? "unknown",
|
|
35
|
+
tier: i.settings?.tier ?? "unknown",
|
|
36
|
+
status: i.state ?? "unknown",
|
|
37
|
+
}));
|
|
38
|
+
// 3. Map Cloud Run Services
|
|
39
|
+
const distributions = (runRes.services ?? []).map((s) => {
|
|
40
|
+
const parts = s.name.split("/");
|
|
41
|
+
const name = parts.pop() ?? "unknown";
|
|
42
|
+
const region = parts[parts.indexOf("locations") + 1] ?? "unknown";
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
region,
|
|
46
|
+
url: s.uri ?? "no-url",
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
// 4. Map Cloud DNS Zones
|
|
50
|
+
const hostedZones = (dnsRes.managedZones ?? []).map((z) => ({
|
|
51
|
+
name: z.name,
|
|
52
|
+
dnsName: z.dnsName ?? "",
|
|
53
|
+
}));
|
|
54
|
+
return { vms, rdsInstances, distributions, hostedZones };
|
|
55
|
+
}
|
|
@@ -23,7 +23,7 @@ export class GCPSecretBuilder extends BaseBuilder {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
catch (err) {
|
|
26
|
-
|
|
26
|
+
console.warn(` ⚠️ Could not fetch latest version of secret "${secretId}": ${err.message}`);
|
|
27
27
|
}
|
|
28
28
|
return secret;
|
|
29
29
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
export declare class GCPTemplateBuilder extends BaseBuilder {
|
|
4
|
+
readonly out: {
|
|
5
|
+
imageId: Output<string>;
|
|
6
|
+
};
|
|
7
|
+
private _baseImage;
|
|
8
|
+
private _machineType;
|
|
9
|
+
private _zone;
|
|
10
|
+
private _network;
|
|
11
|
+
private _sshKeys;
|
|
12
|
+
private _provision;
|
|
13
|
+
constructor(name: string);
|
|
14
|
+
baseImage(img: string): this;
|
|
15
|
+
machineType(type: string): this;
|
|
16
|
+
zone(z: string): this;
|
|
17
|
+
network(netPath: string): this;
|
|
18
|
+
sshKey(keys: string | string[]): this;
|
|
19
|
+
provision(...playbookPaths: (string | string[])[]): this;
|
|
20
|
+
private discoverImage;
|
|
21
|
+
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
22
|
+
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
23
|
+
deploy(): Promise<{
|
|
24
|
+
name: string;
|
|
25
|
+
imageId: string;
|
|
26
|
+
}>;
|
|
27
|
+
destroy(): Promise<{
|
|
28
|
+
destroyed: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
destroyed: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|