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
|
||||
|
||||
|
||||
_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:
|
||||
width = max(len(name) for name, _, _ in checks)
|
||||
failures = 0
|
||||
@ -224,6 +319,27 @@ def main(argv: list[str] | None = None) -> int:
|
||||
p_pw.add_argument("username")
|
||||
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)
|
||||
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