@katmer/core 0.0.3
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 -0
- package/cli/katmer.js +28 -0
- package/cli/run.ts +16 -0
- package/index.ts +5 -0
- package/lib/config.ts +82 -0
- package/lib/interfaces/config.interface.ts +113 -0
- package/lib/interfaces/executor.interface.ts +13 -0
- package/lib/interfaces/module.interface.ts +170 -0
- package/lib/interfaces/provider.interface.ts +214 -0
- package/lib/interfaces/task.interface.ts +100 -0
- package/lib/katmer.ts +126 -0
- package/lib/lookup/env.lookup.ts +13 -0
- package/lib/lookup/file.lookup.ts +23 -0
- package/lib/lookup/index.ts +46 -0
- package/lib/lookup/url.lookup.ts +21 -0
- package/lib/lookup/var.lookup.ts +13 -0
- package/lib/module.ts +560 -0
- package/lib/module_registry.ts +64 -0
- package/lib/modules/apt-repository/apt-repository.module.ts +435 -0
- package/lib/modules/apt-repository/apt-sources-list.ts +363 -0
- package/lib/modules/apt.module.ts +546 -0
- package/lib/modules/archive.module.ts +280 -0
- package/lib/modules/become.module.ts +119 -0
- package/lib/modules/copy.module.ts +807 -0
- package/lib/modules/cron.module.ts +541 -0
- package/lib/modules/debug.module.ts +231 -0
- package/lib/modules/gather_facts.module.ts +605 -0
- package/lib/modules/git.module.ts +243 -0
- package/lib/modules/hostname.module.ts +213 -0
- package/lib/modules/http/http.curl.module.ts +342 -0
- package/lib/modules/http/http.local.module.ts +253 -0
- package/lib/modules/http/http.module.ts +298 -0
- package/lib/modules/index.ts +14 -0
- package/lib/modules/package.module.ts +283 -0
- package/lib/modules/script.module.ts +121 -0
- package/lib/modules/set_fact.module.ts +171 -0
- package/lib/modules/systemd_service.module.ts +373 -0
- package/lib/modules/template.module.ts +478 -0
- package/lib/providers/local.provider.ts +336 -0
- package/lib/providers/provider_response.ts +20 -0
- package/lib/providers/ssh/ssh.provider.ts +420 -0
- package/lib/providers/ssh/ssh.utils.ts +31 -0
- package/lib/schemas/katmer_config.schema.json +358 -0
- package/lib/target_resolver.ts +298 -0
- package/lib/task/controls/environment.control.ts +42 -0
- package/lib/task/controls/index.ts +13 -0
- package/lib/task/controls/loop.control.ts +89 -0
- package/lib/task/controls/register.control.ts +23 -0
- package/lib/task/controls/until.control.ts +64 -0
- package/lib/task/controls/when.control.ts +25 -0
- package/lib/task/task.ts +225 -0
- package/lib/utils/ajv.utils.ts +24 -0
- package/lib/utils/cls.ts +4 -0
- package/lib/utils/datetime.utils.ts +15 -0
- package/lib/utils/errors.ts +25 -0
- package/lib/utils/execute-shell.ts +116 -0
- package/lib/utils/file.utils.ts +68 -0
- package/lib/utils/http.utils.ts +10 -0
- package/lib/utils/json.utils.ts +15 -0
- package/lib/utils/number.utils.ts +9 -0
- package/lib/utils/object.utils.ts +11 -0
- package/lib/utils/os.utils.ts +31 -0
- package/lib/utils/path.utils.ts +9 -0
- package/lib/utils/renderer/render_functions.ts +3 -0
- package/lib/utils/renderer/renderer.ts +89 -0
- package/lib/utils/renderer/twig.ts +191 -0
- package/lib/utils/string.utils.ts +33 -0
- package/lib/utils/typed-event-emitter.ts +26 -0
- package/lib/utils/unix.utils.ts +91 -0
- package/lib/utils/windows.utils.ts +92 -0
- package/package.json +67 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://katmer.dev/schemas/katmer-config.schema.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"title": "Katmer Configuration Schema",
|
|
5
|
+
"description": "Defines the structure of a Katmer configuration file used to describe hosts, groups, and logging settings.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["targets"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Optional JSON Schema identifier to explicitly declare the schema version or custom dialect used by this configuration.",
|
|
13
|
+
"examples": [
|
|
14
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
15
|
+
"https://katmer.dev/schemas/katmer-config.schema.json"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"$id": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Optional unique identifier (URI) for this configuration instance, useful for referencing or composition.",
|
|
21
|
+
"examples": [
|
|
22
|
+
"https://example.com/envs/prod.json",
|
|
23
|
+
"file:///etc/katmer/katmer.config.json"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"cwd": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Working directory in which Katmer operations will be executed. Defaults to the current directory if omitted.",
|
|
29
|
+
"examples": ["./deploy", "/opt/katmer", "/home/admin/katmer-tasks"]
|
|
30
|
+
},
|
|
31
|
+
"logging": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Configuration options for logging.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"dir": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Directory path where log files should be stored.",
|
|
38
|
+
"examples": ["./logs", "/var/log/katmer"]
|
|
39
|
+
},
|
|
40
|
+
"level": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["trace", "debug", "info", "warn", "error", "silent"],
|
|
43
|
+
"description": "Defines the verbosity level of logs.",
|
|
44
|
+
"examples": ["info", "debug"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"examples": [
|
|
49
|
+
{
|
|
50
|
+
"dir": "./logs",
|
|
51
|
+
"level": "debug"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"include": {
|
|
56
|
+
"description": "An optional file or remote URL to include additional configuration from.",
|
|
57
|
+
"anyOf": [
|
|
58
|
+
{
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "A local or remote file path to another Katmer configuration.",
|
|
61
|
+
"examples": ["./common.yml", "https://katmer.dev/configs/base.json"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["url"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"url": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "URL pointing to an external Katmer configuration file.",
|
|
71
|
+
"examples": ["https://katmer.dev/configs/shared.json"]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"examples": [{ "url": "https://katmer.dev/configs/base.json" }]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"targets": {
|
|
79
|
+
"description": "Top-level mapping of named target groups. Each target defines one or more host groups.",
|
|
80
|
+
"anyOf": [
|
|
81
|
+
{
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"hosts": {
|
|
85
|
+
"$ref": "#/$defs/KatmerHostsDefinition",
|
|
86
|
+
"description": "Map of host definitions belonging to this group."
|
|
87
|
+
},
|
|
88
|
+
"variables": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"description": "Arbitrary data variables merged into ctx.variables for all hosts in this (root) group.",
|
|
91
|
+
"additionalProperties": true,
|
|
92
|
+
"examples": [{ "env": "prod", "app_dir": "/opt/myapp" }]
|
|
93
|
+
},
|
|
94
|
+
"environment": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"description": "Key/value pairs exported as environment variables for all hosts in this (root) group.",
|
|
97
|
+
"additionalProperties": {
|
|
98
|
+
"type": ["string", "number", "boolean"]
|
|
99
|
+
},
|
|
100
|
+
"examples": [
|
|
101
|
+
{ "NODE_ENV": "production", "HTTP_PROXY": "http://proxy:8080" }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "object",
|
|
108
|
+
"patternProperties": {
|
|
109
|
+
"^[a-zA-Z0-9_.:-]+$": { "$ref": "#/$defs/KatmerHostGroup" }
|
|
110
|
+
},
|
|
111
|
+
"description": "Named target groups, where each key corresponds to a group identifier."
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"examples": [
|
|
115
|
+
{
|
|
116
|
+
"production": {
|
|
117
|
+
"variables": {
|
|
118
|
+
"env": "prod",
|
|
119
|
+
"app_dir": "/opt/myapp",
|
|
120
|
+
"release": "2024-10-18"
|
|
121
|
+
},
|
|
122
|
+
"environment": {
|
|
123
|
+
"NODE_ENV": "production",
|
|
124
|
+
"HTTP_PROXY": "http://proxy.internal:8080"
|
|
125
|
+
},
|
|
126
|
+
"hosts": {
|
|
127
|
+
"app-server-1": {
|
|
128
|
+
"connection": "ssh",
|
|
129
|
+
"hostname": "10.0.0.11",
|
|
130
|
+
"username": "deploy",
|
|
131
|
+
"private_key": "~/.ssh/id_rsa"
|
|
132
|
+
},
|
|
133
|
+
"app-server-2": {
|
|
134
|
+
"connection": "ssh",
|
|
135
|
+
"hostname": "10.0.0.12",
|
|
136
|
+
"username": "deploy",
|
|
137
|
+
"private_key": "~/.ssh/id_rsa"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"children": {
|
|
141
|
+
"db": {
|
|
142
|
+
"variables": { "pg_version": 15 },
|
|
143
|
+
"environment": { "PGTZ": "UTC" },
|
|
144
|
+
"hosts": {
|
|
145
|
+
"db1": {
|
|
146
|
+
"connection": "ssh",
|
|
147
|
+
"hostname": "10.0.0.20",
|
|
148
|
+
"username": "postgres",
|
|
149
|
+
"private_key": "~/.ssh/id_rsa"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"local": {
|
|
156
|
+
"hosts": {
|
|
157
|
+
"localhost": { "connection": "local" }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
"$defs": {
|
|
166
|
+
"KatmerHostGroup": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"description": "A collection of hosts and/or nested child groups. Groups can inherit settings from parent groups.",
|
|
169
|
+
"properties": {
|
|
170
|
+
"hosts": {
|
|
171
|
+
"$ref": "#/$defs/KatmerHostsDefinition",
|
|
172
|
+
"description": "Map of host definitions belonging to this group."
|
|
173
|
+
},
|
|
174
|
+
"variables": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"description": "Arbitrary data variables merged into ctx.variables for all hosts in this group.",
|
|
177
|
+
"additionalProperties": true,
|
|
178
|
+
"examples": [{ "env": "prod", "app_dir": "/opt/myapp" }]
|
|
179
|
+
},
|
|
180
|
+
"environment": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"description": "Key/value pairs exported as environment variables for all hosts in this group.",
|
|
183
|
+
"additionalProperties": { "type": ["string", "number", "boolean"] },
|
|
184
|
+
"examples": [
|
|
185
|
+
{ "NODE_ENV": "production", "NO_PROXY": "localhost,127.0.0.1" }
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
"children": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"description": "Nested child groups within this host group. Each child can be a group or null (for disabling inheritance).",
|
|
191
|
+
"patternProperties": {
|
|
192
|
+
"^[a-zA-Z0-9_.:-]+$": {
|
|
193
|
+
"oneOf": [
|
|
194
|
+
{ "$ref": "#/$defs/KatmerHostGroup" },
|
|
195
|
+
{ "type": "null" }
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"examples": [
|
|
200
|
+
{
|
|
201
|
+
"backend": {
|
|
202
|
+
"hosts": {
|
|
203
|
+
"api1": {
|
|
204
|
+
"connection": "ssh",
|
|
205
|
+
"hostname": "192.168.10.12",
|
|
206
|
+
"username": "ubuntu"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"examples": [
|
|
215
|
+
{
|
|
216
|
+
"variables": { "env": "staging" },
|
|
217
|
+
"environment": { "NODE_ENV": "staging" },
|
|
218
|
+
"hosts": {
|
|
219
|
+
"node1": { "connection": "ssh", "hostname": "10.0.1.1" },
|
|
220
|
+
"node2": { "connection": "ssh", "hostname": "10.0.1.2" }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
"KatmerHostsDefinition": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"description": "Mapping of host identifiers to their configuration objects.",
|
|
229
|
+
"patternProperties": {
|
|
230
|
+
"^[a-zA-Z0-9_.:-]+$": {
|
|
231
|
+
"oneOf": [
|
|
232
|
+
{ "$ref": "#/$defs/KatmerHostConfig" },
|
|
233
|
+
{
|
|
234
|
+
"type": "null",
|
|
235
|
+
"description": "Explicitly disabled or undefined host entry."
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"additionalProperties": false,
|
|
241
|
+
"examples": [
|
|
242
|
+
{
|
|
243
|
+
"web1": {
|
|
244
|
+
"connection": "ssh",
|
|
245
|
+
"hostname": "10.1.0.5",
|
|
246
|
+
"username": "root"
|
|
247
|
+
},
|
|
248
|
+
"web2": null
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
"KatmerHostConfig": {
|
|
254
|
+
"type": "object",
|
|
255
|
+
"description": "Connection configuration for an individual host.",
|
|
256
|
+
"required": ["connection"],
|
|
257
|
+
"properties": {
|
|
258
|
+
"connection": {
|
|
259
|
+
"type": "string",
|
|
260
|
+
"enum": ["ssh", "local"],
|
|
261
|
+
"description": "Connection type used for the host. Can be 'ssh' for remote or 'local' for local execution.",
|
|
262
|
+
"examples": ["ssh", "local"]
|
|
263
|
+
},
|
|
264
|
+
"hostname": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "Host or IP address for SSH connections.",
|
|
267
|
+
"examples": ["192.168.0.10", "server.local"]
|
|
268
|
+
},
|
|
269
|
+
"port": {
|
|
270
|
+
"type": "integer",
|
|
271
|
+
"minimum": 1,
|
|
272
|
+
"maximum": 65535,
|
|
273
|
+
"description": "Port number used for SSH connections. Defaults to 22 if omitted.",
|
|
274
|
+
"examples": [22, 2222]
|
|
275
|
+
},
|
|
276
|
+
"username": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"description": "Username for authentication.",
|
|
279
|
+
"examples": ["root", "deploy", "ubuntu"]
|
|
280
|
+
},
|
|
281
|
+
"password": {
|
|
282
|
+
"type": ["string", "number", "null"],
|
|
283
|
+
"description": "Password or token for authentication. May be null if using key-based authentication.",
|
|
284
|
+
"examples": ["supersecret", null]
|
|
285
|
+
},
|
|
286
|
+
"private_key": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"description": "Path to a private key file for SSH authentication.",
|
|
289
|
+
"examples": ["~/.ssh/id_rsa", "/etc/keys/deploy.pem"]
|
|
290
|
+
},
|
|
291
|
+
"private_key_password": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"description": "Optional password for the private key file.",
|
|
294
|
+
"examples": ["keypass123"]
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"additionalProperties": false,
|
|
298
|
+
"examples": [
|
|
299
|
+
{
|
|
300
|
+
"connection": "ssh",
|
|
301
|
+
"hostname": "10.0.0.11",
|
|
302
|
+
"port": 22,
|
|
303
|
+
"username": "deploy",
|
|
304
|
+
"private_key": "~/.ssh/id_rsa"
|
|
305
|
+
},
|
|
306
|
+
{ "connection": "local" }
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
"examples": [
|
|
312
|
+
{
|
|
313
|
+
"cwd": "./deploy",
|
|
314
|
+
"logging": { "dir": "./logs", "level": "info" },
|
|
315
|
+
"include": "https://katmer.dev/configs/shared.json",
|
|
316
|
+
"targets": {
|
|
317
|
+
"production": {
|
|
318
|
+
"variables": {
|
|
319
|
+
"env": "prod",
|
|
320
|
+
"app_dir": "/opt/myapp",
|
|
321
|
+
"release": "2024-10-18"
|
|
322
|
+
},
|
|
323
|
+
"environment": {
|
|
324
|
+
"NODE_ENV": "production",
|
|
325
|
+
"HTTP_PROXY": "http://proxy.internal:8080"
|
|
326
|
+
},
|
|
327
|
+
"hosts": {
|
|
328
|
+
"frontend": {
|
|
329
|
+
"connection": "ssh",
|
|
330
|
+
"hostname": "10.0.0.10",
|
|
331
|
+
"username": "deployer",
|
|
332
|
+
"private_key": "~/.ssh/id_rsa"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"children": {
|
|
336
|
+
"database": {
|
|
337
|
+
"variables": { "pg_version": 15 },
|
|
338
|
+
"environment": { "PGTZ": "UTC" },
|
|
339
|
+
"hosts": {
|
|
340
|
+
"db1": {
|
|
341
|
+
"connection": "ssh",
|
|
342
|
+
"hostname": "10.0.0.15",
|
|
343
|
+
"username": "postgres",
|
|
344
|
+
"private_key": "~/.ssh/id_rsa"
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
"local": {
|
|
351
|
+
"hosts": {
|
|
352
|
+
"localhost": { "connection": "local" }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KatmerConfig,
|
|
3
|
+
KatmerHostResolved,
|
|
4
|
+
KatmerHostInput
|
|
5
|
+
} from "./interfaces/config.interface"
|
|
6
|
+
import { toMerged } from "es-toolkit"
|
|
7
|
+
import type { KatmerProvider } from "./interfaces/provider.interface"
|
|
8
|
+
import objectHash from "stable-hash"
|
|
9
|
+
import { SSHProvider } from "./providers/ssh/ssh.provider"
|
|
10
|
+
import type { KatmerCore } from "./katmer"
|
|
11
|
+
import { LocalProvider } from "./providers/local.provider"
|
|
12
|
+
import { wildcardMatch } from "./utils/string.utils"
|
|
13
|
+
import { evalObjectVals, evalTemplate } from "./utils/renderer/renderer"
|
|
14
|
+
|
|
15
|
+
export class KatmerTargetResolver {
|
|
16
|
+
#providerCache = new Map<string, KatmerProvider>()
|
|
17
|
+
|
|
18
|
+
#allNames: Set<string>
|
|
19
|
+
#groups: Map<string, Set<string>>
|
|
20
|
+
#hosts: Map<string, KatmerHostResolved>
|
|
21
|
+
constructor(
|
|
22
|
+
private core: KatmerCore,
|
|
23
|
+
...targets: KatmerConfig["targets"][]
|
|
24
|
+
) {
|
|
25
|
+
const normalized = KatmerTargetResolver.normalizeHosts(
|
|
26
|
+
this.core.config.targets,
|
|
27
|
+
...targets
|
|
28
|
+
)
|
|
29
|
+
this.#hosts = normalized.hosts
|
|
30
|
+
this.#groups = normalized.groups
|
|
31
|
+
this.#allNames = normalized.allNames
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get hosts() {
|
|
35
|
+
return this.#hosts
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async resolveProvider(opts: KatmerHostResolved): Promise<KatmerProvider> {
|
|
39
|
+
const key = objectHash(opts)
|
|
40
|
+
|
|
41
|
+
if (this.#providerCache.has(key)) {
|
|
42
|
+
return this.#providerCache.get(key)!
|
|
43
|
+
}
|
|
44
|
+
let provider: KatmerProvider
|
|
45
|
+
if (!opts.connection || opts.connection === "ssh") {
|
|
46
|
+
provider = new SSHProvider(opts)
|
|
47
|
+
} else if (opts.connection === "local") {
|
|
48
|
+
provider = new LocalProvider(opts)
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`Unknown connection type: ${opts["connection"]}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
provider.logger = this.core.logger.child({
|
|
54
|
+
provider: provider.constructor.name
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (opts.variables) {
|
|
58
|
+
provider.variables = toMerged(provider.variables, opts.variables)
|
|
59
|
+
}
|
|
60
|
+
if (opts.environment) {
|
|
61
|
+
const env = toMerged(provider.environment, opts.environment)
|
|
62
|
+
|
|
63
|
+
provider.environment = await evalObjectVals(env, {
|
|
64
|
+
// TODO: env context
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.#providerCache.set(key, provider)
|
|
69
|
+
return provider
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolveTargets(pattern: string) {
|
|
73
|
+
if (pattern === "all" || pattern === "*") {
|
|
74
|
+
return [...this.#hosts.values()]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parts = pattern.split(/[:,]/)
|
|
78
|
+
const included = [] as string[]
|
|
79
|
+
const excluded = [] as string[]
|
|
80
|
+
const intersected = [] as string[]
|
|
81
|
+
|
|
82
|
+
for (const part of parts) {
|
|
83
|
+
if (part.startsWith("!")) {
|
|
84
|
+
const plainName = part.slice(1)
|
|
85
|
+
if (excluded.indexOf(plainName) === -1) {
|
|
86
|
+
excluded.push(plainName)
|
|
87
|
+
}
|
|
88
|
+
} else if (part.startsWith("@")) {
|
|
89
|
+
const plainName = part.slice(1)
|
|
90
|
+
if (intersected.indexOf(plainName) === -1) {
|
|
91
|
+
intersected.push(part.slice(1))
|
|
92
|
+
}
|
|
93
|
+
} else if (included.indexOf(part) === -1) {
|
|
94
|
+
const token = part === "all" ? "*" : part // ← normalize 'all'
|
|
95
|
+
if (!included.includes(token)) included.push(token)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const wildcard = (name: string, pat: string) => wildcardMatch(name, pat)
|
|
99
|
+
const matchesAny = (name: string, list: string[]) =>
|
|
100
|
+
list.some((p) => wildcard(name, p))
|
|
101
|
+
const isExcluded = (name: string) => excluded.some((p) => wildcard(name, p))
|
|
102
|
+
|
|
103
|
+
// Stage 1: choose label candidates (hosts or groups), honoring exclusion
|
|
104
|
+
const candidateLabels = new Set<string>()
|
|
105
|
+
for (const name of this.#allNames) {
|
|
106
|
+
if (isExcluded(name)) continue
|
|
107
|
+
if (included.length === 0 || matchesAny(name, included)) {
|
|
108
|
+
candidateLabels.add(name)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Stage 2: expand labels to hostnames, honoring exclusion & dedupe
|
|
113
|
+
const expandedHostnames = new Set<string>()
|
|
114
|
+
for (const label of candidateLabels) {
|
|
115
|
+
if (this.#groups.has(label)) {
|
|
116
|
+
for (const host of this.#groups.get(label)!) {
|
|
117
|
+
if (!isExcluded(host)) expandedHostnames.add(host)
|
|
118
|
+
}
|
|
119
|
+
} else if (this.#hosts.has(label)) {
|
|
120
|
+
if (!isExcluded(label)) expandedHostnames.add(label)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Stage 3: optional intersection (@foo) applied on final hostnames
|
|
125
|
+
let finalHostnames = Array.from(expandedHostnames)
|
|
126
|
+
if (intersected.length > 0) {
|
|
127
|
+
finalHostnames = finalHostnames.filter((h) => matchesAny(h, intersected))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const resolved = finalHostnames
|
|
131
|
+
.map((h) => this.#hosts.get(h)!)
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
|
|
134
|
+
if (resolved.length === 0) {
|
|
135
|
+
throw new Error(`No targets found for pattern: ${pattern}`)
|
|
136
|
+
}
|
|
137
|
+
return resolved
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static normalizeHosts(...inputs: KatmerConfig["targets"][]) {
|
|
141
|
+
const reservedKeys = [
|
|
142
|
+
"all",
|
|
143
|
+
"children",
|
|
144
|
+
"settings",
|
|
145
|
+
"variables",
|
|
146
|
+
"environment",
|
|
147
|
+
"hosts"
|
|
148
|
+
]
|
|
149
|
+
const allNames = new Set<string>()
|
|
150
|
+
const hosts = new Map<string, KatmerHostResolved>()
|
|
151
|
+
const groups = new Map<string, Set<string>>()
|
|
152
|
+
|
|
153
|
+
const groupSettingsAccum = new Map<string, Record<string, any>>()
|
|
154
|
+
const groupVariablesAccum = new Map<string, Record<string, any>>()
|
|
155
|
+
const groupEnvAccum = new Map<string, Record<string, any>>() // NEW
|
|
156
|
+
|
|
157
|
+
for (const input of inputs) {
|
|
158
|
+
if (!input) continue
|
|
159
|
+
|
|
160
|
+
const rootKeys = Object.keys(input)
|
|
161
|
+
const isHostsDef =
|
|
162
|
+
rootKeys.includes("hosts") ||
|
|
163
|
+
rootKeys.includes("settings") ||
|
|
164
|
+
rootKeys.includes("variables") ||
|
|
165
|
+
rootKeys.includes("environment") // NEW
|
|
166
|
+
|
|
167
|
+
if (isHostsDef) {
|
|
168
|
+
// Ungrouped (root) settings
|
|
169
|
+
const incomingSettings = (input as any).settings || {}
|
|
170
|
+
const prevSettings = groupSettingsAccum.get("ungrouped") || {}
|
|
171
|
+
const effSettings = toMerged(prevSettings, incomingSettings) as any
|
|
172
|
+
groupSettingsAccum.set("ungrouped", effSettings)
|
|
173
|
+
|
|
174
|
+
// Ungrouped variables
|
|
175
|
+
const incomingVars = (input as any).variables || {}
|
|
176
|
+
const prevVars = groupVariablesAccum.get("ungrouped") || {}
|
|
177
|
+
const effVars = toMerged(prevVars, incomingVars) as any
|
|
178
|
+
groupVariablesAccum.set("ungrouped", effVars)
|
|
179
|
+
|
|
180
|
+
// Ungrouped environment (NEW)
|
|
181
|
+
const incomingEnv = (input as any).environment || {}
|
|
182
|
+
const prevEnv = groupEnvAccum.get("ungrouped") || {}
|
|
183
|
+
const effEnv = toMerged(prevEnv, incomingEnv) as any
|
|
184
|
+
groupEnvAccum.set("ungrouped", effEnv)
|
|
185
|
+
|
|
186
|
+
processHostEntries(
|
|
187
|
+
(input as any).hosts,
|
|
188
|
+
"ungrouped",
|
|
189
|
+
effSettings,
|
|
190
|
+
effVars,
|
|
191
|
+
effEnv
|
|
192
|
+
)
|
|
193
|
+
} else {
|
|
194
|
+
for (const [groupName, def] of Object.entries(input)) {
|
|
195
|
+
if (reservedKeys.includes(groupName)) {
|
|
196
|
+
throw `cannot use '${groupName}' as group name: it is a reserved keyword`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Group settings
|
|
200
|
+
const incomingSettings = (def as any)?.settings || {}
|
|
201
|
+
const prevSettings = groupSettingsAccum.get(groupName) || {}
|
|
202
|
+
const effSettings = toMerged(prevSettings, incomingSettings) as any
|
|
203
|
+
groupSettingsAccum.set(groupName, effSettings)
|
|
204
|
+
|
|
205
|
+
// Group variables
|
|
206
|
+
const incomingVars = (def as any)?.variables || {}
|
|
207
|
+
const prevVars = groupVariablesAccum.get(groupName) || {}
|
|
208
|
+
const effVars = toMerged(prevVars, incomingVars) as any
|
|
209
|
+
groupVariablesAccum.set(groupName, effVars)
|
|
210
|
+
|
|
211
|
+
// Group environment (NEW)
|
|
212
|
+
const incomingEnv = (def as any)?.environment || {}
|
|
213
|
+
const prevEnv = groupEnvAccum.get(groupName) || {}
|
|
214
|
+
const effEnv = toMerged(prevEnv, incomingEnv) as any
|
|
215
|
+
groupEnvAccum.set(groupName, effEnv)
|
|
216
|
+
|
|
217
|
+
processHostEntries(
|
|
218
|
+
def?.hosts,
|
|
219
|
+
groupName,
|
|
220
|
+
effSettings,
|
|
221
|
+
effVars,
|
|
222
|
+
effEnv
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
// Link children: inherit parent *settings/variables/environment* to child hosts
|
|
226
|
+
const childNames = Object.keys(def?.children || {})
|
|
227
|
+
for (const childGroupName of childNames) {
|
|
228
|
+
if (!groups.has(childGroupName)) {
|
|
229
|
+
throw `child group not found: '${childGroupName}' in group: '${groupName}'`
|
|
230
|
+
}
|
|
231
|
+
for (const child_hostname of groups.get(childGroupName)!) {
|
|
232
|
+
const prevChild = (hosts.get(child_hostname) || {}) as any
|
|
233
|
+
const childVarsPrev = prevChild.variables || {}
|
|
234
|
+
const childEnvPrev = prevChild.environment || {}
|
|
235
|
+
const parentVars = groupVariablesAccum.get(groupName) || {}
|
|
236
|
+
const parentEnv = groupEnvAccum.get(groupName) || {}
|
|
237
|
+
|
|
238
|
+
hosts.set(child_hostname, {
|
|
239
|
+
...(toMerged(prevChild, effSettings) as any),
|
|
240
|
+
name: child_hostname,
|
|
241
|
+
variables: toMerged(childVarsPrev, parentVars),
|
|
242
|
+
environment: toMerged(childEnvPrev, parentEnv) // NEW
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function processHostEntries(
|
|
251
|
+
hostConfig?: Record<string, KatmerHostInput>,
|
|
252
|
+
groupName: string = "ungrouped",
|
|
253
|
+
groupSettings: Record<string, any> = {},
|
|
254
|
+
groupVariables: Record<string, any> = {},
|
|
255
|
+
groupEnvironment: Record<string, any> = {} // NEW
|
|
256
|
+
) {
|
|
257
|
+
if (!groups.has(groupName)) groups.set(groupName, new Set<string>())
|
|
258
|
+
const groupHosts = groups.get(groupName)!
|
|
259
|
+
allNames.add(groupName)
|
|
260
|
+
|
|
261
|
+
for (const [hostname, host_settings] of Object.entries(
|
|
262
|
+
hostConfig || {}
|
|
263
|
+
)) {
|
|
264
|
+
if (reservedKeys.includes(hostname)) {
|
|
265
|
+
throw `cannot use '${hostname}' as hostname: it is a reserved keyword`
|
|
266
|
+
}
|
|
267
|
+
allNames.add(hostname)
|
|
268
|
+
groupHosts.add(hostname)
|
|
269
|
+
|
|
270
|
+
const prev = (hosts.get(hostname) || {}) as any
|
|
271
|
+
const mergedConn = toMerged(
|
|
272
|
+
prev,
|
|
273
|
+
toMerged(groupSettings || {}, host_settings || {})
|
|
274
|
+
) as any
|
|
275
|
+
|
|
276
|
+
const prevVars = prev.variables || {}
|
|
277
|
+
const prevEnv = prev.environment || {}
|
|
278
|
+
|
|
279
|
+
hosts.set(hostname, {
|
|
280
|
+
...mergedConn,
|
|
281
|
+
name: hostname,
|
|
282
|
+
variables: toMerged(prevVars, groupVariables || {}),
|
|
283
|
+
environment: toMerged(prevEnv, groupEnvironment || {}) // NEW
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { groups, hosts, allNames }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async [Symbol.asyncDispose]() {
|
|
292
|
+
try {
|
|
293
|
+
for (const [_hash, provider] of this.#providerCache.entries()) {
|
|
294
|
+
await provider.safeShutdown()
|
|
295
|
+
}
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { KatmerTask } from "../task"
|
|
2
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
3
|
+
import { evalExpr, evalObjectVals } from "../../utils/renderer/renderer"
|
|
4
|
+
import { merge } from "es-toolkit/compat"
|
|
5
|
+
import { mapValues } from "es-toolkit"
|
|
6
|
+
|
|
7
|
+
const configKey = "environment" as const
|
|
8
|
+
|
|
9
|
+
export const EnvironmentControl = {
|
|
10
|
+
order: 10,
|
|
11
|
+
configKey,
|
|
12
|
+
register(task: KatmerTask, cfg?: Katmer.TaskRule[typeof configKey]) {
|
|
13
|
+
task.on("before:execute", async (_task, ctx) => {
|
|
14
|
+
if (cfg || Object.keys(ctx.provider.environment).length > 0) {
|
|
15
|
+
const _exec = ctx.exec
|
|
16
|
+
|
|
17
|
+
ctx.exec = async (command, options) => {
|
|
18
|
+
const taskEnv =
|
|
19
|
+
typeof cfg === "string" ? await evalExpr(cfg, ctx.variables) : cfg
|
|
20
|
+
|
|
21
|
+
const env = mapValues(
|
|
22
|
+
merge(
|
|
23
|
+
{},
|
|
24
|
+
ctx.provider.environment,
|
|
25
|
+
taskEnv || {},
|
|
26
|
+
options?.env || {}
|
|
27
|
+
),
|
|
28
|
+
(value) =>
|
|
29
|
+
value !== undefined && value !== null ? String(value) : value
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return _exec(
|
|
33
|
+
command,
|
|
34
|
+
merge({}, options, {
|
|
35
|
+
env: await evalObjectVals(env, ctx.variables)
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|