A simple single-file bash library of idempotent functions useful for configuration management.
  • Shell 59.7%
  • TypeScript 39.5%
  • Just 0.8%
Find a file
Erik Stephens c70f77b6a2
All checks were successful
test / unit (push) Successful in 25s
state the mariadb client requirement as design, not as a usql bug report
The paragraph existed to document xo/usql#587 - a backtick identifier holding a single quote flipped usql's client-side lexer, silently corrupting the next string literal. That is fixed upstream now, and the reason for requiring the vendor clients was never really that bug: they hand the statement to the server verbatim, so the server is the only thing that parses it, and any client that re-lexes gets to disagree with it about quoting. Say that instead.

Also records that either client drives either server, verified against MariaDB 12.3 and MySQL 8.4 in all four combinations, and that MariaDB 12 dropped the `mysql*` compatibility symlinks - which is what makes the fallback to `mysql` the MySQL-host path rather than dead code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Rxw4uRRDVZ9tCUFwN8DFC
2026-09-07 18:29:24 -07:00
.claude a deno+dax port 2026-04-21 19:51:23 -07:00
.forgejo/workflows fix tests and s/make/just/ 2026-05-16 10:46:16 -07:00
openspec install-service: take an explicit unit path, split timers out 2026-09-05 14:30:56 -07:00
test add idem-brew-setup to install homebrew for a user 2026-09-07 06:59:02 -07:00
.gitmodules unit tests 2026-03-16 23:07:50 -07:00
deno.json add mariadb primitives to idem.sh and idem.ts 2026-09-06 16:53:20 -07:00
deno.lock a deno+dax port 2026-04-21 19:51:23 -07:00
idem.sh state the mariadb client requirement as design, not as a usql bug report 2026-09-07 18:29:24 -07:00
idem.ts state the mariadb client requirement as design, not as a usql bug report 2026-09-07 18:29:24 -07:00
Justfile add mariadb primitives to idem.sh and idem.ts 2026-09-06 16:53:20 -07:00
LICENSE initial commit 2025-10-29 21:43:26 -07:00
README.md state the mariadb client requirement as design, not as a usql bug report 2026-09-07 18:29:24 -07:00

idem.sh

A simple single-file bash library of idempotent functions useful for configuration management.

Tests

Motivation

I was a Señor BigEng Weanie that hungered not just to develop platforms, but be even more meta and develop platforms that produced platforms. Then reality hit me hard bro, and that's coming from someone that grew up near the projects in Glendale, AZ. Translation: "git-r-done before you lose ur job, son" - actually that's my redneck upbringing talkin'. Whether or not I'm speaking your kind of language, stick around for a hot minute to see if anything here worth cherry-picking.

As practioners / developers / programmers / engineers / solutions architects / whatev's, it is up to us to choose the right tool for the job. This is my humble submission as a (not "the") tool to help manage a small fleet of servers - correction: don't see it as a tool but more as a template.

It was inspired by Bash Booster. I tried to introduce it at one org that puckered up when mentioning things like "change control" or "automated, reproducible builds". A decade later after working with "big boy" tooling, I struggled to choose how to manage my personal infrastructure. Managing Kubernetes clusters full time for work had me longing for something that allowed me to focus on more important-to-me things in my spare time instead of keeping up with k8s updates & their indifference to us small-timers without Big NAS solutions.

Usage

Simply "install" this however you see fit. The idea is that you clone & taylor this library to your uses & environments. If looking for more concrete examples, please read this blog post.

idem.ts

A TypeScript port of idem.sh for Deno, using dax for shell execution.

#!/usr/bin/env -S deno run --allow-all
import createIdem from "./idem.ts";

const ctx = createIdem({ dryrun: false }); // dryrun: true by default (safe audit mode)

await ctx.mkdir("/etc/myapp");
await ctx.cp("./config/myapp.conf", "/etc/myapp/myapp.conf");
await ctx.enableUnit("myapp.service");
await ctx.installPkgs("curl", "jq");

if (ctx.hasDrift()) {
  console.log("Configuration was updated.");
}

Each function returns Promise<"unchanged" | "drift" | "failed">. With dryrun: true (the default), drift is detected and logged but no changes are applied.

Postgres

idem.sh and idem.ts both carry a set of postgres primitives - roles, databases, extensions and database-level grants. They shell out to psql as a superuser over PGHOST/PGPORT/PGSUPERUSER (default localhost:5432 and postgres), leaving authentication to ~/.pgpass or PGPASSWORD. An unreachable cluster is a quiet no-op during a dry run and a failure when applying, so drift checks on hosts without postgres stay silent.

idem-pg-role atuin password=$PW      # also: superuser, createdb, nologin
idem-pg-db atuin owner=atuin
idem-pg-extension atuin pg_trgm
idem-pg-grant atuin reader CONNECT TEMP
await ctx.pgRole("atuin", { password: pw });
await ctx.pgDb("atuin", { owner: "atuin" });
await ctx.pgExtension("atuin", "pg_trgm");
await ctx.pgGrant("atuin", "reader", "CONNECT", "TEMP");

Password hashes can't be compared, so a supplied password is probed by logging in with it and only reset when the server rejects it. Exercise both implementations against a throwaway cluster in docker with just test-pg.

MariaDB

The same shape for MariaDB/MySQL - accounts, databases and database-level grants. They shell out to the mariadb client (falling back to mysql, override with MARIADB_CLIENT) as a superuser over MARIADB_HOST/MARIADB_PORT/MARIADB_SUPERUSER (default localhost:3306 and root), leaving authentication to ~/.my.cnf or MYSQL_PWD. Accounts are named user@host; a bare name means user@'%'.

idem-maria-user app@localhost password=$PW
idem-maria-db appdb charset=utf8mb4 collate=utf8mb4_unicode_ci
idem-maria-grant appdb app@localhost SELECT INSERT
idem-maria-grant '*' reporter PROCESS      # a db of * is a global grant
await ctx.mariaUser("app@localhost", { password: pw });
await ctx.mariaDb("appdb", { charset: "utf8mb4", collate: "utf8mb4_unicode_ci" });
await ctx.mariaGrant("appdb", "app@localhost", "SELECT", "INSERT");
await ctx.mariaGrant("*", "reporter", "PROCESS");

Only the vendor clients are supported. They hand the statement to the server verbatim, leaving it the only thing that parses it; a DSN client that re-lexes it client-side gets its own opinion about quoting, and the odd-name cases these primitives exist to get right are exactly where the two disagree. Either client drives either server - MariaDB 12 dropped the mysql* compatibility symlinks, so the fallback to mysql is now the MySQL-host path. just test-maria needs no client on the host, borrowing the container's when there is none.

Passwords are probed the same way postgres roles are. ALL PRIVILEGES is refused - there is no reliable way to tell whether it is already held, so name the privileges you want. Exercise both implementations against a throwaway server in docker with just test-maria.

Type-check with:

deno task check

Caveats

This is old school hacker land: enjoy yourself and take only what you need from it. Copy, paste, and embellish as needed. Do not expect any kind of support, even entertaining your questions - we all got more important things in our lives. If your question/bug/fix is worthy, then the Internet God will bless you with a response, and if you're lucky, maybe a PR. The org behind this repo is not interested in building a community nor becoming rich & famous.