puls-dev 0.2.6 → 0.2.8
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 +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/decorators.d.ts +2 -0
- package/dist/core/decorators.js +48 -16
- 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/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/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +105 -0
- package/dist/core/resource.d.ts +16 -0
- package/dist/core/resource.js +44 -0
- package/dist/core/secret.d.ts +40 -0
- package/dist/core/secret.js +95 -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 +50 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/aws/ec2.d.ts +48 -0
- package/dist/providers/aws/ec2.js +297 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +279 -0
- package/dist/providers/aws/index.d.ts +2 -0
- package/dist/providers/aws/index.js +2 -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/do/api.d.ts +1 -1
- package/dist/providers/do/api.js +2 -1
- 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 +132 -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/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 +3 -1
- package/dist/providers/gcp/index.js +3 -1
- package/dist/providers/gcp/vm.d.ts +45 -0
- package/dist/providers/gcp/vm.js +332 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/hash.d.ts +3 -0
- package/dist/providers/proxmox/hash.js +46 -0
- package/dist/providers/proxmox/vm.d.ts +8 -7
- package/dist/providers/proxmox/vm.js +126 -106
- package/dist/providers/proxmox/vm.test.js +224 -0
- package/package.json +3 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Strips comments from a YAML/JSON line while preserving comment hashes inside quotes.
|
|
5
|
+
*/
|
|
6
|
+
function stripComments(line) {
|
|
7
|
+
const hashIdx = line.indexOf("#");
|
|
8
|
+
if (hashIdx < 0)
|
|
9
|
+
return line;
|
|
10
|
+
let inQuotes = false;
|
|
11
|
+
let quoteChar = "";
|
|
12
|
+
for (let i = 0; i < hashIdx; i++) {
|
|
13
|
+
const char = line[i];
|
|
14
|
+
if (char === '"' || char === "'") {
|
|
15
|
+
if (!inQuotes) {
|
|
16
|
+
inQuotes = true;
|
|
17
|
+
quoteChar = char;
|
|
18
|
+
}
|
|
19
|
+
else if (char === quoteChar) {
|
|
20
|
+
inQuotes = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (inQuotes) {
|
|
25
|
+
return line;
|
|
26
|
+
}
|
|
27
|
+
return line.slice(0, hashIdx);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Robust, zero-dependency, indentation-aware YAML parser.
|
|
31
|
+
* Parses sequences of key-value maps and nested string arrays.
|
|
32
|
+
*/
|
|
33
|
+
export function parseYaml(content) {
|
|
34
|
+
const list = [];
|
|
35
|
+
const lines = content.split(/\r?\n/);
|
|
36
|
+
let currentItem = null;
|
|
37
|
+
let activeKey = null;
|
|
38
|
+
let rootIndent = 0;
|
|
39
|
+
for (let line of lines) {
|
|
40
|
+
line = stripComments(line);
|
|
41
|
+
if (!line.trim())
|
|
42
|
+
continue;
|
|
43
|
+
const leadingSpaces = line.length - line.trimStart().length;
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
// Check if it's a bullet item list entry
|
|
46
|
+
if (trimmed.startsWith("-")) {
|
|
47
|
+
const rest = trimmed.slice(1).trim();
|
|
48
|
+
// If it is indented more than the root item and does not match a key-value pattern,
|
|
49
|
+
// treat it as an array item under the active key.
|
|
50
|
+
const isRestKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(rest);
|
|
51
|
+
if (leadingSpaces > rootIndent && !isRestKeyValue) {
|
|
52
|
+
if (currentItem && activeKey) {
|
|
53
|
+
if (!Array.isArray(currentItem[activeKey])) {
|
|
54
|
+
currentItem[activeKey] = [];
|
|
55
|
+
}
|
|
56
|
+
let val = rest;
|
|
57
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
58
|
+
val = val.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
currentItem[activeKey].push(val);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Otherwise, it represents a new item at the root list level
|
|
65
|
+
if (currentItem) {
|
|
66
|
+
list.push(currentItem);
|
|
67
|
+
}
|
|
68
|
+
currentItem = {};
|
|
69
|
+
rootIndent = leadingSpaces;
|
|
70
|
+
activeKey = null;
|
|
71
|
+
if (rest) {
|
|
72
|
+
const colonIdx = rest.indexOf(":");
|
|
73
|
+
if (colonIdx > 0 && isRestKeyValue) {
|
|
74
|
+
const key = rest.slice(0, colonIdx).trim();
|
|
75
|
+
let value = rest.slice(colonIdx + 1).trim();
|
|
76
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
77
|
+
value = value.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
// Convert to number if numeric
|
|
80
|
+
if (/^\d+$/.test(value)) {
|
|
81
|
+
currentItem[key] = parseInt(value, 10);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
currentItem[key] = value;
|
|
85
|
+
}
|
|
86
|
+
activeKey = key;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Key-value pair or list start
|
|
92
|
+
const isKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(trimmed);
|
|
93
|
+
if (isKeyValue && currentItem) {
|
|
94
|
+
const colonIdx = trimmed.indexOf(":");
|
|
95
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
96
|
+
let value = trimmed.slice(colonIdx + 1).trim();
|
|
97
|
+
if (value === "") {
|
|
98
|
+
// Starts a nested list/array
|
|
99
|
+
currentItem[key] = [];
|
|
100
|
+
activeKey = key;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
104
|
+
value = value.slice(1, -1);
|
|
105
|
+
}
|
|
106
|
+
if (/^\d+$/.test(value)) {
|
|
107
|
+
currentItem[key] = parseInt(value, 10);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
currentItem[key] = value;
|
|
111
|
+
}
|
|
112
|
+
activeKey = key;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (currentItem) {
|
|
118
|
+
list.push(currentItem);
|
|
119
|
+
}
|
|
120
|
+
return list;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a file path relative to the current working directory,
|
|
124
|
+
* reads its content, and parses it according to its extension (.json vs .yaml/.yml).
|
|
125
|
+
*/
|
|
126
|
+
export function loadRecordsFromFile(filePath) {
|
|
127
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
128
|
+
const fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
if (ext === ".json") {
|
|
131
|
+
const parsed = JSON.parse(fileContent);
|
|
132
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
133
|
+
}
|
|
134
|
+
else if (ext === ".yaml" || ext === ".yml") {
|
|
135
|
+
return parseYaml(fileContent);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new Error(`Unsupported configuration file format: ${filePath}. Only JSON and YAML/YML files are supported.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseYaml, loadRecordsFromFile } from "./parser.js";
|
|
6
|
+
describe("YAML & JSON Config Parser", () => {
|
|
7
|
+
test("parses standard simple key-value YAML blocks correctly", () => {
|
|
8
|
+
const yaml = `
|
|
9
|
+
- name: www
|
|
10
|
+
type: CNAME
|
|
11
|
+
value: lb.google.com
|
|
12
|
+
- name: mail
|
|
13
|
+
type: A
|
|
14
|
+
value: 1.2.3.4
|
|
15
|
+
ttl: 600
|
|
16
|
+
`;
|
|
17
|
+
const result = parseYaml(yaml);
|
|
18
|
+
assert.strictEqual(result.length, 2);
|
|
19
|
+
assert.deepStrictEqual(result[0], {
|
|
20
|
+
name: "www",
|
|
21
|
+
type: "CNAME",
|
|
22
|
+
value: "lb.google.com",
|
|
23
|
+
});
|
|
24
|
+
assert.deepStrictEqual(result[1], {
|
|
25
|
+
name: "mail",
|
|
26
|
+
type: "A",
|
|
27
|
+
value: "1.2.3.4",
|
|
28
|
+
ttl: 600,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
test("ignores comments and blank lines in YAML", () => {
|
|
32
|
+
const yaml = `
|
|
33
|
+
# This is a comment at the top
|
|
34
|
+
- name: "@"
|
|
35
|
+
type: TXT # inline comment
|
|
36
|
+
value: "v=spf1 include:_spf.google.com ~all"
|
|
37
|
+
|
|
38
|
+
# Another comment with spaces
|
|
39
|
+
- name: api
|
|
40
|
+
type: CNAME
|
|
41
|
+
value: api.service.com
|
|
42
|
+
`;
|
|
43
|
+
const result = parseYaml(yaml);
|
|
44
|
+
assert.strictEqual(result.length, 2);
|
|
45
|
+
assert.strictEqual(result[0].name, "@");
|
|
46
|
+
assert.strictEqual(result[0].type, "TXT");
|
|
47
|
+
assert.strictEqual(result[0].value, "v=spf1 include:_spf.google.com ~all");
|
|
48
|
+
assert.strictEqual(result[1].name, "api");
|
|
49
|
+
});
|
|
50
|
+
test("parses nested array list values based on indentation", () => {
|
|
51
|
+
const yaml = `
|
|
52
|
+
- type: ingress
|
|
53
|
+
protocol: tcp
|
|
54
|
+
port: 80
|
|
55
|
+
sources:
|
|
56
|
+
- 0.0.0.0/0
|
|
57
|
+
- ::/0
|
|
58
|
+
- type: egress
|
|
59
|
+
protocol: tcp
|
|
60
|
+
port: all
|
|
61
|
+
destinations:
|
|
62
|
+
- 10.0.0.0/8
|
|
63
|
+
`;
|
|
64
|
+
const result = parseYaml(yaml);
|
|
65
|
+
assert.strictEqual(result.length, 2);
|
|
66
|
+
assert.deepStrictEqual(result[0], {
|
|
67
|
+
type: "ingress",
|
|
68
|
+
protocol: "tcp",
|
|
69
|
+
port: 80,
|
|
70
|
+
sources: ["0.0.0.0/0", "::/0"],
|
|
71
|
+
});
|
|
72
|
+
assert.deepStrictEqual(result[1], {
|
|
73
|
+
type: "egress",
|
|
74
|
+
protocol: "tcp",
|
|
75
|
+
port: "all",
|
|
76
|
+
destinations: ["10.0.0.0/8"],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
test("loads from JSON and YAML files successfully", () => {
|
|
80
|
+
const tempJsonPath = path.resolve(process.cwd(), "temp-test-records.json");
|
|
81
|
+
const tempYamlPath = path.resolve(process.cwd(), "temp-test-records.yaml");
|
|
82
|
+
const jsonContent = JSON.stringify([
|
|
83
|
+
{ name: "api", type: "CNAME", value: "lb.com" }
|
|
84
|
+
]);
|
|
85
|
+
const yamlContent = `
|
|
86
|
+
- name: web
|
|
87
|
+
type: A
|
|
88
|
+
value: 5.6.7.8
|
|
89
|
+
`;
|
|
90
|
+
// Write temp files
|
|
91
|
+
fs.writeFileSync(tempJsonPath, jsonContent, "utf-8");
|
|
92
|
+
fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
|
|
93
|
+
try {
|
|
94
|
+
const parsedJson = loadRecordsFromFile("temp-test-records.json");
|
|
95
|
+
assert.strictEqual(parsedJson.length, 1);
|
|
96
|
+
assert.deepStrictEqual(parsedJson[0], {
|
|
97
|
+
name: "api",
|
|
98
|
+
type: "CNAME",
|
|
99
|
+
value: "lb.com",
|
|
100
|
+
});
|
|
101
|
+
const parsedYaml = loadRecordsFromFile("temp-test-records.yaml");
|
|
102
|
+
assert.strictEqual(parsedYaml.length, 1);
|
|
103
|
+
assert.deepStrictEqual(parsedYaml[0], {
|
|
104
|
+
name: "web",
|
|
105
|
+
type: "A",
|
|
106
|
+
value: "5.6.7.8",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
// Clean up temp files
|
|
111
|
+
if (fs.existsSync(tempJsonPath))
|
|
112
|
+
fs.unlinkSync(tempJsonPath);
|
|
113
|
+
if (fs.existsSync(tempYamlPath))
|
|
114
|
+
fs.unlinkSync(tempYamlPath);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function checkPort(ip: string, port: number): Promise<boolean>;
|
|
2
|
+
export declare function runAnsible(ip: string, user: string, sshKeys: string | string[] | undefined, playbook: string): Promise<void>;
|
|
3
|
+
export declare function runPuppet(ip: string, user: string, sshKeys: string | string[] | undefined, manifest: string): Promise<void>;
|
|
4
|
+
export declare function runProvisioner(ip: string, user: string, sshKeys: string | string[] | undefined, scriptPath: string): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
export function checkPort(ip, port) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const socket = new net.Socket();
|
|
7
|
+
socket.setTimeout(2000);
|
|
8
|
+
socket.on("connect", () => {
|
|
9
|
+
socket.destroy();
|
|
10
|
+
resolve(true);
|
|
11
|
+
});
|
|
12
|
+
socket.on("error", () => {
|
|
13
|
+
resolve(false);
|
|
14
|
+
});
|
|
15
|
+
socket.on("timeout", () => {
|
|
16
|
+
socket.destroy();
|
|
17
|
+
resolve(false);
|
|
18
|
+
});
|
|
19
|
+
socket.connect(port, ip);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function resolveSshKeyPath(sshKeys) {
|
|
23
|
+
const keyInput = Array.isArray(sshKeys) ? null : sshKeys;
|
|
24
|
+
return (keyInput &&
|
|
25
|
+
!keyInput.startsWith("ssh-") &&
|
|
26
|
+
!keyInput.startsWith("ecdsa-") &&
|
|
27
|
+
!keyInput.startsWith("sk-")
|
|
28
|
+
? keyInput.replace(/\.pub$/, "")
|
|
29
|
+
: `${homedir()}/.ssh/id_ed25519`).replace(/^~/, homedir());
|
|
30
|
+
}
|
|
31
|
+
export function runAnsible(ip, user, sshKeys, playbook) {
|
|
32
|
+
console.log(` 🔧 Running Ansible: ${playbook} → ${ip}`);
|
|
33
|
+
const keyPath = resolveSshKeyPath(sshKeys);
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const proc = spawn("ansible-playbook", [
|
|
36
|
+
playbook,
|
|
37
|
+
"-i",
|
|
38
|
+
`${ip},`,
|
|
39
|
+
"-u",
|
|
40
|
+
user,
|
|
41
|
+
"--private-key",
|
|
42
|
+
keyPath,
|
|
43
|
+
"--ssh-extra-args",
|
|
44
|
+
"-o StrictHostKeyChecking=no -o ConnectTimeout=30",
|
|
45
|
+
], { stdio: "inherit" });
|
|
46
|
+
proc.on("close", (code) => {
|
|
47
|
+
if (code === 0) {
|
|
48
|
+
console.log(` ✅ Provisioning complete`);
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
reject(new Error(`ansible-playbook exited with code ${code}`));
|
|
53
|
+
});
|
|
54
|
+
proc.on("error", (err) => reject(new Error(`Failed to run ansible-playbook: ${err.message}`)));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function runPuppet(ip, user, sshKeys, manifest) {
|
|
58
|
+
console.log(` 🔧 Applying Puppet manifest: ${manifest} → ${ip}`);
|
|
59
|
+
const keyPath = resolveSshKeyPath(sshKeys);
|
|
60
|
+
// Copy manifest then apply it
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const scp = spawn("scp", [
|
|
63
|
+
"-i",
|
|
64
|
+
keyPath,
|
|
65
|
+
"-o",
|
|
66
|
+
"StrictHostKeyChecking=no",
|
|
67
|
+
manifest,
|
|
68
|
+
`${user}@${ip}:/tmp/manifest.pp`,
|
|
69
|
+
], { stdio: "inherit" });
|
|
70
|
+
scp.on("close", (code) => {
|
|
71
|
+
if (code !== 0) {
|
|
72
|
+
reject(new Error(`scp exited with code ${code}`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const puppet = spawn("ssh", [
|
|
76
|
+
"-i",
|
|
77
|
+
keyPath,
|
|
78
|
+
"-o",
|
|
79
|
+
"StrictHostKeyChecking=no",
|
|
80
|
+
`${user}@${ip}`,
|
|
81
|
+
"puppet apply /tmp/manifest.pp",
|
|
82
|
+
], { stdio: "inherit" });
|
|
83
|
+
puppet.on("close", (c) => {
|
|
84
|
+
if (c === 0) {
|
|
85
|
+
console.log(` ✅ Provisioning complete`);
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
reject(new Error(`puppet apply exited with code ${c}`));
|
|
90
|
+
});
|
|
91
|
+
puppet.on("error", (err) => reject(new Error(`Failed to run puppet: ${err.message}`)));
|
|
92
|
+
});
|
|
93
|
+
scp.on("error", (err) => reject(new Error(`Failed to run scp: ${err.message}`)));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function runProvisioner(ip, user, sshKeys, scriptPath) {
|
|
97
|
+
const ext = scriptPath.split(".").pop()?.toLowerCase();
|
|
98
|
+
if (ext === "sh") {
|
|
99
|
+
throw new Error(`Shell script provisioning (.sh) is no longer supported. ` +
|
|
100
|
+
`Please migrate "${scriptPath}" to an Ansible playbook (.yaml/.yml).`);
|
|
101
|
+
}
|
|
102
|
+
if (ext === "pp")
|
|
103
|
+
return runPuppet(ip, user, sshKeys, scriptPath);
|
|
104
|
+
return runAnsible(ip, user, sshKeys, scriptPath); // .yml / .yaml
|
|
105
|
+
}
|
package/dist/core/resource.d.ts
CHANGED
|
@@ -4,9 +4,25 @@ export declare abstract class BaseBuilder {
|
|
|
4
4
|
protected localDryRun: boolean | null;
|
|
5
5
|
protected discoveryPromise: Promise<any>;
|
|
6
6
|
protected sidecars: BaseBuilder[];
|
|
7
|
+
private _beforeDeployHooks;
|
|
8
|
+
private _afterDeployHooks;
|
|
9
|
+
private _beforeDestroyHooks;
|
|
10
|
+
private _afterDestroyHooks;
|
|
7
11
|
constructor(name: string);
|
|
8
12
|
protect(): this;
|
|
9
13
|
dryRun(enabled?: boolean): this;
|
|
14
|
+
beforeDeploy(callback: () => Promise<void> | void): this;
|
|
15
|
+
afterDeploy(callback: (result: any) => Promise<void> | void): this;
|
|
16
|
+
beforeDestroy(callback: () => Promise<void> | void): this;
|
|
17
|
+
afterDestroy(callback: (result: any) => Promise<void> | void): this;
|
|
18
|
+
/** @internal */
|
|
19
|
+
_runBeforeDeploy(): Promise<void>;
|
|
20
|
+
/** @internal */
|
|
21
|
+
_runAfterDeploy(result: any): Promise<void>;
|
|
22
|
+
/** @internal */
|
|
23
|
+
_runBeforeDestroy(): Promise<void>;
|
|
24
|
+
/** @internal */
|
|
25
|
+
_runAfterDestroy(result: any): Promise<void>;
|
|
10
26
|
protected isDryRunActive(): boolean;
|
|
11
27
|
protected checkProtection(hasChanges: boolean): Promise<boolean>;
|
|
12
28
|
protected waitFor(label: string, condition: () => Promise<boolean>, opts?: {
|
package/dist/core/resource.js
CHANGED
|
@@ -5,6 +5,10 @@ export class BaseBuilder {
|
|
|
5
5
|
localDryRun = null;
|
|
6
6
|
discoveryPromise;
|
|
7
7
|
sidecars = [];
|
|
8
|
+
_beforeDeployHooks = [];
|
|
9
|
+
_afterDeployHooks = [];
|
|
10
|
+
_beforeDestroyHooks = [];
|
|
11
|
+
_afterDestroyHooks = [];
|
|
8
12
|
constructor(name) {
|
|
9
13
|
this.name = name;
|
|
10
14
|
}
|
|
@@ -16,6 +20,46 @@ export class BaseBuilder {
|
|
|
16
20
|
this.localDryRun = enabled;
|
|
17
21
|
return this;
|
|
18
22
|
}
|
|
23
|
+
beforeDeploy(callback) {
|
|
24
|
+
this._beforeDeployHooks.push(callback);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
afterDeploy(callback) {
|
|
28
|
+
this._afterDeployHooks.push(callback);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
beforeDestroy(callback) {
|
|
32
|
+
this._beforeDestroyHooks.push(callback);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
afterDestroy(callback) {
|
|
36
|
+
this._afterDestroyHooks.push(callback);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/** @internal */
|
|
40
|
+
async _runBeforeDeploy() {
|
|
41
|
+
for (const cb of this._beforeDeployHooks) {
|
|
42
|
+
await cb();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** @internal */
|
|
46
|
+
async _runAfterDeploy(result) {
|
|
47
|
+
for (const cb of this._afterDeployHooks) {
|
|
48
|
+
await cb(result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
async _runBeforeDestroy() {
|
|
53
|
+
for (const cb of this._beforeDestroyHooks) {
|
|
54
|
+
await cb();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** @internal */
|
|
58
|
+
async _runAfterDestroy(result) {
|
|
59
|
+
for (const cb of this._afterDestroyHooks) {
|
|
60
|
+
await cb(result);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
19
63
|
isDryRunActive() {
|
|
20
64
|
return this.localDryRun !== null
|
|
21
65
|
? this.localDryRun
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Output } from "./output.js";
|
|
2
|
+
/**
|
|
3
|
+
* Secret represents a lazy, secure credential that is fetched asynchronously
|
|
4
|
+
* at deployment time instead of during the eager construction phase.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Secret extends Output<string> {
|
|
7
|
+
readonly secretName: string;
|
|
8
|
+
constructor(secretName: string, fetcher: () => Promise<string>);
|
|
9
|
+
private startFetching;
|
|
10
|
+
/**
|
|
11
|
+
* Helper method to seamlessly unpack either a static string, a standard Output, or a Secret.
|
|
12
|
+
* Call this within resource builder deploy() methods.
|
|
13
|
+
*/
|
|
14
|
+
static resolve(val: string | Output<string> | Secret | any): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetches a secret from a local environment variable.
|
|
17
|
+
*/
|
|
18
|
+
static env(envName: string, fallback?: string): Secret;
|
|
19
|
+
/**
|
|
20
|
+
* Fetches a secret from AWS Secrets Manager.
|
|
21
|
+
* Uses dynamic imports so AWS SDK is only loaded if this helper is actually called.
|
|
22
|
+
*/
|
|
23
|
+
static aws(secretId: string, options?: {
|
|
24
|
+
region?: string;
|
|
25
|
+
}): Secret;
|
|
26
|
+
/**
|
|
27
|
+
* Fetches a parameter from AWS SSM Parameter Store.
|
|
28
|
+
* Uses dynamic imports so AWS SDK is only loaded if this helper is actually called.
|
|
29
|
+
*/
|
|
30
|
+
static ssm(parameterName: string, options?: {
|
|
31
|
+
region?: string;
|
|
32
|
+
}): Secret;
|
|
33
|
+
/**
|
|
34
|
+
* Fetches a secret from GCP Secret Manager.
|
|
35
|
+
* Uses dynamic imports so GCP config/fetch tools are only loaded if this helper is actually called.
|
|
36
|
+
*/
|
|
37
|
+
static gcp(secretId: string, options?: {
|
|
38
|
+
projectId?: string;
|
|
39
|
+
}): Secret;
|
|
40
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Output } from "./output.js";
|
|
2
|
+
import { Config } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Secret represents a lazy, secure credential that is fetched asynchronously
|
|
5
|
+
* at deployment time instead of during the eager construction phase.
|
|
6
|
+
*/
|
|
7
|
+
export class Secret extends Output {
|
|
8
|
+
secretName;
|
|
9
|
+
constructor(secretName, fetcher) {
|
|
10
|
+
super();
|
|
11
|
+
this.secretName = secretName;
|
|
12
|
+
this.startFetching(fetcher);
|
|
13
|
+
}
|
|
14
|
+
async startFetching(fetcher) {
|
|
15
|
+
try {
|
|
16
|
+
if (Config.isGlobalDryRun()) {
|
|
17
|
+
// Resolve immediately to a secure placeholder during dry-run testing
|
|
18
|
+
this.resolve(`[SECRET:${this.secretName}]`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const val = await fetcher();
|
|
22
|
+
this.resolve(val);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
this.reject(err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Helper method to seamlessly unpack either a static string, a standard Output, or a Secret.
|
|
30
|
+
* Call this within resource builder deploy() methods.
|
|
31
|
+
*/
|
|
32
|
+
static async resolve(val) {
|
|
33
|
+
if (val instanceof Output) {
|
|
34
|
+
return await val.get();
|
|
35
|
+
}
|
|
36
|
+
return val;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetches a secret from a local environment variable.
|
|
40
|
+
*/
|
|
41
|
+
static env(envName, fallback) {
|
|
42
|
+
return new Secret(envName, async () => {
|
|
43
|
+
const val = process.env[envName] ?? fallback;
|
|
44
|
+
if (val === undefined) {
|
|
45
|
+
throw new Error(`Environment secret "${envName}" is not set and has no fallback.`);
|
|
46
|
+
}
|
|
47
|
+
return val;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetches a secret from AWS Secrets Manager.
|
|
52
|
+
* Uses dynamic imports so AWS SDK is only loaded if this helper is actually called.
|
|
53
|
+
*/
|
|
54
|
+
static aws(secretId, options) {
|
|
55
|
+
return new Secret(secretId, async () => {
|
|
56
|
+
const { SecretsManagerClient, GetSecretValueCommand } = await import("@aws-sdk/client-secrets-manager");
|
|
57
|
+
const client = new SecretsManagerClient({ region: options?.region ?? process.env.AWS_REGION ?? "us-east-1" });
|
|
58
|
+
const result = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
|
|
59
|
+
if (!result.SecretString) {
|
|
60
|
+
throw new Error(`AWS Secret "${secretId}" is empty or not a string.`);
|
|
61
|
+
}
|
|
62
|
+
return result.SecretString;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fetches a parameter from AWS SSM Parameter Store.
|
|
67
|
+
* Uses dynamic imports so AWS SDK is only loaded if this helper is actually called.
|
|
68
|
+
*/
|
|
69
|
+
static ssm(parameterName, options) {
|
|
70
|
+
return new Secret(parameterName, async () => {
|
|
71
|
+
const { SSMClient, GetParameterCommand } = await import("@aws-sdk/client-ssm");
|
|
72
|
+
const client = new SSMClient({ region: options?.region ?? process.env.AWS_REGION ?? "us-east-1" });
|
|
73
|
+
const result = await client.send(new GetParameterCommand({ Name: parameterName, WithDecryption: true }));
|
|
74
|
+
if (!result.Parameter?.Value) {
|
|
75
|
+
throw new Error(`AWS SSM Parameter "${parameterName}" is empty.`);
|
|
76
|
+
}
|
|
77
|
+
return result.Parameter.Value;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetches a secret from GCP Secret Manager.
|
|
82
|
+
* Uses dynamic imports so GCP config/fetch tools are only loaded if this helper is actually called.
|
|
83
|
+
*/
|
|
84
|
+
static gcp(secretId, options) {
|
|
85
|
+
return new Secret(secretId, async () => {
|
|
86
|
+
const { gcpFetch, getProjectId } = await import("../providers/gcp/api.js");
|
|
87
|
+
const project = options?.projectId ?? getProjectId();
|
|
88
|
+
const payload = await gcpFetch("https://secretmanager.googleapis.com", `/v1/projects/${project}/secrets/${secretId}/versions/latest:access`);
|
|
89
|
+
if (!payload?.payload?.data) {
|
|
90
|
+
throw new Error(`GCP Secret "${secretId}" payload is empty.`);
|
|
91
|
+
}
|
|
92
|
+
return Buffer.from(payload.payload.data, "base64").toString("utf8");
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|