Karl logo karl docs / idiomatic

Idiomatic Karl for DevOps Tooling

Karl code is best when the dataflow is explicit and concise: process handles for external tooling, stream operators for transformation, and recover blocks only at failure boundaries.

1. Principles

2. Env + Config

let ns = trim(env("K8S_NS")) ? "default"
let pollMs = parseInt(trim(env("POLL_MS"))) ? 1000

let cfg = fromJson(readFile("config.json")) ? {
    { mode: "safe", retries: 3, }
}

3. Use run() for command wrappers

let st = run("kubectl", "get", "ns", { maxOutputBytes: 32768, }) ? {
    fail("kubectl invocation failed: " + error.message)
}
if !st.ok { fail("kubectl exit " + str(st.code) + ": " + st.error) }

let names = st.output.split("\n").filter(_ != "")
log({ count: len(names), })

4. Use proc() for long-lived jobs

let p = proc(
    "kubectl", "logs", "-f", "deploy/api",
    { stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
)

for true with pair = p.stdout.read() {
    let [line, eof] = pair
    if eof { break () }
    if line.contains("ERROR") { logt("error", line) }
    pair = p.stdout.read()
} then ()

wait p ? {}

5. Stream-first transforms

let p = proc(
    "kubectl", "get", "pods", "-A",
    "-o", "jsonpath={range .items[*]}{.status.phase}{\"\\n\"}{end}",
    { stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
)

let phases = p
    | lines()
    | filter(_ != "")
    | group_count()

wait p
log(phases)

6. Live matrix loop (one-statement fan-in)

merge(
  listPods()
    .map(pod ->
      proc(
        "kubectl", "logs", "-f", "-n", ns, pod, "--all-containers=true", "--tail=100",
        { stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
      )
      | lines()
      | filter(_ != "")
      | map(line -> { pod, level: levelOf(line), })
    )
) | forEach(event -> {
    bump(counts, event.pod, event.level)
    if now() - last >= matrixMs {
        render(counts)
        last = now()
    }
})

7. SQL boundaries

let db = sqlOpen(dsn) ? { fail("db open: " + error.message) }

let row = sqlQueryOne(db,
    "SELECT id, email FROM users WHERE id = $1",
    [userId]
) ? { fail("query failed: " + error.message) }

let result = match row {
    case null -> { found: false, }
    case r -> { found: true, id: r.id, email: r.email, }
}

sqlClose(db)
log(result)

8. Graceful shutdown pattern

let worker = spawn (() -> {
    for true with running = true {
        sleep(1000)
        tick()
    } then ()
})()

let sigs = signalWatch(["SIGINT", "SIGTERM"])
let [sig, closed] = sigs.recv()
if !closed { log("shutdown:", sig) }

worker.cancel()
wait worker ? {}

9. Conciseness rules

// good
let mkJSONHeaders = () -> map().set("Content-Type", "application/json")
let jsonResponse = (status, payload) -> { status: status, headers: mkJSONHeaders(), body: toJson(payload), }

// avoid redundant wrappers
// let msToRFC3339 = ms -> timeFormatRFC3339(ms)

10. Anti-patterns