Command-line client

weavster without server, test, or config is the command-line client. It talks to a running server over the REST API, either as an interactive shell or running a script file (batch mode). Checking config-as-code files needs no server: weavster config validate FILE... (see Check a document).

# Interactive shell
weavster -a http://127.0.0.1:8080 -u admin -p 'A-Strong-Passw0rd'
weavster> flow list
adt started ADT normalize
weavster> quit

# Batch mode
weavster -a http://127.0.0.1:8080 -u admin -p 'A-Strong-Passw0rd' -s script.txt
Flag Meaning
-a address Server address (default http://127.0.0.1:8080). Include the scheme, and the server's context path if it has one: https://weavster.internal:8443/weavster. With -u/-p and a plain http:// address on another machine, the client warns that the password is sent unencrypted.
-u user, -p password Log in as this user. The credentials are checked at startup. If they are wrong, or the account must change its password first, the client prints Could not log in to server. and the server's reason, then continues (commands then fail with 401 or 403). A password without a user exits 2.
-c file Connection file with the address and credentials (see below). -a, -u, -p, and -ca override its values. A missing or invalid file exits 2.
-ca file For an https address: a PEM file of CA certificates to trust besides the system's, for a server whose certificate comes from a private CA or is self-signed. A missing file, or one without a certificate, exits 2.
-s file Script file: one command per line. Empty lines and lines starting with # are skipped.
-v Print the server's version and exit. It needs -u/-p (or -c), because the server only answers signed-in users; it exits 2 if the server cannot be reached or refuses the credentials.
-h Print usage and exit.
-d Debug mode: error messages also list each underlying cause.

Connection file (-c), YAML with only these keys:

address: https://weavster.internal:8443/weavster
user: admin
password: A-Strong-Passw0rd
ca: /etc/weavster/ca.pem          # optional: a private CA for https (a relative path is next to this file)

Keep it readable only by you (chmod 600), because it holds a password.

Give -a (or address in the connection file) the server's final address. The client does not follow HTTP redirects, so a redirect never carries your password or a message body to another address; it reports the 3xx instead, as an error such as Error: server returned 301 Moved Permanently: …. If a proxy redirects, use the address it redirects to.

Connect over HTTPS

The client speaks TLS 1.2 or later and checks the server's certificate. If the certificate comes from a private CA (or is self-signed), pass that CA:

weavster -a https://weavster.internal:8443 -ca /etc/weavster/ca.pem -u admin -p 'A-Strong-Passw0rd'

Without it, a command fails with the reason and a hint:

Error: Get "https://weavster.internal:8443/api/v1/system": tls: failed to verify certificate: x509: certificate signed by unknown authority
  The server's certificate is not signed by a CA this machine trusts: pass that CA with -ca FILE (or ca: in the connection file).

Other certificate problems get their own hint: a certificate issued for another name (connect with a name it lists), an expired one (renew it, or check the clock), or one refused for another reason, such as a missing server-auth key usage (reissue it). -ca applies to https addresses only; with an http address the client warns that it is not used.

Interactive shell

The shell prints the prompt weavster>, runs each command you type, and shows errors without leaving. quit, exit, or end of input (Ctrl-D) ends it with exit code 0. A line longer than 1 MiB, or a read error, ends it with exit code 2. It accepts the same commands as batch mode.

Batch mode

Commands are split on spaces. Put an argument that contains spaces in double quotes, and write \" and \\ for a quote or backslash inside them: flow rename adt "ADT \"Inbound\"". A missing closing quote is an error. File names are relative to the directory you run weavster from.

Every command in the script runs, even after an error. The client exits 0 when every command succeeded and 2 when any command failed: an unknown command, a usage error, a file that cannot be read, a server that cannot be reached, or an error reply from the server. Server errors are printed as:

Error: server returned 404 Not Found: flow not found

Each command needs the same permission as the API call it makes.

Flow commands

# script.txt
flow create adt.json
flow deploy adt
flow start adt
flow list
Command What it does API call
flow list One line per flow: id, status, and name, separated by tabs. Tabs, newlines, and other control characters in a name are printed as \t, \n, … GET /api/v1/flows
flow get <id> Prints the flow as JSON. GET /api/v1/flows/{id}
flow create <file> Creates a flow from a JSON file (a flow definition). POST /api/v1/flows
flow update <id> <file> Replaces a flow's definition. The flow keeps its status (and enabled unless the file sets it). PUT /api/v1/flows/{id}
flow update-all <file> Replaces several definitions from a {"flows":[…]} file; nothing is written if one is invalid or unknown. PUT /api/v1/flows
flow rename <id> <name> Changes the flow's name to the remaining words, joined by single spaces. It reads the definition and writes it back with the new name, so a change another user makes in between is overwritten (status and enabled are kept). GET then PUT /api/v1/flows/{id}
flow enable <id>, flow disable <id> Sets automatic deployment at startup. POST /api/v1/flows/{id}/enable, …/disable
flow remove <id> Deletes the flow and prints removed <id>. DELETE /api/v1/flows/{id}
flow export <file> [<id>…] Writes an export document (all flows, or the listed ids and their dependencies) to <file>. GET /api/v1/flows/export
flow import <file> [--overwrite] Imports an export document; --overwrite replaces existing flows. POST /api/v1/flows/import
flow deploy <id>, and likewise undeploy, start, stop, pause, halt, resume Changes the flow's lifecycle status. POST /api/v1/flows/{id}/{action}
flow deploy-all, and likewise undeploy-all, start-all, stop-all, pause-all, halt-all, resume-all Runs the action on every flow it applies to and prints changed and skipped (see Flow lifecycle). POST /api/v1/flows/{action}-all
flow redeploy-all Undeploys and redeploys every flow that is not undeployed; each ends deployed. POST /api/v1/flows/redeploy-all
flow stop-destination <id> <destination>, flow start-destination <id> <destination> Holds or releases one destination's deliveries. POST /api/v1/flows/{id}/destinations/{name}/{stop,start}
flow connectors Every flow's source type (its source.type when it has a source, otherwise sourceType) and destination names. GET /api/v1/flows/connector-names
flow ports The ports the server listens on: the API, and the http sources of started flows (flow:<id>). GET /api/v1/flows/ports-in-use
flow reset-stats <id> [lifetime] Clears the flow's current statistics; lifetime also clears its lifetime totals. POST /api/v1/flows/{id}/stats/reset
flow stats [<id>] Statistics of one flow, or of every flow: id received=… filtered=… transformed=… sent=… errored=… queued=…. GET /api/v1/flows/{id}/stats
flow help Lists the flow commands. —

Wherever a command takes <id>, you can also give the flow's name (spec: id|name). The client first uses the argument as an id. Only if the server reports that flow missing does it look the argument up by name (this needs the flows:view permission) and run the command again. An unknown name, or a name several flows share, is an error that lists their ids.

Commands that change a flow print the server's reply (the flow, or the import/update result). flow export and flow import use the export document format.

Deploy, import, and export (spec forms)

Command What it does
exportmessages "path" <flow> Writes an archive of the flow's messages (by id or name; * for every flow), the newest 10,000, to path.
importmessages "path" <flow> Imports an archive file into the flow (existing message ids are skipped).
exportmap "path", importmap "path" Writes the config map to a JSON file, or replaces it with one. See Config map, scripts, and settings.
exportscripts "path", importscripts "path" The same for the global scripts.
snippet list, snippet import "path", snippet export "path", snippet remove <name> Manages code snippets.
snippet library list, snippet library import "path", snippet library export "path", snippet library remove <name> The same for snippet libraries. See Code snippets and libraries.
config validate "path" Checks a config-as-code document (YAML or JSON) on this machine; it needs no server or login. See Config-as-code documents.
config diff "path", config plan "path" Shows what applying the document would change: diff as text (+, ~ with changed values, -), plan as JSON. Nothing changes.
config apply "path" [--dry-run] [reason…] Plans the document, prints the plan, and applies it; refused if the server changed meanwhile, undone completely if a change fails. See Apply.
exportcfg "path" [overwriteconfigmap], importcfg "path" [nodeploy] [overwriteconfigmap] [force] Exports or imports the whole configuration (flows, alerts, snippets, scripts, settings, and optionally the config map). See Export and import the whole configuration.
importalert "path" [force], exportalert <id, "name", or *> "path" Imports alerts from a JSON file (force replaces existing ids), or exports one alert or all of them. See Alerts.
deadletter list [flow], deadletter show <id> Lists dead-lettered messages (each destination's attempts and last error), or shows one as JSON.
deadletter requeue <id>, deadletter requeue all [flow] Gives dead-lettered messages another round of delivery attempts; delivered destinations are not sent again. See Dead-lettered messages.
deadletter remove <id> Deletes a dead-lettered message (refuses any other status).
clearallmessages Removes every message. Started flows are stopped first and started again afterwards; the output names them, and counts messages kept because they were being processed.
dump stats "path", dump events "path" Writes every flow's statistics, or the newest 10,000 events, to a JSON file.
resetstats [lifetime] Clears every flow's current statistics; lifetime also clears the lifetime totals.
deploy [timeout] Deploys every flow that is enabled and undeployed (dependencies first, as flow deploy does); disabled flows are skipped, as at server start. Prints deployed <id> for each and deployed N flows. After timeout seconds no further flow is started (a deploy already sent finishes) and the command exits 2. A flow that fails is reported, the others still deploy, and the command exits 2.
import "path" [force] Same as flow import; force replaces existing flows.
export <id> "path", export "name" "path", export * "path" Same as flow export: one flow (by id or name) and its dependencies, or * for all flows.
deploy 60
export * "backups/all flows.json"
import "backups/all flows.json" force

Other commands

Command What it does
status Lists deployed flows (every status except undeployed): id, status, and name per line, or no deployed flows.
version Prints the client version.
user list One line per account: username, permissions, and must change password / locked when they apply (permission users:admin).
user add <name> <password> [permission…] Creates an account; the user must change the password at the first login.
user remove <name> Deletes an account.
user changepw <name> <password> Sets an account's password; the user must change it at the next login.
help Lists the commands.
quit, exit End the interactive shell; ignored in batch mode.

Test your flows (weavster test)

weavster test runs your flows' transforms against sample messages, on your machine: no server and no database. Keep sample messages and what each flow must make of them in fixture files next to your config-as-code documents, and run the command in CI before config apply.

weavster test --format junit --output artifacts/ .

It looks under each PATH (default: the current directory) for:

  • config-as-code documents: *.yaml, *.yml, or *.json files with version: "1", or without a version when every top-level key is a config section (flows, alerts, …). Their flows are the ones the fixtures test. Other YAML and JSON (a compose file, a CI workflow, package.json) is left alone.
  • fixture files: *.test.yaml, *.test.yml, or *.test.json.

Hidden directories, node_modules, and directories it cannot read are skipped, and a file under two of the paths is read once.

A fixture file names one flow and lists cases:

# tests/adt.test.yaml
flow: adt
cases:
  - name: admit
    inputFile: a01.hl7              # relative to this file; or `input:` with the message inline
    expect:
      output: {patient: {lastName: DOE, mrn: "12345"}}
      excluded: []
      destinations:
        his: {output: {id: "12345"}}
  - name: update is filtered
    input: "MSH|^~\\&|LAB|H|EHR|H|20240101120000||ADT^A08|2|P|2.5\rPID|1||12345^^^MRN||DOE^JOHN\r"
    expect: {status: filtered}
  - name: not hl7
    input: hello
    expect: {status: errored, error: HL7 v2}

Each case runs the message through the flow exactly as the server processes it: read with the flow's inputFormat, then its transform (filters, maps, sets, destinationSet, a build step), then the own transform of each destination the message reaches. Nothing is stored or delivered.

expect key Checks Default
status transformed, filtered (a filter step dropped it), or errored (the input could not be read, or a step failed) transformed
error Text the error must contain (with status: errored) not checked
output The flow's JSON output has these fields with these values; fields you leave out are ignored, arrays and values must be equal. A field expected as null must be there, with null not checked
outputText The flow's output, exactly (for a build step's HL7 v2, XML, or text) not checked
outputFile The flow's output, exactly as in this file (relative to the fixture file); instead of outputText not checked
excluded The destinations destinationSet steps left out ([]: none) not checked
destinations.NAME That destination's own transform: status, error, output, outputText, outputFile as above not checked

Each case is named <fixture file without .test.yaml>/<case name>, for example tests/adt/admit. The built-in codec checks also run, named identity/…: each parses a sample of a data format (HL7 v2, JSON, XML, delimited, raw) and must write it back byte for byte.

$ weavster test --format json tests
FAIL tests/adt/admit: output differs at patient.lastName: it is {"patient":{"lastName":"DOE",…}}, want the fields {"patient":{"lastName":"SMITH"}}
[
  {"name": "identity/hl7", "passed": true},
  …
  {"name": "tests/adt/admit", "passed": false, "failure": "output differs at patient.lastName: …"}
]
  • --format junit (default) writes JUnit XML, --format json JSON; --output DIR writes results.xml or results.json into DIR instead of printing it. Failures are also printed to stderr, one line each (FAIL name: reason).
  • --filter TEXT runs only the cases whose name contains TEXT (others are not run at all). If none does, the command fails with no case matches --filter "TEXT".
  • A fixture that cannot be run fails with the reason: a flow no document defines, a flow defined in two documents, an unknown key (field casez not found), more than one YAML document in the file, a case without a name, or a missing inputFile. A config-as-code document that does not parse fails the run as well, whatever --filter selects.
  • Numbers in output compare by exact value: 1 and 1.0 are equal, but two 20-digit ids that differ in the last digit are not.

A complete example

The repository's examples/golden directory has one case per data format: HL7 v2 to JSON and to HL7 v2, XML to JSON and to XML (and one that is filtered), delimited text to JSON, and JSON to JSON and to text. Each case directory holds a one-flow weavster.json, the sample input.*, a fixture, and the expected.* output (except the filtered case, whose fixture expects status: filtered and has no output):

# examples/golden/hl7v2-to-json/hl7v2-to-json.test.yaml
flow: hl7v2-to-json
cases:
  - name: golden
    inputFile: input.hl7
    expect: {outputFile: expected.json}
weavster test --format junit --output artifacts/ examples/golden

Weavster's own CI runs weavster test --format junit --output artifacts/ examples/ (every example under examples/, these included), keeps results.xml, and sends the same cases through a running server to check that it produces the same outputs. Each case's fixture file is the one statement of what it expects, for both runs.

Pitfalls

  • A fixture only sees the documents under the paths you give. Pass the directory that holds both (weavster test .), not just the tests directory.
  • output compares JSON values: "12345" (text) and 12345 (a number) differ. HL7 v2 fields are text.
  • A flow without a transform passes messages through unchanged, so any input is transformed.

Run the server (weavster server)

weavster server --config /etc/weavster/weavster-server.yaml
weavster server --config weavster-server.yaml 127.0.0.1:9090   # the address overrides listen.address
weavster server                                                  # defaults: 127.0.0.1:8080, memory store
  • --config FILE reads the server configuration; without it the defaults apply (a memory store: nothing is kept after the server stops).
  • An address after the flags replaces listen.address from the file.
  • The server runs until SIGINT or SIGTERM, then drains in-flight messages for up to listen.shutdownTimeoutMs and exits 0. It refuses to run as root unless WEAVSTER_ALLOW_ROOT=1 is set.

Deprecated command names

Scripts written for older tools can keep their command names. A deprecated name prints a warning on stderr and then runs its replacement with the same arguments; the warning does not change the exit code.

Deprecated Runs
channel … flow …
codetemplate … snippet …
weavster> channel list
Warning: "channel" is deprecated; use "flow"
adt started ADT Inbound

Update your scripts to the new names; the old ones may be removed in a later release.

Exit codes and errors

Command 0 1 2
weavster -s script (batch) Every command succeeded — Any command failed (the script still runs to the end), a line longer than 1 MiB (the script stops there), an unknown flag, or a missing connection file
weavster (interactive shell) quit, exit, or end of input, even after failed commands (their errors are shown) — A line longer than 1 MiB, a read error, an unknown flag, or a missing connection file
weavster server -h, or a clean stop on SIGINT/SIGTERM The configuration is invalid, the server could not start (store, TLS, bootstrap), or it runs as a privileged user without WEAVSTER_ALLOW_ROOT=1 An unknown flag or extra arguments
weavster test Every case passed, or -h A case or fixture failed, a config-as-code document did not parse, or --filter matched nothing An unknown flag, --format other than junit or json, a PATH that does not exist, or the results could not be written
weavster version Always (prints the binary's version, build date, Go version, and platform; no server needed) — Extra arguments
weavster config validate FILE... Every file is valid, or -h A file is invalid No file given, a file cannot be read or is larger than 50 MiB, or another config command (diff, plan, and apply need a server: run them in the shell or with -s)

-h (or --help) prints usage and exits 0 for every command. Usage errors are checked before anything else runs.

Errors go to stderr and start with Error:; an unknown flag is followed by the list of flags (Error: flag provided but not defined: -nope). A reply from the server shows its status and message, for example Error: server returned 404 Not Found: snippet not found. With -d, the client also prints each underlying cause:

Error: Get "http://127.0.0.1:8080/api/v1/flows": dial tcp 127.0.0.1:8080: connect: connection refused
  caused by *net.OpError: dial tcp 127.0.0.1:8080: connect: connection refused
  caused by *os.SyscallError: connect: connection refused
  caused by syscall.Errno: connection refused

A failed login prints Could not log in to server. and continues (the prompt or the script); the commands that follow then fail with the server's 401 reply.