@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,298 @@
|
|
|
1
|
+
import { type ModuleCommonReturn } from "../../interfaces/module.interface"
|
|
2
|
+
import { SSHProvider } from "../../providers/ssh/ssh.provider"
|
|
3
|
+
import type { KatmerProvider } from "../../interfaces/provider.interface"
|
|
4
|
+
|
|
5
|
+
import { HttpModule as HTTPCurlModule } from "./http.curl.module"
|
|
6
|
+
import { HttpModule as HTTPLocalModule } from "./http.local.module"
|
|
7
|
+
|
|
8
|
+
declare module "../../interfaces/task.interface" {
|
|
9
|
+
export namespace Katmer {
|
|
10
|
+
export interface TaskActions {
|
|
11
|
+
/**
|
|
12
|
+
* Perform an HTTP(S) request.
|
|
13
|
+
* See HttpModuleOptions for all parameters.
|
|
14
|
+
*/
|
|
15
|
+
http?: HttpModuleOptions
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* HTTP methods
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
type HttpMethod =
|
|
25
|
+
| "GET"
|
|
26
|
+
| "POST"
|
|
27
|
+
| "PUT"
|
|
28
|
+
| "PATCH"
|
|
29
|
+
| "DELETE"
|
|
30
|
+
| "HEAD"
|
|
31
|
+
| "OPTIONS"
|
|
32
|
+
| "TRACE"
|
|
33
|
+
| "CONNECT"
|
|
34
|
+
|
|
35
|
+
type HttpBasicAuth = {
|
|
36
|
+
/**
|
|
37
|
+
* Use Basic authentication.
|
|
38
|
+
* @defaultValue "basic"
|
|
39
|
+
*/
|
|
40
|
+
type: "basic"
|
|
41
|
+
/**
|
|
42
|
+
* Username used for basic auth.
|
|
43
|
+
*/
|
|
44
|
+
username: string
|
|
45
|
+
/**
|
|
46
|
+
* Password used for basic auth.
|
|
47
|
+
*/
|
|
48
|
+
password: string
|
|
49
|
+
}
|
|
50
|
+
type HttpBearerAuth = {
|
|
51
|
+
/**
|
|
52
|
+
* Use Bearer token authentication.
|
|
53
|
+
* @defaultValue "bearer"
|
|
54
|
+
*/
|
|
55
|
+
type: "bearer"
|
|
56
|
+
/**
|
|
57
|
+
* Bearer token to send in Authorization header.
|
|
58
|
+
*/
|
|
59
|
+
token: string
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Authentication configuration.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type HttpAuth = HttpBasicAuth | HttpBearerAuth
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retry configuration.
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export interface HttpRetry {
|
|
72
|
+
/**
|
|
73
|
+
* Total number of retry attempts on transient failures.
|
|
74
|
+
* Maps to: --retry [tries]
|
|
75
|
+
*/
|
|
76
|
+
tries?: number
|
|
77
|
+
/**
|
|
78
|
+
* Delay in seconds between retries.
|
|
79
|
+
* Maps to: --retry-delay [seconds]
|
|
80
|
+
*/
|
|
81
|
+
delay?: number
|
|
82
|
+
/**
|
|
83
|
+
* Total time limit in seconds for all retries.
|
|
84
|
+
* Maps to: --retry-max-time [seconds]
|
|
85
|
+
*/
|
|
86
|
+
max_time?: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Output configuration for the response body.
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export interface HttpOutput {
|
|
94
|
+
/**
|
|
95
|
+
* Remote path to save the response body to.
|
|
96
|
+
* If set, the module writes to a temporary file and atomically moves it into place.
|
|
97
|
+
*/
|
|
98
|
+
toFile?: string
|
|
99
|
+
/**
|
|
100
|
+
* Additionally capture the response body in the module result (body field).
|
|
101
|
+
* Note: may increase memory usage for large responses.
|
|
102
|
+
*/
|
|
103
|
+
captureBody?: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Options for the http module.
|
|
108
|
+
* @public
|
|
109
|
+
*/
|
|
110
|
+
export interface HttpModuleOptions {
|
|
111
|
+
/**
|
|
112
|
+
* Target URL
|
|
113
|
+
*/
|
|
114
|
+
url: string | URL
|
|
115
|
+
/**
|
|
116
|
+
* HTTP method to use.
|
|
117
|
+
* @defaultValue "GET"
|
|
118
|
+
*
|
|
119
|
+
* Special handling:
|
|
120
|
+
* - When method is "HEAD" and writing to file, uses `-X HEAD`.
|
|
121
|
+
* - When method is "HEAD" and not writing to file, uses `-I`.
|
|
122
|
+
*/
|
|
123
|
+
method?: HttpMethod
|
|
124
|
+
/**
|
|
125
|
+
* Additional HTTP headers to send.
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* headers: { "Accept": "application/json", "User-Agent": "katmer" }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
headers?: Record<string, string>
|
|
132
|
+
/**
|
|
133
|
+
* Key-value query parameters to append to the URL.
|
|
134
|
+
* Null or undefined values are skipped.
|
|
135
|
+
*/
|
|
136
|
+
query?: Record<string, string | number | boolean | null | undefined>
|
|
137
|
+
/**
|
|
138
|
+
* Request body.
|
|
139
|
+
* - If an object is provided, it is JSON-encoded and `Content-Type: application/json` is added unless already set.
|
|
140
|
+
* - If a string is provided, it is sent via `--data-binary "string"`.
|
|
141
|
+
* - For binary payloads, prefer {@link HttpModuleOptions.bodyFile | bodyFile}.
|
|
142
|
+
*/
|
|
143
|
+
body?: string | Record<string, any> | Uint8Array
|
|
144
|
+
/**
|
|
145
|
+
* Remote path to a file whose content will be sent via `--data-binary @[file]`.
|
|
146
|
+
*/
|
|
147
|
+
bodyFile?: string
|
|
148
|
+
/**
|
|
149
|
+
* Authentication configuration (basic or bearer).
|
|
150
|
+
*/
|
|
151
|
+
auth?: HttpAuth
|
|
152
|
+
/**
|
|
153
|
+
* Total request timeout in seconds.
|
|
154
|
+
* Maps to: `--max-time [seconds]`
|
|
155
|
+
* @defaultValue 30
|
|
156
|
+
*/
|
|
157
|
+
timeout?: number
|
|
158
|
+
/**
|
|
159
|
+
* Follow redirects.
|
|
160
|
+
* Maps to: `-L`
|
|
161
|
+
* @defaultValue true
|
|
162
|
+
*/
|
|
163
|
+
follow_redirects?: boolean
|
|
164
|
+
/**
|
|
165
|
+
* Validate TLS certificates. If false, passes `--insecure`.
|
|
166
|
+
* @defaultValue true
|
|
167
|
+
*/
|
|
168
|
+
validate_certs?: boolean
|
|
169
|
+
/**
|
|
170
|
+
* Output configuration. A string is shorthand for `output.toFile`.
|
|
171
|
+
*/
|
|
172
|
+
output?: HttpOutput | string
|
|
173
|
+
/**
|
|
174
|
+
* Remote path to save response headers.
|
|
175
|
+
* Maps to: `-D [file]`
|
|
176
|
+
*/
|
|
177
|
+
save_headers_to?: string
|
|
178
|
+
/**
|
|
179
|
+
* Treat non-2xx HTTP codes as fatal.
|
|
180
|
+
* Maps to: `--fail-with-body`
|
|
181
|
+
* @defaultValue true
|
|
182
|
+
*/
|
|
183
|
+
fail_on_http_error?: boolean
|
|
184
|
+
/**
|
|
185
|
+
* Retry settings.
|
|
186
|
+
*/
|
|
187
|
+
retry?: HttpRetry
|
|
188
|
+
/**
|
|
189
|
+
* Additional raw arguments to append to the curl command. Only works with Local provider
|
|
190
|
+
*/
|
|
191
|
+
extra_args?: string[]
|
|
192
|
+
/**
|
|
193
|
+
* File mode to set on output file (when `output.toFile` is set).
|
|
194
|
+
* Accepts octal number (e.g., `0o644`) or string (e.g., `"0644"`).
|
|
195
|
+
*/
|
|
196
|
+
mode?: string | number
|
|
197
|
+
/**
|
|
198
|
+
* File owner to set on the output file (`chown`).
|
|
199
|
+
*/
|
|
200
|
+
owner?: string
|
|
201
|
+
/**
|
|
202
|
+
* File group to set on the output file (`chown`).
|
|
203
|
+
*/
|
|
204
|
+
group?: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Result returned by the http module.
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
export interface HttpModuleResult extends ModuleCommonReturn {
|
|
212
|
+
/**
|
|
213
|
+
* Parsed whatwg-url URL object.
|
|
214
|
+
*/
|
|
215
|
+
url: URL
|
|
216
|
+
/**
|
|
217
|
+
* Parsed HTTP status code when available (best-effort).
|
|
218
|
+
*/
|
|
219
|
+
status?: number
|
|
220
|
+
/**
|
|
221
|
+
* Raw response headers if saved via {@link HttpModuleOptions.save_headers_to | save_headers_to}.
|
|
222
|
+
*/
|
|
223
|
+
headers?: Record<string, string | string[]>
|
|
224
|
+
/**
|
|
225
|
+
* Response body if `captureBody=true`, or when output is not used and `captureBody=true`.
|
|
226
|
+
*/
|
|
227
|
+
body?: string
|
|
228
|
+
/**
|
|
229
|
+
* Destination path when `output.toFile` (or string shorthand) was used.
|
|
230
|
+
*/
|
|
231
|
+
dest?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Execute HTTP(S) requests.
|
|
236
|
+
*
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* - Uses `curl` when running with ssh provider
|
|
240
|
+
* - Follows redirects by default (`-L`).
|
|
241
|
+
* - Validates TLS by default; set {@link HttpModuleOptions.validate_certs | validate_certs}: false to pass `--insecure`.
|
|
242
|
+
* - When {@link HttpModuleOptions.output | output.toFile} is set, writes to a temporary file then moves atomically; `changed=true` on success.
|
|
243
|
+
* - If {@link HttpModuleOptions.fail_on_http_error | fail_on_http_error} is true (default), non-2xx responses cause a failure using `--fail-with-body`.
|
|
244
|
+
* - Best-effort status parsing is performed from saved headers or stderr when available.
|
|
245
|
+
*
|
|
246
|
+
* @examples
|
|
247
|
+
* ```yaml
|
|
248
|
+
* - name: Download a file to a path (like get_url)
|
|
249
|
+
* http:
|
|
250
|
+
* url: "https://example.com/app.tar.gz"
|
|
251
|
+
* output: "/opt/app/app.tar.gz"
|
|
252
|
+
* mode: "0644"
|
|
253
|
+
*
|
|
254
|
+
* - name: GET JSON with headers and save response headers
|
|
255
|
+
* http:
|
|
256
|
+
* url: "https://api.example.com/meta"
|
|
257
|
+
* headers:
|
|
258
|
+
* Accept: "application/json"
|
|
259
|
+
* save_headers_to: "/tmp/meta.headers"
|
|
260
|
+
* output:
|
|
261
|
+
* toFile: "/tmp/meta.json"
|
|
262
|
+
* captureBody: true
|
|
263
|
+
*
|
|
264
|
+
* - name: POST JSON with bearer token
|
|
265
|
+
* http:
|
|
266
|
+
* url: "https://api.example.com/resources"
|
|
267
|
+
* method: "POST"
|
|
268
|
+
* headers:
|
|
269
|
+
* Accept: "application/json"
|
|
270
|
+
* body:
|
|
271
|
+
* name: "demo"
|
|
272
|
+
* enabled: true
|
|
273
|
+
* auth:
|
|
274
|
+
* type: "bearer"
|
|
275
|
+
* token: "{{ MY_API_TOKEN }}"
|
|
276
|
+
* fail_on_http_error: true
|
|
277
|
+
*
|
|
278
|
+
* - name: Download with basic auth
|
|
279
|
+
* http:
|
|
280
|
+
* url: "https://intranet.local/file.bin"
|
|
281
|
+
* auth:
|
|
282
|
+
* type: "basic"
|
|
283
|
+
* username: "user"
|
|
284
|
+
* password: "pass"
|
|
285
|
+
* validate_certs: false
|
|
286
|
+
* retry:
|
|
287
|
+
* tries: 5
|
|
288
|
+
* delay: 2
|
|
289
|
+
* ```
|
|
290
|
+
* @public
|
|
291
|
+
*/
|
|
292
|
+
export function HttpModule(opts: HttpModuleOptions, provider: KatmerProvider) {
|
|
293
|
+
if (provider instanceof SSHProvider) {
|
|
294
|
+
return new HTTPCurlModule(opts, provider)
|
|
295
|
+
} else {
|
|
296
|
+
return new HTTPLocalModule(opts, provider)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./apt.module"
|
|
2
|
+
export * from "./apt-repository/apt-repository.module"
|
|
3
|
+
export * from "./archive.module"
|
|
4
|
+
export * from "./become.module"
|
|
5
|
+
export * from "./copy.module"
|
|
6
|
+
export * from "./gather_facts.module"
|
|
7
|
+
export * from "./git.module"
|
|
8
|
+
export * from "./debug.module"
|
|
9
|
+
export * from "./http/http.module"
|
|
10
|
+
export * from "./hostname.module"
|
|
11
|
+
export * from "./package.module"
|
|
12
|
+
export * from "./script.module"
|
|
13
|
+
export * from "./set_fact.module"
|
|
14
|
+
export * from "./template.module"
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ModuleCommonReturn,
|
|
3
|
+
type ModuleConstraints
|
|
4
|
+
} from "../interfaces/module.interface"
|
|
5
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
6
|
+
import type { KatmerProvider } from "../interfaces/provider.interface"
|
|
7
|
+
import { KatmerModule } from "../module"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Unified package management module.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* This module provides a **single, portable interface** for installing,
|
|
14
|
+
* updating, or removing packages across different operating systems
|
|
15
|
+
* and package managers.
|
|
16
|
+
*
|
|
17
|
+
* Supported package managers:
|
|
18
|
+
*
|
|
19
|
+
* - **Linux**: apt, dnf, yum, pacman, apk, zypper
|
|
20
|
+
* - **macOS**: brew
|
|
21
|
+
* - **Windows**: winget, choco
|
|
22
|
+
*
|
|
23
|
+
* The module automatically:
|
|
24
|
+
* - Detects the target OS via {@link KatmerProvider.os}
|
|
25
|
+
* - Probes for available package managers
|
|
26
|
+
* - Selects the most appropriate one
|
|
27
|
+
*
|
|
28
|
+
* @examples
|
|
29
|
+
* ```yaml
|
|
30
|
+
* - name: Install curl
|
|
31
|
+
* package:
|
|
32
|
+
* name: curl
|
|
33
|
+
*
|
|
34
|
+
* - name: Ensure git is removed
|
|
35
|
+
* package:
|
|
36
|
+
* name: git
|
|
37
|
+
* state: absent
|
|
38
|
+
*
|
|
39
|
+
* - name: Upgrade docker
|
|
40
|
+
* package:
|
|
41
|
+
* name: docker
|
|
42
|
+
* state: latest
|
|
43
|
+
*
|
|
44
|
+
* - name: Install multiple packages
|
|
45
|
+
* package:
|
|
46
|
+
* name:
|
|
47
|
+
* - curl
|
|
48
|
+
* - git
|
|
49
|
+
* - jq
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class PackageModule extends KatmerModule<
|
|
53
|
+
PackageModuleOptions,
|
|
54
|
+
PackageModuleResult
|
|
55
|
+
> {
|
|
56
|
+
static name = "package" as const
|
|
57
|
+
|
|
58
|
+
constraints = {
|
|
59
|
+
platform: {
|
|
60
|
+
any: true
|
|
61
|
+
}
|
|
62
|
+
} satisfies ModuleConstraints
|
|
63
|
+
|
|
64
|
+
async check(): Promise<void> {
|
|
65
|
+
const o = normalizeOptions(this.params)
|
|
66
|
+
if (!o.name || (Array.isArray(o.name) && o.name.length === 0)) {
|
|
67
|
+
throw new Error("package: 'name' is required")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async initialize(): Promise<void> {}
|
|
72
|
+
async cleanup(): Promise<void> {}
|
|
73
|
+
|
|
74
|
+
async execute(ctx: Katmer.TaskContext): Promise<PackageModuleResult> {
|
|
75
|
+
const o = normalizeOptions(this.params)
|
|
76
|
+
const names = Array.isArray(o.name) ? o.name : [o.name]
|
|
77
|
+
|
|
78
|
+
const pm = await detectPackageManager(ctx)
|
|
79
|
+
if (!pm) {
|
|
80
|
+
return {
|
|
81
|
+
changed: false,
|
|
82
|
+
failed: true,
|
|
83
|
+
msg: "No supported package manager detected on target"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cmd = buildCommand(pm, o.state, names)
|
|
88
|
+
if (!cmd) {
|
|
89
|
+
return {
|
|
90
|
+
changed: false,
|
|
91
|
+
failed: true,
|
|
92
|
+
msg: `Unsupported operation for package manager: ${pm}`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const r = await ctx.execSafe(cmd)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
changed: r.code === 0,
|
|
100
|
+
failed: r.code !== 0,
|
|
101
|
+
stdout: r.stdout,
|
|
102
|
+
stderr: r.stderr,
|
|
103
|
+
manager: pm
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
109
|
+
/* Options & Result Types */
|
|
110
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Options for the {@link PackageModule | `package`} module.
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
117
|
+
export type PackageModuleOptions =
|
|
118
|
+
| string
|
|
119
|
+
| {
|
|
120
|
+
/**
|
|
121
|
+
* Package name or list of packages.
|
|
122
|
+
*/
|
|
123
|
+
name: string | string[]
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Desired state of the package(s).
|
|
127
|
+
*
|
|
128
|
+
* - `present`: ensure installed (default)
|
|
129
|
+
* - `absent`: ensure removed
|
|
130
|
+
* - `latest`: upgrade to latest version
|
|
131
|
+
*
|
|
132
|
+
* @defaultValue "present"
|
|
133
|
+
*/
|
|
134
|
+
state?: "present" | "absent" | "latest"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Result returned by the {@link PackageModule | `package`} module.
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
142
|
+
export interface PackageModuleResult extends ModuleCommonReturn {
|
|
143
|
+
/** Package manager that was used */
|
|
144
|
+
manager?: PackageManager
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
148
|
+
/* Task context augmentation */
|
|
149
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
declare module "../interfaces/task.interface" {
|
|
152
|
+
export namespace Katmer {
|
|
153
|
+
export interface TaskActions {
|
|
154
|
+
package?: PackageModuleOptions
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
160
|
+
/* Internals */
|
|
161
|
+
/* ───────────────────────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
type PackageManager =
|
|
164
|
+
| "apt"
|
|
165
|
+
| "dnf"
|
|
166
|
+
| "yum"
|
|
167
|
+
| "pacman"
|
|
168
|
+
| "apk"
|
|
169
|
+
| "zypper"
|
|
170
|
+
| "brew"
|
|
171
|
+
| "winget"
|
|
172
|
+
| "choco"
|
|
173
|
+
|
|
174
|
+
function normalizeOptions(p: PackageModuleOptions): {
|
|
175
|
+
name: string | string[]
|
|
176
|
+
state: "present" | "absent" | "latest"
|
|
177
|
+
} {
|
|
178
|
+
if (typeof p === "string") {
|
|
179
|
+
return { name: p, state: "present" }
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
name: p.name,
|
|
183
|
+
state: p.state ?? "present"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function detectPackageManager(
|
|
188
|
+
ctx: Katmer.TaskContext
|
|
189
|
+
): Promise<PackageManager | null> {
|
|
190
|
+
const fam = ctx.provider.os.family
|
|
191
|
+
|
|
192
|
+
const probes: Array<[PackageManager, string]> = []
|
|
193
|
+
|
|
194
|
+
if (fam === "linux") {
|
|
195
|
+
probes.push(
|
|
196
|
+
["apt", "command -v apt-get"],
|
|
197
|
+
["dnf", "command -v dnf"],
|
|
198
|
+
["yum", "command -v yum"],
|
|
199
|
+
["pacman", "command -v pacman"],
|
|
200
|
+
["apk", "command -v apk"],
|
|
201
|
+
["zypper", "command -v zypper"]
|
|
202
|
+
)
|
|
203
|
+
} else if (fam === "darwin") {
|
|
204
|
+
probes.push(["brew", "command -v brew"])
|
|
205
|
+
} else if (fam === "windows") {
|
|
206
|
+
probes.push(["winget", "where winget"], ["choco", "where choco"])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const [pm, probe] of probes) {
|
|
210
|
+
const r = await ctx.execSafe(probe)
|
|
211
|
+
if (r.code === 0) return pm
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildCommand(
|
|
218
|
+
pm: PackageManager,
|
|
219
|
+
state: "present" | "absent" | "latest",
|
|
220
|
+
pkgs: string[]
|
|
221
|
+
): string | null {
|
|
222
|
+
const list = pkgs.join(" ")
|
|
223
|
+
|
|
224
|
+
switch (pm) {
|
|
225
|
+
case "apt":
|
|
226
|
+
if (state === "present")
|
|
227
|
+
return `apt-get update -y && apt-get install -y ${list}`
|
|
228
|
+
if (state === "latest")
|
|
229
|
+
return `apt-get update -y && apt-get install -y --only-upgrade ${list}`
|
|
230
|
+
return `apt-get remove -y ${list}`
|
|
231
|
+
|
|
232
|
+
case "dnf":
|
|
233
|
+
return (
|
|
234
|
+
state === "absent" ? `dnf remove -y ${list}`
|
|
235
|
+
: state === "latest" ? `dnf upgrade -y ${list}`
|
|
236
|
+
: `dnf install -y ${list}`
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
case "yum":
|
|
240
|
+
return (
|
|
241
|
+
state === "absent" ? `yum remove -y ${list}`
|
|
242
|
+
: state === "latest" ? `yum update -y ${list}`
|
|
243
|
+
: `yum install -y ${list}`
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
case "pacman":
|
|
247
|
+
return state === "absent" ?
|
|
248
|
+
`pacman -R --noconfirm ${list}`
|
|
249
|
+
: `pacman -S --noconfirm ${list}`
|
|
250
|
+
|
|
251
|
+
case "apk":
|
|
252
|
+
return state === "absent" ? `apk del ${list}` : `apk add ${list}`
|
|
253
|
+
|
|
254
|
+
case "zypper":
|
|
255
|
+
return state === "absent" ?
|
|
256
|
+
`zypper remove -y ${list}`
|
|
257
|
+
: `zypper install -y ${list}`
|
|
258
|
+
|
|
259
|
+
case "brew":
|
|
260
|
+
return (
|
|
261
|
+
state === "absent" ? `brew uninstall ${list}`
|
|
262
|
+
: state === "latest" ? `brew upgrade ${list}`
|
|
263
|
+
: `brew install ${list}`
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
case "winget":
|
|
267
|
+
return (
|
|
268
|
+
state === "absent" ? `winget uninstall --silent ${list}`
|
|
269
|
+
: state === "latest" ? `winget upgrade --silent ${list}`
|
|
270
|
+
: `winget install --silent ${list}`
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
case "choco":
|
|
274
|
+
return (
|
|
275
|
+
state === "absent" ? `choco uninstall -y ${list}`
|
|
276
|
+
: state === "latest" ? `choco upgrade -y ${list}`
|
|
277
|
+
: `choco install -y ${list}`
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
default:
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ModuleCommonReturn,
|
|
3
|
+
type ModuleConstraints
|
|
4
|
+
} from "../interfaces/module.interface"
|
|
5
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
6
|
+
import { evalTemplate } from "../utils/renderer/renderer"
|
|
7
|
+
import type { KatmerProvider } from "../interfaces/provider.interface"
|
|
8
|
+
import { KatmerModule } from "../module"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute an ad-hoc shell script on the target.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* - Accepts either a **string** (backwards compatible) or an **object** with a `content` string
|
|
15
|
+
* and a `render` flag to enable/disable template rendering.
|
|
16
|
+
* - When `render` is `true` (default), the script string is rendered with Twig against `ctx.variables`
|
|
17
|
+
* before execution. When `false`, the string is executed **as-is**.
|
|
18
|
+
* - Uses the provider's shell via {@link Katmer.TaskContext.exec | `ctx.exec`}.
|
|
19
|
+
* - Return semantics are simple: `changed` is always `false`; `failed` is set when the exit code is non-zero.
|
|
20
|
+
* - Standard output and error are surfaced as `stdout` and `stderr`.
|
|
21
|
+
*
|
|
22
|
+
* @examples
|
|
23
|
+
* ```yaml
|
|
24
|
+
* - name: Simple one-liner (rendering enabled by default):
|
|
25
|
+
* script: "echo Hello {{ env | default('world') }}"
|
|
26
|
+
*
|
|
27
|
+
* - name: Multi-line script (rendering enabled by default):
|
|
28
|
+
* script: |
|
|
29
|
+
* set -euo pipefail
|
|
30
|
+
* echo "cwd={{ cwd }}"
|
|
31
|
+
* ls -la
|
|
32
|
+
*
|
|
33
|
+
* - name: Disable templating (execute exactly the given text):
|
|
34
|
+
* script:
|
|
35
|
+
* content: "echo {{ literally-not-rendered }}"
|
|
36
|
+
* render: false
|
|
37
|
+
*
|
|
38
|
+
* - name: Conditional logic - restart service in prod
|
|
39
|
+
* when: env == 'prod'
|
|
40
|
+
* script: "systemctl restart myapp || true"
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class ScriptModule extends KatmerModule<
|
|
44
|
+
ScriptModuleOptions,
|
|
45
|
+
ScriptModuleResult,
|
|
46
|
+
KatmerProvider
|
|
47
|
+
> {
|
|
48
|
+
static name = "script" as const
|
|
49
|
+
|
|
50
|
+
constraints = {
|
|
51
|
+
platform: {
|
|
52
|
+
any: true
|
|
53
|
+
}
|
|
54
|
+
} satisfies ModuleConstraints
|
|
55
|
+
|
|
56
|
+
async check(_ctx: Katmer.TaskContext): Promise<void> {
|
|
57
|
+
const o = normalizeOptions(this.params)
|
|
58
|
+
if (!o.content || typeof o.content !== "string") {
|
|
59
|
+
throw new Error("script: 'content' must be a non-empty string")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async initialize(_ctx: Katmer.TaskContext): Promise<void> {}
|
|
64
|
+
async cleanup(_ctx: Katmer.TaskContext): Promise<void> {}
|
|
65
|
+
|
|
66
|
+
async execute(ctx: Katmer.TaskContext): Promise<ScriptModuleResult> {
|
|
67
|
+
const { content, render } = normalizeOptions(this.params)
|
|
68
|
+
|
|
69
|
+
const scriptText =
|
|
70
|
+
render ? await evalTemplate(content, ctx.variables) : content
|
|
71
|
+
const r = await ctx.exec(scriptText)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
failed: r.code !== undefined && r.code !== 0,
|
|
75
|
+
changed: false,
|
|
76
|
+
stdout: r.stdout,
|
|
77
|
+
stderr: r.stderr
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* You can pass a **raw string** which will be rendered using template engine,
|
|
84
|
+
* or an **object** to control rendering explicitly.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export type ScriptModuleOptions =
|
|
89
|
+
| string
|
|
90
|
+
| {
|
|
91
|
+
/** Inline script content to execute. */
|
|
92
|
+
content: string
|
|
93
|
+
/**
|
|
94
|
+
* Whether to render the script with Twig against `ctx.variables` before execution.
|
|
95
|
+
* @defaultValue true
|
|
96
|
+
*/
|
|
97
|
+
render?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Result of the script execution.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export interface ScriptModuleResult extends ModuleCommonReturn {
|
|
106
|
+
// inherits: changed, failed?, skipped?, msg?, stdout?, stderr?
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
// internals
|
|
111
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function normalizeOptions(p: ScriptModuleOptions): {
|
|
114
|
+
content: string
|
|
115
|
+
render: boolean
|
|
116
|
+
} {
|
|
117
|
+
if (typeof p === "string") return { content: p, render: true }
|
|
118
|
+
const content = p?.content ?? ""
|
|
119
|
+
const render = p?.render ?? true
|
|
120
|
+
return { content, render }
|
|
121
|
+
}
|