Add deploy.sh, systemd unit template, and pipekit secrets CLI.
deploy.sh is the idempotent rollout path: venv + deps, launcher,
/etc/pipekit/secrets.env skeleton (mode 0600), schema init, and
auto-register of every JDBC driver shipped with jrunner. systemd
unit is a template, not auto-installed — user copies it when ready
to cut over.
`pipekit secrets {list,set,unset}` manages /etc/pipekit/secrets.env
with atomic 0600 writes so passwords don't need sudoedit. Prompted
input by default; positional value allowed for scripting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e27167a4a3
commit
e6a615bf70
101
deploy.sh
Executable file
101
deploy.sh
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pipekit deployment — idempotent. Re-run any time.
|
||||||
|
#
|
||||||
|
# Steps:
|
||||||
|
# 1. Check prerequisites (python3, jrunner on PATH)
|
||||||
|
# 2. Create Python venv at $REPO/.venv and install requirements
|
||||||
|
# 3. Install launcher at /usr/local/bin/pipekit (wraps the venv python)
|
||||||
|
# 4. Ensure /etc/pipekit/secrets.env exists (mode 0600, placeholder body)
|
||||||
|
# 5. Run `pipekit init` to create/upgrade the SQLite schema
|
||||||
|
# 6. Register driver rows for every JDBC jar shipped with jrunner
|
||||||
|
#
|
||||||
|
# After running:
|
||||||
|
# - Set DB passwords with: sudo pipekit secrets set <KEY>
|
||||||
|
# - See systemd/pipekit.service for a unit file template
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="${PIPEKIT_REPO:-$(cd "$(dirname "$0")" && pwd)}"
|
||||||
|
VENV_DIR="$REPO_DIR/.venv"
|
||||||
|
LAUNCHER="/usr/local/bin/pipekit"
|
||||||
|
CONFIG_DIR="/etc/pipekit"
|
||||||
|
SECRETS_FILE="$CONFIG_DIR/secrets.env"
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
exec sudo -E "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== pipekit deploy =="
|
||||||
|
echo "repo: $REPO_DIR"
|
||||||
|
echo "venv: $VENV_DIR"
|
||||||
|
echo "secrets: $SECRETS_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
command -v python3 >/dev/null || { echo "ERROR: python3 not on PATH"; exit 1; }
|
||||||
|
command -v jrunner >/dev/null || { echo "ERROR: jrunner not on PATH — install /opt/jrunner first"; exit 1; }
|
||||||
|
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "Creating venv at $VENV_DIR"
|
||||||
|
python3 -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
|
||||||
|
"$VENV_DIR/bin/pip" install --quiet -r "$REPO_DIR/requirements.txt"
|
||||||
|
echo "Python deps installed."
|
||||||
|
|
||||||
|
cat > "$REPO_DIR/bin/pipekit" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
exec "$VENV_DIR/bin/python3" -m pipekit "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$REPO_DIR/bin/pipekit"
|
||||||
|
ln -sf "$REPO_DIR/bin/pipekit" "$LAUNCHER"
|
||||||
|
echo "Launcher: $LAUNCHER -> $REPO_DIR/bin/pipekit"
|
||||||
|
|
||||||
|
install -d -m 0755 "$CONFIG_DIR"
|
||||||
|
if [ ! -f "$SECRETS_FILE" ]; then
|
||||||
|
install -m 0600 /dev/null "$SECRETS_FILE"
|
||||||
|
cat > "$SECRETS_FILE" <<'EOF'
|
||||||
|
# pipekit secrets — sourced by the service process (EnvironmentFile=)
|
||||||
|
# or by the shell before `pipekit serve`. One KEY=VALUE per line.
|
||||||
|
# Connection rows reference these as $KEY (e.g. password: "$DB2PW").
|
||||||
|
#
|
||||||
|
# This file must stay mode 0600 and out of version control.
|
||||||
|
# Use `sudo pipekit secrets set <KEY>` to add entries safely.
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$SECRETS_FILE"
|
||||||
|
echo "Created $SECRETS_FILE"
|
||||||
|
else
|
||||||
|
echo "Keeping existing $SECRETS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$LAUNCHER" init
|
||||||
|
|
||||||
|
# Register drivers for each JDBC jar jrunner ships with.
|
||||||
|
JR_LIB="$(dirname "$(readlink -f "$(command -v jrunner)")")/../lib"
|
||||||
|
register_jar() {
|
||||||
|
local kind="$1" pattern="$2"
|
||||||
|
local jar
|
||||||
|
jar="$(find "$JR_LIB" -maxdepth 1 -name "$pattern" 2>/dev/null | head -1)"
|
||||||
|
if [ -n "$jar" ]; then
|
||||||
|
"$LAUNCHER" drivers register "$kind" --jar "$jar"
|
||||||
|
else
|
||||||
|
echo " (no $pattern in $JR_LIB — skipping $kind)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
register_jar db2 "jt400-*.jar"
|
||||||
|
register_jar pg "postgresql-*.jar"
|
||||||
|
register_jar mssql "mssql-jdbc-*.jar"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "pipekit deployed."
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Set passwords: sudo pipekit secrets set DB2PW"
|
||||||
|
echo " sudo pipekit secrets set PGPW"
|
||||||
|
echo " 2. Start the server manually:"
|
||||||
|
echo " set -a; source $SECRETS_FILE; set +a"
|
||||||
|
echo " pipekit serve"
|
||||||
|
echo " 3. Or install the systemd unit:"
|
||||||
|
echo " sudo cp $REPO_DIR/systemd/pipekit.service /etc/systemd/system/"
|
||||||
|
echo " sudo systemctl daemon-reload"
|
||||||
|
echo " sudo systemctl enable --now pipekit"
|
||||||
116
pipekit/cli.py
116
pipekit/cli.py
@ -155,6 +155,101 @@ def cmd_set_password(args) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_SECRETS_FILE = "/etc/pipekit/secrets.env"
|
||||||
|
|
||||||
|
|
||||||
|
def _secrets_path(args) -> str:
|
||||||
|
import os
|
||||||
|
return (getattr(args, "file", None)
|
||||||
|
or os.environ.get("PIPEKIT_SECRETS")
|
||||||
|
or _DEFAULT_SECRETS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_secrets_list(args) -> int:
|
||||||
|
import os
|
||||||
|
path = _secrets_path(args)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"{path}: no such file")
|
||||||
|
return 0
|
||||||
|
keys: list[str] = []
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
s = line.strip()
|
||||||
|
if not s or s.startswith("#") or "=" not in s:
|
||||||
|
continue
|
||||||
|
keys.append(s.split("=", 1)[0])
|
||||||
|
print(f"{path} — {len(keys)} secret(s):")
|
||||||
|
for k in keys:
|
||||||
|
print(f" {k}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_secrets_set(args) -> int:
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
path = _secrets_path(args)
|
||||||
|
value = args.value if args.value is not None else getpass.getpass(
|
||||||
|
f"value for {args.key}: ")
|
||||||
|
if not value:
|
||||||
|
print("error: empty value")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
replaced = False
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
s = line.strip()
|
||||||
|
if s and not s.startswith("#") and "=" in s \
|
||||||
|
and s.split("=", 1)[0] == args.key:
|
||||||
|
lines.append(f"{args.key}={value}\n")
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
if not replaced:
|
||||||
|
if lines and not lines[-1].endswith("\n"):
|
||||||
|
lines.append("\n")
|
||||||
|
lines.append(f"{args.key}={value}\n")
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
tmp = path + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
os.chmod(tmp, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||||
|
os.replace(tmp, path)
|
||||||
|
print(f"{'updated' if replaced else 'added'} {args.key} in {path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_secrets_unset(args) -> int:
|
||||||
|
import os
|
||||||
|
path = _secrets_path(args)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"{path}: no such file")
|
||||||
|
return 1
|
||||||
|
kept: list[str] = []
|
||||||
|
removed = False
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
s = line.strip()
|
||||||
|
if s and not s.startswith("#") and "=" in s \
|
||||||
|
and s.split("=", 1)[0] == args.key:
|
||||||
|
removed = True
|
||||||
|
continue
|
||||||
|
kept.append(line)
|
||||||
|
if not removed:
|
||||||
|
print(f"{args.key} not set in {path}")
|
||||||
|
return 0
|
||||||
|
tmp = path + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
f.writelines(kept)
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
print(f"removed {args.key} from {path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _report(checks) -> int:
|
def _report(checks) -> int:
|
||||||
width = max(len(name) for name, _, _ in checks)
|
width = max(len(name) for name, _, _ in checks)
|
||||||
failures = 0
|
failures = 0
|
||||||
@ -224,6 +319,27 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
p_pw.add_argument("username")
|
p_pw.add_argument("username")
|
||||||
p_pw.set_defaults(func=cmd_set_password)
|
p_pw.set_defaults(func=cmd_set_password)
|
||||||
|
|
||||||
|
p_sec = sub.add_parser(
|
||||||
|
"secrets",
|
||||||
|
help=f"manage the env-var file for DB passwords (default {_DEFAULT_SECRETS_FILE})")
|
||||||
|
sec_sub = p_sec.add_subparsers(dest="sec_cmd", required=True)
|
||||||
|
|
||||||
|
p_sec_list = sec_sub.add_parser("list", help="list keys in the secrets file")
|
||||||
|
p_sec_list.add_argument("--file", help=f"override path (default {_DEFAULT_SECRETS_FILE})")
|
||||||
|
p_sec_list.set_defaults(func=cmd_secrets_list)
|
||||||
|
|
||||||
|
p_sec_set = sec_sub.add_parser("set", help="add or update a KEY (prompted if value omitted)")
|
||||||
|
p_sec_set.add_argument("key")
|
||||||
|
p_sec_set.add_argument("value", nargs="?", default=None,
|
||||||
|
help="value; omit to be prompted (safer)")
|
||||||
|
p_sec_set.add_argument("--file", help=f"override path (default {_DEFAULT_SECRETS_FILE})")
|
||||||
|
p_sec_set.set_defaults(func=cmd_secrets_set)
|
||||||
|
|
||||||
|
p_sec_unset = sec_sub.add_parser("unset", help="remove a KEY from the secrets file")
|
||||||
|
p_sec_unset.add_argument("key")
|
||||||
|
p_sec_unset.add_argument("--file", help=f"override path (default {_DEFAULT_SECRETS_FILE})")
|
||||||
|
p_sec_unset.set_defaults(func=cmd_secrets_unset)
|
||||||
|
|
||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
return args.func(args)
|
return args.func(args)
|
||||||
|
|
||||||
|
|||||||
29
systemd/pipekit.service
Normal file
29
systemd/pipekit.service
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Pipekit systemd unit template.
|
||||||
|
#
|
||||||
|
# Install:
|
||||||
|
# sudo cp pipekit.service /etc/systemd/system/
|
||||||
|
# sudo systemctl daemon-reload
|
||||||
|
# sudo systemctl enable --now pipekit
|
||||||
|
#
|
||||||
|
# Runs as root by default. For a dedicated service account, create the
|
||||||
|
# user and uncomment User=/Group= below:
|
||||||
|
# sudo useradd --system --home-dir /opt/pipekit --shell /usr/sbin/nologin pipekit
|
||||||
|
# sudo chown -R pipekit:pipekit /opt/pipekit/pipekit.db /etc/pipekit
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Pipekit sync engine
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# User=pipekit
|
||||||
|
# Group=pipekit
|
||||||
|
WorkingDirectory=/opt/pipekit
|
||||||
|
EnvironmentFile=/etc/pipekit/secrets.env
|
||||||
|
ExecStart=/usr/local/bin/pipekit serve --host 0.0.0.0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Reference in New Issue
Block a user