hub#
Corpus-hub helpers for capture clients: ask whether the hub is local or
remote, prove the answer, write it to .env, bootstrap a local hub through
the hub's own scripts, and offer -- to a person -- to run the hub's own
./update. Implements the fleet's ADR-0013, ADR-0015 and the corpus-client
convention (recorded in the contracts repository): never compose, never stop the hub, never a prompt without a
terminal.
Three renderers behind one set of prompts: dialog when installed and both
stdin and stdout are terminals, plain read -p otherwise, and no prompts at
all when stdin or stdout is not a terminal -- then every value must already
be in the environment or the env file, and the failure names the variable it
wanted.
Functions#
- hub_ui_mode
- Purpose: Which renderer applies: prints
dialog,plainornone. - Behavior:
HUB_UI=dialog|plain|noneforces one (tests, scripted runs);HUB_SETUP_PLAIN=1downgradesdialogtoplain; no terminal on stdin or stdout isnone. -
Returns: 0; 1 on an unknown
HUB_UI. -
hub_probe url [timeout_seconds=5]
- Purpose: One
GET url/v1/servicewith a short deadline; prints the body. -
Returns: 0 when the hub answered; 1 otherwise. A trailing slash on the URL is tolerated.
-
hub_probe_field body key
- Purpose: Print one top-level string field of a
/v1/servicebody (name,version,instance_id); nothing when absent. -
Dependencies:
json_get_field(python3 when present; a sed fallback otherwise). -
hub_check_key url key
- Purpose: Prove an API key with an authenticated read,
GET url/v1/documents?limit=1underX-API-Key. - Behavior: The key is handed to curl on stdin (
curl -K -), never on the command line, sopsdoes not show it. A 200 counts as accepted only with a JSONContent-Type(application/jsonorapplication/*+json, parameters allowed, with or without whitespace before the;): a web app or captive portal answering 200 with HTML is not a hub. -
Returns: 0 accepted; 2 the hub answered 401/403 (a wrong key -- not a missing hub); 1 the hub did not answer or answered something else.
-
hub_write_env file key value
- Purpose: Upsert
KEY=valuein an env file, creating it when absent. - Behavior: Rewrites in place (follows a symlink, keeps the inode and the
file's mode) rather than replacing the file; a file it creates is
0600. Refuses a key that is not an environment variable name and a value containing a newline or carriage return; keeps every other line. - Quoting: the file is sourced by
load_env, so values are written in a form the shell, dotenv andresolve_env_valueall read literally. A value made only ofA-Z a-z 0-9 . _ / : @ % + = , - [ ](URLs, base64url keys, ids, plain paths) is written bare,KEY=value. Anything else is single-quoted, or double-quoted when it holds an apostrophe but no$`\". A value no quoting keeps literal for every reader is refused: an apostrophe together with one of those, any doubled backslash (a lonea\\bincluded -- dotenv reads\\in single quotes as one), a trailing backslash (it escapes dotenv's closing quote, and docker compose then rejects the whole file) and${(dotenv expands it even inside single quotes).$HOME,a$band a single backslash inside a value are still written single-quoted. -
Returns: 0; 1 on bad input or a write error.
-
hub_latest_tag clone_dir
- Purpose: Newest semver tag of a clone, fetched from origin first, a
vprefix stripped. Offline, the local tags are what there is. -
Returns: 0 and the tag; 1 when the directory is not a clone.
-
hub_bootstrap clone_dir repo_url
- Purpose: Make a local hub exist through the hub's own scripts.
- Behavior: When
clone_diris absent:git clone repo_url clone_dir, then the hub's./start --configure-superuser(interactive, once) and./install-service; prints theloginctl enable-lingerstep rather than running it. When present:./install-service --dry-runonly. Never runs compose. The repository URL is an argument -- this library names no repository. -
Returns: 0; 1 when the URL is missing and the clone is absent, the clone fails, or the hub's scripts fail.
-
hub_offer_update url clone_dir
- Purpose: Compare the version the running hub reports with the newest
tag in its clone and, when behind, offer the hub's own
./update. - Behavior: With a person (
dialogorplain): ask; on yesexec clone_dir/update. Without one (none): print the versions and the command. WithHUB_MODE=remote: print only -- a remote hub is updated where it runs. An up-to-date hub gets no offer. -
Returns: 0 (a declined or impossible offer is not a failure); 1 on missing arguments. Does not return when
./updateis exec'd. -
hub_setup_dialog env_file [--mode local|remote] [--url URL] [--key KEY] [--hub-dir DIR] [--repo-url URL] [--client NAME]
- Purpose: The install-time question, end to end. Every answer comes from
a flag, else the environment /
env_file(HUB_MODE,HUB_URL,HUB_API_KEY,HUB_DIR,HUB_REPO_URL), else a prompt -- and with no terminal, nothing is asked and the run fails naming the variable. - Behavior:
- remote: URL (validated as
http(s)://host[:port]; a warning for plain http to a non-loopback host) ->hub_probe-> key ->hub_check_key(a 401 is reported as a wrong key and, with a person, offers another) -> writesHUB_MODE,HUB_URL,HUB_API_KEY,HUB_INSTANCE_ID. - local:
hub_bootstrap(clone dir defaults to../$HUB_CLONE_NAME(default../hub) beside the env file's directory; the clone URL must be given when the clone is absent) -> URL defaults tohttp://localhost:<BACKEND_PORT>from the hub's.envorenv.example-> the same probe and key steps; when no key is known it prints the hub's./manage issue_api_keycommand and asks for the result -> also writesHUB_DIR. - Either mode: a
/v1/servicereply with neithernamenorversionis not a hub and fails; aninstance_idoutside[A-Za-z0-9._:-]is warned about and not recorded. With no--hub-dir/HUB_DIRand an env file whose directory does not exist, local mode fails namingHUB_DIRinstead of guessing a location.
- remote: URL (validated as
- Returns: 0 with the hub answering and the key accepted; 1 otherwise (nothing is written before the key is accepted); 2 on an unknown option or a flag missing its value.
- Dependencies: curl, git;
env(env_set_value,resolve_env_value),version(version_offer_update),dialog(ui_*),http,json(json_get_field) -- sourced automatically when not already imported.
Environment#
HUB_UI-- forcedialog,plainornone.HUB_SETUP_PLAIN-- set to anything to prefer plain prompts overdialog.HUB_MODE,HUB_URL,HUB_API_KEY,HUB_INSTANCE_ID,HUB_DIR,HUB_REPO_URL-- read from the environment or the env file; the first four are whathub_setup_dialogwrites.HUB_CLIENT_NAME-- default for--client, used in theissue_api_keyhint.HUB_CLONE_NAME-- directory name of a local hub clone beside the client (defaulthub), when--hub-dir/HUB_DIRis not given.
Examples#
shlib_import hub
# Install time: ask, prove, record. Re-runnable.
HUB_CLONE_NAME=my-hub hub_setup_dialog .env --client "my-scanner" --repo-url "$HUB_REPO_URL"
# CI or a unit: no terminal, so everything comes from the environment.
HUB_MODE=remote HUB_URL=http://203.0.113.7:8000 HUB_API_KEY="$KEY" hub_setup_dialog .env
# Start time, local mode: offer the hub's own ./update when it is behind.
load_env .env
if [[ "${HUB_MODE:-}" == "local" ]]; then
hub_offer_update "$HUB_URL" "${HUB_DIR:-../my-hub}"
fi