@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,13 @@
|
|
|
1
|
+
import { LoopControl } from "./loop.control"
|
|
2
|
+
import { RegisterControl } from "./register.control"
|
|
3
|
+
import { WhenControl } from "./when.control"
|
|
4
|
+
import { UntilControl } from "./until.control"
|
|
5
|
+
import { sortBy } from "es-toolkit"
|
|
6
|
+
import { EnvironmentControl } from "./environment.control"
|
|
7
|
+
|
|
8
|
+
export const TaskControls = sortBy(
|
|
9
|
+
[LoopControl, RegisterControl, WhenControl, UntilControl, EnvironmentControl],
|
|
10
|
+
["order"]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const TaskControlKeys = TaskControls.map((ctrl) => ctrl.configKey)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
2
|
+
import { defaults, isObjectLike } from "es-toolkit/compat"
|
|
3
|
+
import { wrapInArray } from "../../utils/json.utils"
|
|
4
|
+
import type { KatmerTask } from "../task"
|
|
5
|
+
import { evalExpr } from "../../utils/renderer/renderer"
|
|
6
|
+
import type { ModuleCommonReturn } from "../../interfaces/module.interface"
|
|
7
|
+
|
|
8
|
+
const configKey = "loop" as const
|
|
9
|
+
export const LoopControl = {
|
|
10
|
+
order: 100,
|
|
11
|
+
configKey,
|
|
12
|
+
register(task: KatmerTask, cfg?: Katmer.TaskRule[typeof configKey]) {
|
|
13
|
+
if (cfg) {
|
|
14
|
+
const control = defaults(
|
|
15
|
+
cfg && typeof cfg === "object" && "for" in cfg ?
|
|
16
|
+
cfg
|
|
17
|
+
: {
|
|
18
|
+
for: (cfg || []) as string[] | string
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
for: [] as string[],
|
|
22
|
+
index_var: "index",
|
|
23
|
+
break_when: [] as string[],
|
|
24
|
+
loop_var: "item",
|
|
25
|
+
extended: false,
|
|
26
|
+
extended_allitems: true
|
|
27
|
+
} satisfies Katmer.LoopControl
|
|
28
|
+
)
|
|
29
|
+
control.break_when = wrapInArray(control.break_when)
|
|
30
|
+
|
|
31
|
+
const baseExecute = task.execute
|
|
32
|
+
task.execute = async function runWithLoop(ctx: Katmer.TaskContext) {
|
|
33
|
+
ctx.logger.trace(`[loop] start`)
|
|
34
|
+
const loops =
|
|
35
|
+
typeof control.for === "string" ? await evalExpr(control.for)
|
|
36
|
+
: isObjectLike(control.for) ? control.for
|
|
37
|
+
: []
|
|
38
|
+
|
|
39
|
+
const loopEntries = Object.entries(loops)
|
|
40
|
+
const loopItems = Object.values(loops)
|
|
41
|
+
const loopResults = {
|
|
42
|
+
changed: false,
|
|
43
|
+
skipped: undefined as boolean | undefined,
|
|
44
|
+
failed: false,
|
|
45
|
+
results: [] as (ModuleCommonReturn | { skipped: true })[]
|
|
46
|
+
}
|
|
47
|
+
taskLoop: for (let i = 0; i < loopEntries.length; i++) {
|
|
48
|
+
const [loop_key, loop_val] = loopEntries[i]
|
|
49
|
+
ctx.variables[control.index_var] = loop_key
|
|
50
|
+
ctx.variables[control.loop_var] = loop_val
|
|
51
|
+
|
|
52
|
+
if (control.extended) {
|
|
53
|
+
ctx.variables.katmer_loop = {
|
|
54
|
+
allitems: control.extended_allitems ? loopItems : undefined,
|
|
55
|
+
index: i + 1,
|
|
56
|
+
index0: i,
|
|
57
|
+
revindex: loopItems.length - i,
|
|
58
|
+
revindex0: loopItems.length - i - 1,
|
|
59
|
+
first: i === 0,
|
|
60
|
+
last: i === loopItems.length - 1,
|
|
61
|
+
length: loopItems.length,
|
|
62
|
+
previtem: loopItems[i - 1],
|
|
63
|
+
nextitem: loopItems[i + 1]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ctx.logger.trace(`[loop] ${i} ${loop_key} ${loop_val}`)
|
|
67
|
+
const result = await baseExecute.call(task, ctx)
|
|
68
|
+
|
|
69
|
+
if (result) {
|
|
70
|
+
result.item = loop_val
|
|
71
|
+
loopResults.changed = loopResults.changed || result.changed
|
|
72
|
+
loopResults.failed = loopResults.failed || !!result.failed
|
|
73
|
+
loopResults.skipped = !!(result.skipped && loopResults.skipped)
|
|
74
|
+
loopResults.results.push(result)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const condition of control.break_when) {
|
|
78
|
+
if (await evalExpr(condition, ctx.variables)) {
|
|
79
|
+
break taskLoop
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ctx.logger.trace(`[loop] end`)
|
|
84
|
+
|
|
85
|
+
return loopResults
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { KatmerTask } from "../task"
|
|
2
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
3
|
+
|
|
4
|
+
const configKey = "register" as const
|
|
5
|
+
|
|
6
|
+
export const RegisterControl = {
|
|
7
|
+
order: 1000,
|
|
8
|
+
configKey,
|
|
9
|
+
register(task: KatmerTask, cfg?: Katmer.TaskRule[typeof configKey]) {
|
|
10
|
+
if (cfg) {
|
|
11
|
+
const baseExecute = task.execute
|
|
12
|
+
task.execute = async function registerResult(ctx: Katmer.TaskContext) {
|
|
13
|
+
ctx.logger.trace(`[register] start`)
|
|
14
|
+
|
|
15
|
+
const result = await baseExecute.call(task, ctx)
|
|
16
|
+
ctx.variables[cfg] = result
|
|
17
|
+
|
|
18
|
+
ctx.logger.trace(`[register] end`)
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { KatmerTask } from "../task"
|
|
2
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
3
|
+
import { evalExpr } from "../../utils/renderer/renderer"
|
|
4
|
+
import { delay } from "es-toolkit"
|
|
5
|
+
import type { ModuleCommonReturn } from "../../interfaces/module.interface"
|
|
6
|
+
|
|
7
|
+
const configKey = "until" as const
|
|
8
|
+
|
|
9
|
+
export const UntilControl = {
|
|
10
|
+
order: 50,
|
|
11
|
+
configKey,
|
|
12
|
+
register(task: KatmerTask, cfg?: Katmer.TaskRule[typeof configKey]) {
|
|
13
|
+
if (cfg) {
|
|
14
|
+
const control =
|
|
15
|
+
cfg && typeof cfg === "object" ?
|
|
16
|
+
cfg
|
|
17
|
+
: {
|
|
18
|
+
condition: cfg || ""
|
|
19
|
+
}
|
|
20
|
+
const baseExecute = task.execute
|
|
21
|
+
task.execute = async function runUntil(ctx: Katmer.TaskContext) {
|
|
22
|
+
let shouldRun = false
|
|
23
|
+
let attempts = 0
|
|
24
|
+
let result = {} as ModuleCommonReturn
|
|
25
|
+
do {
|
|
26
|
+
result = Object.assign(
|
|
27
|
+
result || {},
|
|
28
|
+
await baseExecute.call(task, ctx)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (control.condition) {
|
|
32
|
+
const untilResult = await evalExpr(control.condition, ctx.variables)
|
|
33
|
+
if (untilResult) {
|
|
34
|
+
shouldRun = false
|
|
35
|
+
} else {
|
|
36
|
+
if (control.retries && attempts === control.retries + 1) {
|
|
37
|
+
shouldRun = false
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!result) {
|
|
42
|
+
result = {} as any
|
|
43
|
+
}
|
|
44
|
+
result.retries = control.retries
|
|
45
|
+
result.attempts = attempts++
|
|
46
|
+
ctx.log("error", {
|
|
47
|
+
msg: `[FAILED]`,
|
|
48
|
+
result: result
|
|
49
|
+
})
|
|
50
|
+
if (control.delay) {
|
|
51
|
+
await delay(control.delay)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
ctx.log("debug", "Condiiton failed")
|
|
56
|
+
shouldRun = false
|
|
57
|
+
}
|
|
58
|
+
} while (shouldRun)
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KatmerTask } from "../task"
|
|
2
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
3
|
+
import { evalExpr } from "../../utils/renderer/renderer"
|
|
4
|
+
|
|
5
|
+
const configKey = "when" as const
|
|
6
|
+
|
|
7
|
+
export const WhenControl = {
|
|
8
|
+
order: 10,
|
|
9
|
+
configKey,
|
|
10
|
+
register(task: KatmerTask, cfg?: Katmer.TaskRule[typeof configKey]) {
|
|
11
|
+
if (cfg) {
|
|
12
|
+
const baseExecute = task.execute
|
|
13
|
+
task.execute = async function runWhen(ctx: Katmer.TaskContext) {
|
|
14
|
+
const whenResult = await evalExpr(cfg, ctx.variables)
|
|
15
|
+
if (!whenResult) {
|
|
16
|
+
return {
|
|
17
|
+
changed: false,
|
|
18
|
+
skipped: true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return await baseExecute.call(task, ctx)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib/task/task.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Katmer } from "../katmer"
|
|
2
|
+
import { type KatmerCore } from "../katmer"
|
|
3
|
+
import type { KatmerTargetResolver } from "../target_resolver"
|
|
4
|
+
import { wrapInArray } from "../utils/json.utils"
|
|
5
|
+
import type { ModuleCommonReturn } from "../interfaces/module.interface"
|
|
6
|
+
import { omit, toMerged } from "es-toolkit"
|
|
7
|
+
import type { KatmerProvider } from "../interfaces/provider.interface"
|
|
8
|
+
import { TaskControlKeys, TaskControls } from "./controls"
|
|
9
|
+
import { msToDelta, nowIso } from "../utils/datetime.utils"
|
|
10
|
+
import {
|
|
11
|
+
evalExpr,
|
|
12
|
+
evalObjectVals,
|
|
13
|
+
evalTemplate
|
|
14
|
+
} from "../utils/renderer/renderer"
|
|
15
|
+
import { merge } from "es-toolkit/compat"
|
|
16
|
+
import { TypedEventEmitter } from "../utils/typed-event-emitter"
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
18
|
+
import { cls } from "../utils/cls"
|
|
19
|
+
import { ExecutionFailedError, TaskExecutionFailedError } from "../utils/errors"
|
|
20
|
+
import type { KatmerModule } from "../module"
|
|
21
|
+
|
|
22
|
+
export class KatmerTask extends TypedEventEmitter<{
|
|
23
|
+
"before:execute": [KatmerTask, Katmer.TaskContext]
|
|
24
|
+
"module:check": [KatmerTask, Katmer.TaskContext, KatmerModule]
|
|
25
|
+
"module:init": [KatmerTask, Katmer.TaskContext, KatmerModule]
|
|
26
|
+
"module:execute": [
|
|
27
|
+
KatmerTask,
|
|
28
|
+
Katmer.TaskContext,
|
|
29
|
+
KatmerModule,
|
|
30
|
+
ModuleCommonReturn
|
|
31
|
+
]
|
|
32
|
+
"after:execute": [KatmerTask, Katmer.TaskContext, ModuleCommonReturn]
|
|
33
|
+
}> {
|
|
34
|
+
variables = {} as Record<string, any>
|
|
35
|
+
modules = [] as [string, any][]
|
|
36
|
+
targets: string[]
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
public core: KatmerCore,
|
|
40
|
+
public targetResolver: KatmerTargetResolver,
|
|
41
|
+
public cfg: Katmer.Task
|
|
42
|
+
) {
|
|
43
|
+
super()
|
|
44
|
+
const taskModules = omit(cfg, [
|
|
45
|
+
"name",
|
|
46
|
+
"become",
|
|
47
|
+
"targets",
|
|
48
|
+
"variables",
|
|
49
|
+
...TaskControlKeys
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
this.modules = this.normalizeModules(taskModules)
|
|
53
|
+
|
|
54
|
+
// TODO: use better way for internal modules
|
|
55
|
+
if (cfg.become) {
|
|
56
|
+
this.modules.unshift(["become", cfg.become])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.variables = cfg.variables || {}
|
|
60
|
+
this.targets = wrapInArray(cfg.targets)
|
|
61
|
+
|
|
62
|
+
for (const taskControl of TaskControls) {
|
|
63
|
+
taskControl.register(this, cfg[taskControl.configKey] as any)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async run(context: Record<string, any> = {}) {
|
|
68
|
+
for (const target of this.targets) {
|
|
69
|
+
const providers = this.targetResolver.resolveTargets(target)
|
|
70
|
+
|
|
71
|
+
for (const runFor of providers) {
|
|
72
|
+
const provider = await this.targetResolver.resolveProvider(runFor)
|
|
73
|
+
await this.runForProvider(provider, context)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async runForProvider(
|
|
79
|
+
provider: KatmerProvider,
|
|
80
|
+
context: Record<string, any> = {}
|
|
81
|
+
) {
|
|
82
|
+
await provider.ensureReady()
|
|
83
|
+
const ctx = this.generateTaskContext(provider, context)
|
|
84
|
+
return await cls.run(ctx, async () => {
|
|
85
|
+
return await this.execute(ctx)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async execute(ctx: Katmer.TaskContext) {
|
|
90
|
+
this.emit("before:execute", this, ctx)
|
|
91
|
+
|
|
92
|
+
let lastResult: any
|
|
93
|
+
|
|
94
|
+
const modules = [] as KatmerModule[]
|
|
95
|
+
for (const [moduleName, moduleParams] of this.modules) {
|
|
96
|
+
modules.push(
|
|
97
|
+
this.core.registry.get(moduleName, moduleParams) as KatmerModule
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const module of modules) {
|
|
102
|
+
await module.doCheck(ctx)
|
|
103
|
+
this.emit("module:check", this, ctx, module)
|
|
104
|
+
|
|
105
|
+
ctx.logger.trace(`Initializing module`)
|
|
106
|
+
await module.doInitialize?.(ctx)
|
|
107
|
+
this.emit("module:init", this, ctx, module)
|
|
108
|
+
|
|
109
|
+
ctx.logger.trace(`Executing module`)
|
|
110
|
+
|
|
111
|
+
// Per-module timing
|
|
112
|
+
const startAt = Date.now()
|
|
113
|
+
const startIso = nowIso()
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const res = await module.doExecute(ctx)
|
|
117
|
+
const endAt = Date.now()
|
|
118
|
+
const endIso = nowIso()
|
|
119
|
+
|
|
120
|
+
// Ensure ModuleCommonReturn core fields are present if missing
|
|
121
|
+
if (res.changed === undefined) res.changed = false
|
|
122
|
+
if (res.failed === undefined) res.failed = false
|
|
123
|
+
|
|
124
|
+
if (!res.start) res.start = startIso
|
|
125
|
+
if (!res.end) res.end = endIso
|
|
126
|
+
if (!res.delta) {
|
|
127
|
+
// If module supplied start/end, honor them; otherwise compute
|
|
128
|
+
const started = Date.parse(res.start || startIso) || startAt
|
|
129
|
+
const ended = Date.parse(res.end || endIso) || endAt
|
|
130
|
+
res.delta = msToDelta(ended - started)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lastResult = res
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
const endAt = Date.now()
|
|
136
|
+
const endIso = nowIso()
|
|
137
|
+
|
|
138
|
+
// Convert thrown error into ModuleCommonReturn shape
|
|
139
|
+
lastResult = {
|
|
140
|
+
changed: false,
|
|
141
|
+
failed: true,
|
|
142
|
+
msg:
|
|
143
|
+
typeof err === "string" ? err : (
|
|
144
|
+
err?.message || err?.msg || "Task failed"
|
|
145
|
+
),
|
|
146
|
+
stdout: err?.stdout,
|
|
147
|
+
stderr: err?.stderr,
|
|
148
|
+
start: startIso,
|
|
149
|
+
end: endIso,
|
|
150
|
+
delta: msToDelta(endAt - startAt)
|
|
151
|
+
}
|
|
152
|
+
// Stop at the first failure (keeps current behavior of bubbling errors)
|
|
153
|
+
break
|
|
154
|
+
} finally {
|
|
155
|
+
this.emit("module:execute", this, ctx, module, lastResult)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (lastResult.failed) {
|
|
160
|
+
throw new TaskExecutionFailedError(this, lastResult)
|
|
161
|
+
} else {
|
|
162
|
+
ctx.logger.info({ msg: "Task finished", result: lastResult })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.emit("after:execute", this, ctx, lastResult)
|
|
166
|
+
return lastResult
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected normalizeModules(modules: Record<string, unknown>) {
|
|
170
|
+
const registered = [] as [string, any][]
|
|
171
|
+
|
|
172
|
+
const moduleEntries = Object.entries(modules)
|
|
173
|
+
if (moduleEntries.length === 0) {
|
|
174
|
+
throw new Error("No modules encountered in the task")
|
|
175
|
+
}
|
|
176
|
+
if (moduleEntries.length > 1) {
|
|
177
|
+
throw new Error("Only one module is allowed in the task")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const [moduleName, moduleParams] = moduleEntries[0]
|
|
181
|
+
if (this.core.registry.has(moduleName)) {
|
|
182
|
+
registered.push([moduleName, moduleParams])
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error(`Unknown module encountered: ${moduleName}`)
|
|
185
|
+
}
|
|
186
|
+
return registered
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
generateTaskContext<Provider extends KatmerProvider>(
|
|
190
|
+
provider: Provider,
|
|
191
|
+
context: Record<string, any> = {}
|
|
192
|
+
): Katmer.TaskContext<Provider> {
|
|
193
|
+
const logger = provider.logger.child({ task: this.cfg.name || "" })
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
config: this.core.config,
|
|
197
|
+
exec: provider.executor(),
|
|
198
|
+
async execSafe(...args: [any, any]) {
|
|
199
|
+
try {
|
|
200
|
+
return (await this.exec(...args)) as any
|
|
201
|
+
} catch (err: any) {
|
|
202
|
+
return err
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
fail(msg: string | { message: string }): never {
|
|
206
|
+
throw msg
|
|
207
|
+
},
|
|
208
|
+
log(
|
|
209
|
+
level: "fatal" | "error" | "warn" | "info" | "debug" | "trace",
|
|
210
|
+
message: any
|
|
211
|
+
): void {
|
|
212
|
+
logger[level](message)
|
|
213
|
+
},
|
|
214
|
+
logger: logger,
|
|
215
|
+
progress(data) {
|
|
216
|
+
console.info("Progress:", data)
|
|
217
|
+
},
|
|
218
|
+
provider,
|
|
219
|
+
variables: merge(context, this.variables || {}),
|
|
220
|
+
warn(opts: { message: string } | string): void {
|
|
221
|
+
console.warn("WARN", opts)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ErrorObject } from "ajv"
|
|
2
|
+
|
|
3
|
+
export function normalizeAjvError(error: ErrorObject): string {
|
|
4
|
+
let message: string
|
|
5
|
+
error.message = error.message?.toLowerCase()
|
|
6
|
+
const objPath = error.instancePath?.replace("/", "").replaceAll("/", ".")
|
|
7
|
+
|
|
8
|
+
if (error.keyword === "pattern") {
|
|
9
|
+
message = `"${objPath}" contains invalid characters`
|
|
10
|
+
} else if (error.message?.startsWith("must ") && error.instancePath!.length) {
|
|
11
|
+
message = `"${objPath || ""}" ${error.message}`
|
|
12
|
+
} else {
|
|
13
|
+
message = error.message!
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
error.keyword === "additionalProperties" &&
|
|
18
|
+
error.params?.additionalProperty
|
|
19
|
+
) {
|
|
20
|
+
message = `${message}: "${error.params.additionalProperty}"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return message
|
|
24
|
+
}
|
package/lib/utils/cls.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function nowIso(): string {
|
|
2
|
+
return new Date().toISOString()
|
|
3
|
+
}
|
|
4
|
+
export function msToDelta(ms: number): string {
|
|
5
|
+
// format: H:MM:SS.mmm (e.g., "0:00:00.123")
|
|
6
|
+
const sign = ms < 0 ? "-" : ""
|
|
7
|
+
ms = Math.abs(ms)
|
|
8
|
+
const h = Math.floor(ms / 3600000)
|
|
9
|
+
const m = Math.floor((ms % 3600000) / 60000)
|
|
10
|
+
const s = Math.floor((ms % 60000) / 1000)
|
|
11
|
+
const msRest = ms % 1000
|
|
12
|
+
const pad2 = (n: number) => n.toString().padStart(2, "0")
|
|
13
|
+
const pad3 = (n: number) => n.toString().padStart(3, "0")
|
|
14
|
+
return `${sign}${h}:${pad2(m)}:${pad2(s)}.${pad3(msRest)}`
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderResponse } from "../providers/provider_response"
|
|
2
|
+
import type { Katmer } from "../katmer"
|
|
3
|
+
import type { KatmerTask } from "../task/task"
|
|
4
|
+
|
|
5
|
+
export class KatmerError extends Error {}
|
|
6
|
+
export class ExecutionFailedError extends KatmerError {
|
|
7
|
+
constructor(
|
|
8
|
+
public result: ProviderResponse,
|
|
9
|
+
public message = "Execution failed"
|
|
10
|
+
) {
|
|
11
|
+
super(message)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TaskExecutionFailedError extends ExecutionFailedError {
|
|
16
|
+
task: string | undefined
|
|
17
|
+
constructor(
|
|
18
|
+
task: KatmerTask,
|
|
19
|
+
public result: ProviderResponse,
|
|
20
|
+
public message = "Task execution failed"
|
|
21
|
+
) {
|
|
22
|
+
super(result, message)
|
|
23
|
+
this.task = task.cfg.name
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { uint8ArrayToString } from "uint8array-extras"
|
|
2
|
+
import { safeJsonParse } from "./json.utils"
|
|
3
|
+
|
|
4
|
+
export async function executeScript<T extends boolean = false>(
|
|
5
|
+
scriptContents: string,
|
|
6
|
+
opts: {
|
|
7
|
+
/**
|
|
8
|
+
* Parse stdout/stderr as json. If not valid json the data will be available in `.message`
|
|
9
|
+
*/
|
|
10
|
+
json?: T
|
|
11
|
+
/**
|
|
12
|
+
* If provided will be called with each line of script stdout
|
|
13
|
+
*/
|
|
14
|
+
stream?: (
|
|
15
|
+
err?: (T extends true ? any : string) | null,
|
|
16
|
+
data?: (T extends true ? any : string) | null
|
|
17
|
+
) => void
|
|
18
|
+
} = {}
|
|
19
|
+
) {
|
|
20
|
+
const command = Bun.spawn(["bash", "-s", "<", "<(cat)"], {
|
|
21
|
+
stdin: new Response(scriptContents),
|
|
22
|
+
stdout: "pipe",
|
|
23
|
+
stderr: "pipe"
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (opts.stream) {
|
|
27
|
+
command.stdout.pipeTo(
|
|
28
|
+
new WritableStream({
|
|
29
|
+
write(log) {
|
|
30
|
+
if (log.length) {
|
|
31
|
+
const str = uint8ArrayToString(log).trim()
|
|
32
|
+
if (str) {
|
|
33
|
+
opts.stream?.(undefined, opts.json ? safeJsonParse(str) : str)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
command.stderr.pipeTo(
|
|
41
|
+
new WritableStream({
|
|
42
|
+
write(log) {
|
|
43
|
+
const str = uint8ArrayToString(log).trim()
|
|
44
|
+
if (str) {
|
|
45
|
+
opts.stream?.(opts.json ? safeJsonParse(str) : str)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const failed = (await command.exited) !== 0
|
|
53
|
+
|
|
54
|
+
if (!opts.stream) {
|
|
55
|
+
let stdout = await new Response(command.stdout).text()
|
|
56
|
+
let stderr = await new Response(command.stderr).text()
|
|
57
|
+
if (opts.json) {
|
|
58
|
+
stdout = safeJsonParse(stdout)
|
|
59
|
+
stderr = safeJsonParse(stderr)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (failed) {
|
|
63
|
+
throw stderr
|
|
64
|
+
}
|
|
65
|
+
return stdout
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function executeShellCommand(cmd: string) {
|
|
70
|
+
try {
|
|
71
|
+
const command = Bun.spawn(cmd.split(" "), {
|
|
72
|
+
stdout: "pipe",
|
|
73
|
+
stderr: "pipe"
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const output = {
|
|
77
|
+
code: null as null | number,
|
|
78
|
+
stdout: [] as string[],
|
|
79
|
+
stderr: [] as string[]
|
|
80
|
+
}
|
|
81
|
+
command.stdout.pipeTo(
|
|
82
|
+
new WritableStream({
|
|
83
|
+
write(log) {
|
|
84
|
+
if (log.length) {
|
|
85
|
+
const str = uint8ArrayToString(log).trim()
|
|
86
|
+
if (str) {
|
|
87
|
+
output.stdout.push(str)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
command.stderr.pipeTo(
|
|
95
|
+
new WritableStream({
|
|
96
|
+
write(log) {
|
|
97
|
+
const str = uint8ArrayToString(log)
|
|
98
|
+
output.stderr.push(str)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
output.code = await command.exited
|
|
104
|
+
return output
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const err = e as InstanceType<typeof Bun.$.ShellError>
|
|
107
|
+
if ("exitCode" in err) {
|
|
108
|
+
return {
|
|
109
|
+
code: err.exitCode,
|
|
110
|
+
stdout: [err.stdout.toString("utf-8")],
|
|
111
|
+
stderr: [err.stderr.toString("utf-8")]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw e
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import fs, { exists, mkdir } from "node:fs/promises"
|
|
3
|
+
import { evalTemplate } from "./renderer/renderer"
|
|
4
|
+
import JSON5 from "json5"
|
|
5
|
+
|
|
6
|
+
export function resolveFile(...args: string[]) {
|
|
7
|
+
return path.resolve(process.cwd(), ...args)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function ensureDirectory(filePath: string): Promise<void> {
|
|
11
|
+
const dir = path.dirname(filePath)
|
|
12
|
+
if (!(await exists(dir))) {
|
|
13
|
+
await mkdir(dir, { recursive: true })
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function readKatmerFile(
|
|
18
|
+
filePath: string,
|
|
19
|
+
opts: {
|
|
20
|
+
cwd?: string
|
|
21
|
+
process?: boolean
|
|
22
|
+
processOpts?: any
|
|
23
|
+
errorMessage?: string
|
|
24
|
+
} = {}
|
|
25
|
+
): Promise<Record<string, any>> {
|
|
26
|
+
try {
|
|
27
|
+
const isTwigTemplate = filePath.endsWith(".twig")
|
|
28
|
+
|
|
29
|
+
const baseFile = path.basename(filePath.replace(/\.twig$/, ""))
|
|
30
|
+
|
|
31
|
+
let contents = await fs.readFile(
|
|
32
|
+
path.resolve(opts.cwd || process.cwd(), filePath),
|
|
33
|
+
"utf-8"
|
|
34
|
+
)
|
|
35
|
+
if ((isTwigTemplate && opts.process !== false) || opts.process) {
|
|
36
|
+
contents = await evalTemplate(contents, {}, opts.processOpts)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return await parseKatmerFile(baseFile, contents)
|
|
40
|
+
} catch (e: any) {
|
|
41
|
+
const message = (typeof e === "string" ? e : e.message) || e
|
|
42
|
+
if (opts.errorMessage) {
|
|
43
|
+
throw new Error(`${opts.errorMessage}: ${message}`)
|
|
44
|
+
}
|
|
45
|
+
throw new Error(message)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function parseKatmerFile(filename: string, contents?: string) {
|
|
50
|
+
if (!contents) {
|
|
51
|
+
throw new Error(`No contents provided to parse file: ${filename}`)
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const extension = path.extname(filename)
|
|
55
|
+
if (/\.ya?ml/.test(extension)) {
|
|
56
|
+
return await Bun.YAML.parse(contents)
|
|
57
|
+
} else if (/\.json(c|5|rc)?/.test(extension)) {
|
|
58
|
+
return JSON5.parse(contents)
|
|
59
|
+
} else if (/\.toml/.test(extension)) {
|
|
60
|
+
return Bun.TOML.parse(contents)
|
|
61
|
+
} else {
|
|
62
|
+
throw `Unsupported file type: ${extension}`
|
|
63
|
+
}
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
const message = (typeof e === "string" ? e : e.message) || e
|
|
66
|
+
throw new Error(message)
|
|
67
|
+
}
|
|
68
|
+
}
|