mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
Migrate Ansible-based scripts and documentation to 1Password integration. Removed deprecated and redundant Ansible vault workflows, added 1Password CLI handling for secrets management, and updated tests to reflect the change.
This commit is contained in:
parent
55b6a6aff3
commit
b421f49af1
@ -64,7 +64,7 @@ It packages the core building blocks needed for a modern internal developer plat
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
90
config.py
90
config.py
@ -730,96 +730,6 @@ def _extract_yaml_scalar_from_text(text: str, key: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _extract_inline_vault_block(text: str, key: str) -> str:
|
|
||||||
lines = text.splitlines()
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith(f"{key}:"):
|
|
||||||
base_indent = len(line) - len(line.lstrip())
|
|
||||||
block = []
|
|
||||||
i += 1
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i]
|
|
||||||
if not line.strip():
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
indent = len(line) - len(line.lstrip())
|
|
||||||
if indent <= base_indent:
|
|
||||||
break
|
|
||||||
block.append(line.strip())
|
|
||||||
i += 1
|
|
||||||
if block and block[0].startswith("$ANSIBLE_VAULT"):
|
|
||||||
return "\n".join(block) + "\n"
|
|
||||||
return ""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
|
||||||
if not vault_path.exists():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Simple search in plain text first
|
|
||||||
try:
|
|
||||||
raw_text = vault_path.read_text()
|
|
||||||
plain = _extract_yaml_scalar_from_text(raw_text, key)
|
|
||||||
if (
|
|
||||||
plain
|
|
||||||
and not plain.startswith("$ANSIBLE_VAULT")
|
|
||||||
and not plain.startswith("!vault")
|
|
||||||
):
|
|
||||||
return plain
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
password_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
if not password_file:
|
|
||||||
candidate = PROJECT_ROOT / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
password_file = str(candidate)
|
|
||||||
|
|
||||||
if not password_file or shutil.which("ansible-vault") is None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _vault_view(path: str) -> str:
|
|
||||||
try:
|
|
||||||
res = subprocess.run(
|
|
||||||
["ansible-vault", "view", path, "--vault-password-file", password_file],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
env=os.environ.copy(),
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
if res.returncode == 0:
|
|
||||||
return res.stdout or ""
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = _vault_view(str(vault_path))
|
|
||||||
if output:
|
|
||||||
return _extract_yaml_scalar_from_text(output, key)
|
|
||||||
|
|
||||||
# Inline vault
|
|
||||||
inline_block = _extract_inline_vault_block(vault_path.read_text(), key)
|
|
||||||
if not inline_block:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
|
|
||||||
tmp.write(inline_block)
|
|
||||||
tmp_name = tmp.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = _vault_view(tmp_name)
|
|
||||||
return output.strip()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(tmp_name):
|
|
||||||
os.unlink(tmp_name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _update_knoe_cfg_value(
|
def _update_knoe_cfg_value(
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
# knoe.cfg Secrets
|
# knoe.cfg Secrets
|
||||||
|
|
||||||
This document describes how secrets are handled in `knoe.cfg` and where they are stored in OpenBao.
|
This document describes how secrets are handled in `knoe.cfg` and where they are stored.
|
||||||
|
|
||||||
|
## External secret store: 1Password (`knoey` vault)
|
||||||
|
|
||||||
|
The master database password (`administrator`) is stored in a dedicated 1Password vault called **`knoey`**, isolated from the user's personal vaults.
|
||||||
|
|
||||||
|
| 1Password item | Field | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `administrator` | `password` | Database master password (formerly in Ansible vault) |
|
||||||
|
| `k3s-token` | `credential` | k3s cluster join token |
|
||||||
|
| `samba-dns-admin` | `password` | Samba/AD DNS admin password |
|
||||||
|
|
||||||
|
`install.sh` creates the `knoey` vault and the `administrator` item on first run (via `etc/init_1password.sh`). The `op` CLI (`brew install 1password-cli`) is required.
|
||||||
|
|
||||||
## Temporary encrypted values
|
## Temporary encrypted values
|
||||||
|
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
### Installer ↔ Ansible boundary
|
|
||||||
|
|
||||||
`install.py` (and the `installer/` package) and Ansible are treated as **independent tools**.
|
|
||||||
|
|
||||||
- The installer may *invoke* `ansible` / `ansible-playbook` as an external subprocess.
|
|
||||||
- The installer must not assume:
|
|
||||||
- Ansible vault access
|
|
||||||
- inventory parsing as a required part of the workflow
|
|
||||||
- any specific private repo checkout layout (e.g. a sibling `knoe/infrastructure`)
|
|
||||||
- Ansible playbooks/roles must not assume `install.py` is discovering secrets or passing hidden, repo-local state.
|
|
||||||
|
|
||||||
This is designed so private inventory/secret material can live entirely in `knoe/infrastructure` (or elsewhere) without this repo requiring it.
|
|
||||||
|
|
||||||
#### Explicit inputs (recommended)
|
|
||||||
|
|
||||||
When the installer needs to call `ansible-playbook` (for example to fetch a kubeconfig), provide paths explicitly via environment variables:
|
|
||||||
|
|
||||||
- `PROLE_ANSIBLE_K3S_FETCH_KUBECONFIG_PLAYBOOK`
|
|
||||||
- Full path to `k3s_fetch_kubeconfig.yml`.
|
|
||||||
- `PROLE_ANSIBLE_PLAYBOOKS_DIR` (or `PROLE_ANSIBLE_PLAYBOOK_DIR`)
|
|
||||||
- Directory that contains `k3s_fetch_kubeconfig.yml`.
|
|
||||||
- `PROLE_ANSIBLE_INVENTORY` (or `ANSIBLE_INVENTORY`)
|
|
||||||
- Inventory path passed as `ansible-playbook -i ...`.
|
|
||||||
- `PROLE_ANSIBLE_VAULT_PASSWORD_FILE` (or `ANSIBLE_VAULT_PASSWORD_FILE`)
|
|
||||||
- Vault password file path passed as `ansible-playbook --vault-password-file ...`.
|
|
||||||
- `PROLE_ANSIBLE_CONFIG` (or `ANSIBLE_CONFIG`)
|
|
||||||
- Path to an Ansible config file to use for the subprocess.
|
|
||||||
|
|
||||||
Separately, when the deployment workflow needs k3s connection info, provide it explicitly:
|
|
||||||
|
|
||||||
- `PROLE_K3S_SERVER`
|
|
||||||
- `PROLE_K3S_TOKEN` (or `K3S_TOKEN`)
|
|
||||||
|
|
||||||
#### Optional legacy auto-detection (opt-in)
|
|
||||||
|
|
||||||
For local developer convenience only, repo-relative discovery can be enabled explicitly:
|
|
||||||
|
|
||||||
- `PROLE_ANSIBLE_AUTO_DETECT=true`
|
|
||||||
|
|
||||||
When enabled, some flows may look for legacy repo-relative playbooks (under `infrastructure/playbooks/`). This is **not** required for normal operation and should not be relied on for private infrastructure.
|
|
||||||
|
|
||||||
### K3s slow-storage tolerance knobs (Ansible)
|
|
||||||
|
|
||||||
The `infrastructure/roles/k3s` role exposes configurable defaults intended for slow storage (Pi / USB) rollouts:
|
|
||||||
|
|
||||||
- `k3s_kubelet_args` (default includes `runtime-request-timeout=15m`, `image-pull-progress-deadline=15m`, `node-status-update-frequency=20s`)
|
|
||||||
- `k3s_systemd_override_enabled` (default `true`)
|
|
||||||
- `k3s_systemd_timeout_start_sec` (default `15min`)
|
|
||||||
- `k3s_systemd_timeout_stop_sec` (default `10min`)
|
|
||||||
- `k3s_systemd_restart` (default `always`)
|
|
||||||
- `k3s_systemd_restart_sec` (default `15s`)
|
|
||||||
74
etc/init_1password.sh
Executable file
74
etc/init_1password.sh
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# init_1password.sh
|
||||||
|
# Preflight: sign in to 1Password, create the 'knoey' vault if absent,
|
||||||
|
# and ensure the 'administrator' item (DB master password) exists.
|
||||||
|
#
|
||||||
|
# Called by install.sh before the Python installer. Skipped in --min mode.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_info() { echo "==> [1Password] $*"; }
|
||||||
|
_warn() { echo " [WARN] $*" >&2; }
|
||||||
|
_fatal() { echo " [ERROR] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
VAULT="knoey"
|
||||||
|
ADMIN_ITEM="administrator"
|
||||||
|
|
||||||
|
# ── skip in --min mode ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--min" ]]; then
|
||||||
|
_warn "Skipping 1Password preflight in --min mode."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── require op CLI ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! command -v op >/dev/null 2>&1; then
|
||||||
|
_fatal "1Password CLI (op) not found.
|
||||||
|
Install: brew install 1password-cli
|
||||||
|
Docs: https://developer.1password.com/docs/cli"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "op CLI found: $(op --version)"
|
||||||
|
|
||||||
|
# ── sign in ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! op whoami >/dev/null 2>&1; then
|
||||||
|
_info "No active 1Password session. Signing in..."
|
||||||
|
op signin
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "Signed in as: $(op whoami --format json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('email','unknown'))" 2>/dev/null || op whoami)"
|
||||||
|
|
||||||
|
# ── create knoey vault if absent ───────────────────────────────────────────
|
||||||
|
|
||||||
|
if op vault get "$VAULT" >/dev/null 2>&1; then
|
||||||
|
_info "Vault '$VAULT' already exists."
|
||||||
|
else
|
||||||
|
_info "Creating vault '$VAULT'..."
|
||||||
|
op vault create "$VAULT"
|
||||||
|
_info "Vault '$VAULT' created."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── ensure administrator item exists ───────────────────────────────────────
|
||||||
|
|
||||||
|
if op item get "$ADMIN_ITEM" --vault "$VAULT" >/dev/null 2>&1; then
|
||||||
|
_info "Item '$ADMIN_ITEM' already exists in vault '$VAULT'."
|
||||||
|
else
|
||||||
|
_info "Creating item '$ADMIN_ITEM' in vault '$VAULT' with a generated password..."
|
||||||
|
op item create \
|
||||||
|
--category login \
|
||||||
|
--title "$ADMIN_ITEM" \
|
||||||
|
--vault "$VAULT" \
|
||||||
|
--generate-password="32,letters,digits"
|
||||||
|
_info "Item '$ADMIN_ITEM' created."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── export for child processes ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export OP_VAULT="$VAULT"
|
||||||
|
_info "OP_VAULT=$OP_VAULT"
|
||||||
@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# init_ansible.sh
|
|
||||||
# Purpose:
|
|
||||||
# - Initialize or update the Ansible vault password file (.vault_pass)
|
|
||||||
# - Should be run after init_openbao.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
|
||||||
knoe_set_mode "${2:-}"
|
|
||||||
shift 2
|
|
||||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
|
||||||
knoe_set_mode "${1#*=}"
|
|
||||||
shift
|
|
||||||
fi
|
|
||||||
|
|
||||||
PROLE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
||||||
VAULT_PASS_FILE="${PROLE_VAULT_PASS_FILE:-${ANSIBLE_VAULT_PASSWORD_FILE:-$PROLE_ROOT/.vault_pass}}"
|
|
||||||
|
|
||||||
ACTION=${1:-initialize}
|
|
||||||
|
|
||||||
case "$ACTION" in
|
|
||||||
initialize|update)
|
|
||||||
# Use PROLE_PASSWD if set, otherwise prompt
|
|
||||||
if [[ -n "${PROLE_PASSWD:-}" ]]; then
|
|
||||||
echo "Using PROLE_PASSWD for Ansible Vault password."
|
|
||||||
echo "$PROLE_PASSWD" > "$VAULT_PASS_FILE"
|
|
||||||
else
|
|
||||||
echo -n "Enter Ansible Vault password: "
|
|
||||||
read -rs vault_pass
|
|
||||||
echo
|
|
||||||
echo "$vault_pass" > "$VAULT_PASS_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 600 "$VAULT_PASS_FILE"
|
|
||||||
echo "Ansible Vault password file $ACTION""d at $VAULT_PASS_FILE"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {initialize|update}" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
56
etc/set-k3s-token-1password.sh
Normal file
56
etc/set-k3s-token-1password.sh
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# set-k3s-token-1password.sh
|
||||||
|
# Reads the k3s server join token and stores it in the 'knoey' 1Password vault.
|
||||||
|
# Replaces etc/set-k3s-token-vault.sh (formerly used ansible-vault).
|
||||||
|
#
|
||||||
|
# Run on the k3s control-plane node (requires sudo to read the token file).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TOKEN_FILE="${TOKEN_FILE:-/var/lib/rancher/k3s/server/node-token}"
|
||||||
|
VAULT="knoey"
|
||||||
|
ITEM="k3s-token"
|
||||||
|
FIELD="credential"
|
||||||
|
|
||||||
|
if [[ ! -r "$TOKEN_FILE" ]]; then
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
TOKEN="$(sudo cat "$TOKEN_FILE" | tr -d '\r\n')"
|
||||||
|
else
|
||||||
|
echo "ERROR: Cannot read token file: $TOKEN_FILE" >&2
|
||||||
|
echo "Are you running this on a k3s server?" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TOKEN="$(cat "$TOKEN_FILE" | tr -d '\r\n')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "ERROR: Token read from $TOKEN_FILE is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v op >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: 1Password CLI (op) not found. Install: brew install 1password-cli" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! op whoami >/dev/null 2>&1; then
|
||||||
|
op signin
|
||||||
|
fi
|
||||||
|
|
||||||
|
if op item get "$ITEM" --vault "$VAULT" >/dev/null 2>&1; then
|
||||||
|
echo "Updating existing item '$ITEM' in vault '$VAULT'..."
|
||||||
|
op item edit "$ITEM" --vault "$VAULT" "${FIELD}=${TOKEN}"
|
||||||
|
else
|
||||||
|
echo "Creating item '$ITEM' in vault '$VAULT'..."
|
||||||
|
op item create \
|
||||||
|
--category login \
|
||||||
|
--title "$ITEM" \
|
||||||
|
--vault "$VAULT" \
|
||||||
|
"${FIELD}=${TOKEN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done. k3s token stored in 1Password vault '$VAULT' as item '$ITEM'."
|
||||||
|
echo
|
||||||
|
echo "Verify with:"
|
||||||
|
echo " op item get $ITEM --vault $VAULT --fields $FIELD --reveal"
|
||||||
@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
||||||
|
|
||||||
# Where k3s keeps the server join token on the first/control-plane server
|
|
||||||
TOKEN_FILE="${TOKEN_FILE:-/var/lib/rancher/k3s/server/node-token}"
|
|
||||||
|
|
||||||
# Pick a dedicated vault file so we don't stomp your existing vault.yml.
|
|
||||||
# Change this if you want it somewhere else.
|
|
||||||
VAULT_FILE="${VAULT_FILE:-infrastructure/inventory/group_vars/all/vault_k3s.yml}"
|
|
||||||
|
|
||||||
# Variable name to store in the vault
|
|
||||||
VAR_NAME="${VAR_NAME:-vault_k3s_token}"
|
|
||||||
|
|
||||||
if [[ ! -r "$TOKEN_FILE" ]]; then
|
|
||||||
echo "ERROR: Cannot read token file: $TOKEN_FILE"
|
|
||||||
echo "Are you running this on a k3s server (pi.knoe.org)?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v ansible-vault >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: ansible-vault not found in PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TOKEN="$(sudo cat "$TOKEN_FILE" | tr -d '\r\n')"
|
|
||||||
|
|
||||||
if [[ -z "$TOKEN" ]]; then
|
|
||||||
echo "ERROR: Token read from $TOKEN_FILE is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$VAULT_FILE")"
|
|
||||||
|
|
||||||
TMP="$(mktemp)"
|
|
||||||
trap 'rm -f "$TMP"' EXIT
|
|
||||||
|
|
||||||
# Build a vault yaml with exactly one variable
|
|
||||||
# (ansible-vault encrypt_string outputs a full YAML block)
|
|
||||||
ansible-vault encrypt_string \
|
|
||||||
--name "$VAR_NAME" \
|
|
||||||
"$TOKEN" > "$TMP"
|
|
||||||
|
|
||||||
# If the vault file already exists, avoid duplicate var definitions:
|
|
||||||
# - if VAR_NAME already present, we replace the whole file (simple + safe)
|
|
||||||
# - otherwise, append
|
|
||||||
if [[ -f "$VAULT_FILE" ]]; then
|
|
||||||
if grep -qE "^\s*${VAR_NAME}:" "$VAULT_FILE"; then
|
|
||||||
echo "Updating existing $VAR_NAME in $VAULT_FILE (replacing file contents)."
|
|
||||||
mv "$TMP" "$VAULT_FILE"
|
|
||||||
else
|
|
||||||
echo "Appending $VAR_NAME to $VAULT_FILE"
|
|
||||||
printf "\n" >> "$VAULT_FILE"
|
|
||||||
cat "$TMP" >> "$VAULT_FILE"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Creating vault file: $VAULT_FILE"
|
|
||||||
mv "$TMP" "$VAULT_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 0600 "$VAULT_FILE"
|
|
||||||
|
|
||||||
echo "Done."
|
|
||||||
echo "Wrote: $VAULT_FILE"
|
|
||||||
echo "Var: $VAR_NAME"
|
|
||||||
echo
|
|
||||||
echo "Verify with:"
|
|
||||||
echo " ansible -i infrastructure/inventory/hosts.ini pi.knoe.org -m assert -a 'that=${VAR_NAME} is defined' --ask-vault-pass"
|
|
||||||
@ -2,23 +2,17 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add project root to path
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||||
sys.path.append(str(ROOT_DIR))
|
sys.path.append(str(ROOT_DIR))
|
||||||
|
|
||||||
from knoe import config as inst_config
|
from knoe import config as inst_config
|
||||||
|
from knoe.core.onepassword import get_secret, op_available
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
vault_path = (
|
token = ""
|
||||||
ROOT_DIR
|
if op_available():
|
||||||
/ "infrastructure"
|
token = get_secret("k3s-token", "credential").strip()
|
||||||
/ "inventory"
|
|
||||||
/ "group_vars"
|
|
||||||
/ "all"
|
|
||||||
/ "vault_k3s.yml"
|
|
||||||
)
|
|
||||||
token = inst_config._try_read_ansible_vault_value(vault_path, "vault_k3s_token")
|
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
print("Syncing K3S_TOKEN to active config...")
|
print("Syncing K3S_TOKEN to active config...")
|
||||||
@ -26,7 +20,7 @@ def main():
|
|||||||
inst_config._update_knoe_cfg_value("Inputs", "init_cluster.k3s_token", token)
|
inst_config._update_knoe_cfg_value("Inputs", "init_cluster.k3s_token", token)
|
||||||
inst_config._update_knoe_cfg_value("Service Cluster (k3s)", "K3S_TOKEN", token)
|
inst_config._update_knoe_cfg_value("Service Cluster (k3s)", "K3S_TOKEN", token)
|
||||||
else:
|
else:
|
||||||
print("Warning: Could not extract vault_k3s_token from Ansible vault.")
|
print("Warning: Could not retrieve k3s-token from 1Password knoey vault.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
$ANSIBLE_VAULT;1.1;AES256
|
|
||||||
33623434353765353030333339626631656163343239353230643430356466306461663835383566
|
|
||||||
3564333737623034343837616632386462306462366538390a623538373034383839376261653734
|
|
||||||
36653238393437656332373965663866653730343864333063366462303661356366323262363839
|
|
||||||
3961633266353131310a313930346164393031623037393339356539616639343536353163343036
|
|
||||||
61353535633234363535633437356162626234356139323531643534613961633166393135356562
|
|
||||||
30306634646130353665656262393132656632373634353765316630643665356331363435366165
|
|
||||||
356261383336383736336336356564386337
|
|
||||||
@ -81,6 +81,12 @@ if [[ -n "${_script_dir}" && -d "${_script_dir}/knoe" ]]; then
|
|||||||
# Local checkout: delegate to the Python installer module.
|
# Local checkout: delegate to the Python installer module.
|
||||||
cd "${_script_dir}"
|
cd "${_script_dir}"
|
||||||
|
|
||||||
|
# 1Password preflight — sign in, create 'knoey' vault, ensure 'administrator' item.
|
||||||
|
# Skipped in --min mode (init_1password.sh handles the flag check itself).
|
||||||
|
if [[ -f "${_script_dir}/etc/init_1password.sh" ]]; then
|
||||||
|
bash "${_script_dir}/etc/init_1password.sh" "$@" || true
|
||||||
|
fi
|
||||||
|
|
||||||
# If a built binary exists, prefer it.
|
# If a built binary exists, prefer it.
|
||||||
if [[ -x "dist/knoe" ]]; then
|
if [[ -x "dist/knoe" ]]; then
|
||||||
exec "./dist/knoe" "$@"
|
exec "./dist/knoe" "$@"
|
||||||
|
|||||||
122
knoe/config.py
122
knoe/config.py
@ -759,96 +759,6 @@ def _extract_yaml_scalar_from_text(text: str, key: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _extract_inline_vault_block(text: str, key: str) -> str:
|
|
||||||
lines = text.splitlines()
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith(f"{key}:"):
|
|
||||||
base_indent = len(line) - len(line.lstrip())
|
|
||||||
block = []
|
|
||||||
i += 1
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i]
|
|
||||||
if not line.strip():
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
indent = len(line) - len(line.lstrip())
|
|
||||||
if indent <= base_indent:
|
|
||||||
break
|
|
||||||
block.append(line.strip())
|
|
||||||
i += 1
|
|
||||||
if block and block[0].startswith("$ANSIBLE_VAULT"):
|
|
||||||
return "\n".join(block) + "\n"
|
|
||||||
return ""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
|
||||||
if not vault_path.exists():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Simple search in plain text first
|
|
||||||
try:
|
|
||||||
raw_text = vault_path.read_text()
|
|
||||||
plain = _extract_yaml_scalar_from_text(raw_text, key)
|
|
||||||
if (
|
|
||||||
plain
|
|
||||||
and not plain.startswith("$ANSIBLE_VAULT")
|
|
||||||
and not plain.startswith("!vault")
|
|
||||||
):
|
|
||||||
return plain
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
password_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
if not password_file:
|
|
||||||
candidate = PROJECT_ROOT / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
password_file = str(candidate)
|
|
||||||
|
|
||||||
if not password_file or shutil.which("ansible-vault") is None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _vault_view(path: str) -> str:
|
|
||||||
try:
|
|
||||||
res = subprocess.run(
|
|
||||||
["ansible-vault", "view", path, "--vault-password-file", password_file],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
env=os.environ.copy(),
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
if res.returncode == 0:
|
|
||||||
return res.stdout or ""
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = _vault_view(str(vault_path))
|
|
||||||
if output:
|
|
||||||
return _extract_yaml_scalar_from_text(output, key)
|
|
||||||
|
|
||||||
# Inline vault
|
|
||||||
inline_block = _extract_inline_vault_block(vault_path.read_text(), key)
|
|
||||||
if not inline_block:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
|
|
||||||
tmp.write(inline_block)
|
|
||||||
tmp_name = tmp.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = _vault_view(tmp_name)
|
|
||||||
return output.strip()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(tmp_name):
|
|
||||||
os.unlink(tmp_name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _update_knoe_cfg_value(
|
def _update_knoe_cfg_value(
|
||||||
@ -1154,14 +1064,14 @@ _MACOS_DEPENDENCIES = [
|
|||||||
"bin": "python3",
|
"bin": "python3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ansible",
|
"id": "op",
|
||||||
"name": "ansible",
|
"name": "1password-cli",
|
||||||
"parent": "brew",
|
"parent": "brew",
|
||||||
"description": "Infrastructure automation tool",
|
"description": "1Password CLI for secret management",
|
||||||
"url": "https://www.ansible.com",
|
"url": "https://developer.1password.com/docs/cli",
|
||||||
"install_cmd": "brew install ansible",
|
"install_cmd": "brew install 1password-cli",
|
||||||
"check_cmd": "ansible --version",
|
"check_cmd": "op --version",
|
||||||
"bin": "ansible",
|
"bin": "op",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kubectl",
|
"id": "kubectl",
|
||||||
@ -1265,13 +1175,13 @@ def _linux_dependencies(manager: str) -> list[dict]:
|
|||||||
"bin": "python3",
|
"bin": "python3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ansible",
|
"id": "op",
|
||||||
"name": "ansible",
|
"name": "1password-cli",
|
||||||
"description": "Infrastructure automation tool",
|
"description": "1Password CLI for secret management",
|
||||||
"url": "https://www.ansible.com",
|
"url": "https://developer.1password.com/docs/cli",
|
||||||
"install_cmd": install("ansible"),
|
"install_cmd": install("1password-cli"),
|
||||||
"check_cmd": "ansible --version",
|
"check_cmd": "op --version",
|
||||||
"bin": "ansible",
|
"bin": "op",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kubectl",
|
"id": "kubectl",
|
||||||
@ -1485,9 +1395,9 @@ def get_required_dependency_ids(inputs: dict | None = None) -> set[str]:
|
|||||||
mode = _deployment_mode_hint(inputs)
|
mode = _deployment_mode_hint(inputs)
|
||||||
|
|
||||||
if mode == "min":
|
if mode == "min":
|
||||||
return {"python", "ansible", "containerd", "docker-buildx"}
|
return {"python", "containerd", "docker-buildx"}
|
||||||
|
|
||||||
base_required = {"python", "ansible", "kubectl", "kubectx", "docker"}
|
base_required = {"python", "op", "kubectl", "kubectx", "docker"}
|
||||||
if mode == "gke":
|
if mode == "gke":
|
||||||
required = base_required | {"gcloud"}
|
required = base_required | {"gcloud"}
|
||||||
platform_info = detect_dependency_platform()
|
platform_info = detect_dependency_platform()
|
||||||
|
|||||||
@ -26,7 +26,6 @@ from knoe.config import (
|
|||||||
_write_k3s_kubeconfig,
|
_write_k3s_kubeconfig,
|
||||||
_encrypt_cfg_secret,
|
_encrypt_cfg_secret,
|
||||||
_merge_kubeconfig,
|
_merge_kubeconfig,
|
||||||
_try_read_ansible_vault_value,
|
|
||||||
)
|
)
|
||||||
from knoe.core.build_context import copy_build_context_dir
|
from knoe.core.build_context import copy_build_context_dir
|
||||||
from knoe.core.cnpg_placement import (
|
from knoe.core.cnpg_placement import (
|
||||||
@ -4472,14 +4471,7 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
|||||||
self.err(f"[WARN] Ansible playbook not found: {playbook}")
|
self.err(f"[WARN] Ansible playbook not found: {playbook}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
vault_pass = self.project_root / ".vault_pass"
|
cmd = ["ansible-playbook", str(playbook)]
|
||||||
cmd = ["ansible-playbook"]
|
|
||||||
if vault_pass.exists():
|
|
||||||
cmd += ["--vault-password-file", str(vault_pass)]
|
|
||||||
vault_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
if not vault_pass.exists() and vault_file:
|
|
||||||
cmd += ["--vault-password-file", vault_file]
|
|
||||||
cmd.append(str(playbook))
|
|
||||||
|
|
||||||
self.log("==> Fetching k3s kubeconfig via Ansible")
|
self.log("==> Fetching k3s kubeconfig via Ansible")
|
||||||
try:
|
try:
|
||||||
@ -5829,16 +5821,11 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
|||||||
self.log("[RESET] Resetting k3s cluster (mode k3s)...")
|
self.log("[RESET] Resetting k3s cluster (mode k3s)...")
|
||||||
self._cleanup_local_k3s_artifacts()
|
self._cleanup_local_k3s_artifacts()
|
||||||
|
|
||||||
vault_args = []
|
|
||||||
v_file = os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE")
|
|
||||||
if v_file:
|
|
||||||
vault_args = ["-v", v_file]
|
|
||||||
|
|
||||||
delete_cmd = [
|
delete_cmd = [
|
||||||
"./ansible.sh",
|
"./ansible.sh",
|
||||||
"-p",
|
"-p",
|
||||||
"infrastructure/playbooks/k3s_delete.yml",
|
"infrastructure/playbooks/k3s_delete.yml",
|
||||||
] + vault_args
|
]
|
||||||
self.log(f"Running: {' '.join(delete_cmd)}")
|
self.log(f"Running: {' '.join(delete_cmd)}")
|
||||||
rc = self._run_cmd(delete_cmd)
|
rc = self._run_cmd(delete_cmd)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
@ -6989,132 +6976,30 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
|||||||
)
|
)
|
||||||
out.flush()
|
out.flush()
|
||||||
|
|
||||||
def _db_master_vault_file(self) -> Path:
|
def _load_db_password_from_1password(self, *, log_found: bool = True) -> str:
|
||||||
raw = (os.environ.get("PROLE_DB_MASTER_VAULT_FILE") or "").strip()
|
try:
|
||||||
if raw:
|
from knoe.core.onepassword import get_secret, op_available
|
||||||
expanded = _expand_cfg_value(raw, _collect_cfg_vars())
|
if not op_available():
|
||||||
return Path(expanded).expanduser()
|
return ""
|
||||||
return (
|
password = get_secret("administrator", "password").strip()
|
||||||
self.project_root
|
if password and log_found:
|
||||||
/ "infrastructure"
|
self.log("[CONFIG] Loaded DB master password from 1Password knoey vault.")
|
||||||
/ "inventory"
|
return password
|
||||||
/ "group_vars"
|
except Exception:
|
||||||
/ "all"
|
return ""
|
||||||
/ "vault_db_master.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _db_master_vault_key(self) -> str:
|
def _persist_db_password_to_1password(self, password: str) -> None:
|
||||||
key = (os.environ.get("PROLE_DB_MASTER_VAULT_KEY") or "").strip()
|
|
||||||
return key or "vault_knoe_db_master_password"
|
|
||||||
|
|
||||||
def _resolve_ansible_vault_password_file(self) -> str:
|
|
||||||
vault_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
if vault_file and Path(vault_file).expanduser().is_file():
|
|
||||||
return str(Path(vault_file).expanduser())
|
|
||||||
for base in (self.project_root, Path.cwd()):
|
|
||||||
candidate = base / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
os.environ["ANSIBLE_VAULT_PASSWORD_FILE"] = str(candidate)
|
|
||||||
return str(candidate)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _load_db_password_from_ansible_vault(self, *, log_found: bool = True) -> str:
|
|
||||||
vault_file = self._db_master_vault_file()
|
|
||||||
vault_key = self._db_master_vault_key()
|
|
||||||
password = _try_read_ansible_vault_value(vault_file, vault_key).strip()
|
|
||||||
if password and log_found:
|
|
||||||
self.log(
|
|
||||||
f"[CONFIG] Loaded DB master password from Ansible Vault ({vault_file})."
|
|
||||||
)
|
|
||||||
return password
|
|
||||||
|
|
||||||
def _persist_db_password_to_ansible_vault(self, password: str) -> None:
|
|
||||||
if not password:
|
if not password:
|
||||||
raise RuntimeError("Cannot persist an empty database master password.")
|
raise RuntimeError("Cannot persist an empty database master password.")
|
||||||
if shutil.which("ansible-vault") is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"ansible-vault command is required to store the database master password securely."
|
|
||||||
)
|
|
||||||
|
|
||||||
vault_file = self._db_master_vault_file()
|
|
||||||
vault_key = self._db_master_vault_key()
|
|
||||||
vault_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
vault_password_file = self._resolve_ansible_vault_password_file()
|
|
||||||
vault_password = (os.environ.get("ANSIBLE_VAULT_PASSWORD") or "").strip()
|
|
||||||
if not vault_password_file and not vault_password:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Ansible Vault password is not configured. Set ANSIBLE_VAULT_PASSWORD_FILE (or create .vault_pass) "
|
|
||||||
"or set ANSIBLE_VAULT_PASSWORD before bootstrap."
|
|
||||||
)
|
|
||||||
|
|
||||||
tmp_password_file = None
|
|
||||||
if not vault_password_file and vault_password:
|
|
||||||
tmp_pw = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
|
||||||
tmp_pw.write(vault_password)
|
|
||||||
tmp_pw.flush()
|
|
||||||
tmp_pw.close()
|
|
||||||
tmp_password_file = tmp_pw.name
|
|
||||||
vault_password_file = tmp_password_file
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
"ansible-vault",
|
|
||||||
"encrypt_string",
|
|
||||||
"--name",
|
|
||||||
vault_key,
|
|
||||||
password,
|
|
||||||
]
|
|
||||||
if vault_password_file:
|
|
||||||
cmd += ["--vault-password-file", vault_password_file]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = subprocess.run(
|
from knoe.core.onepassword import set_secret, op_available
|
||||||
cmd,
|
if not op_available():
|
||||||
capture_output=True,
|
self.log("[WARN] 1Password CLI not available; skipping password persistence.")
|
||||||
text=True,
|
return
|
||||||
timeout=20,
|
set_secret("administrator", "password", password)
|
||||||
env=os.environ.copy(),
|
self.log("[CONFIG] Persisted DB master password to 1Password knoey vault.")
|
||||||
stdin=subprocess.DEVNULL,
|
except Exception as e:
|
||||||
)
|
raise RuntimeError(f"Failed to persist database master password to 1Password: {e}") from e
|
||||||
if res.returncode != 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Failed to write database master password to Ansible Vault: "
|
|
||||||
f"{(res.stderr or res.stdout or '').strip()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
rendered_block = (res.stdout or "").rstrip() + "\n"
|
|
||||||
if not rendered_block.strip():
|
|
||||||
raise RuntimeError(
|
|
||||||
"Failed to write database master password to Ansible Vault: empty encrypted output."
|
|
||||||
)
|
|
||||||
|
|
||||||
existing = ""
|
|
||||||
if vault_file.exists():
|
|
||||||
try:
|
|
||||||
existing = vault_file.read_text()
|
|
||||||
except Exception:
|
|
||||||
existing = ""
|
|
||||||
|
|
||||||
key_pattern = rf"(?ms)^\s*{re.escape(vault_key)}\s*:.*?(?=^\S|\Z)"
|
|
||||||
if existing and re.search(key_pattern, existing):
|
|
||||||
updated = re.sub(key_pattern, rendered_block.rstrip(), existing, count=1)
|
|
||||||
output_text = updated.rstrip() + "\n"
|
|
||||||
elif existing.strip():
|
|
||||||
output_text = existing.rstrip() + "\n\n" + rendered_block
|
|
||||||
else:
|
|
||||||
output_text = rendered_block
|
|
||||||
|
|
||||||
vault_file.write_text(output_text)
|
|
||||||
os.chmod(vault_file, 0o600)
|
|
||||||
self.log(
|
|
||||||
f"[CONFIG] Persisted DB master password into Ansible Vault ({vault_file}:{vault_key})."
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
if tmp_password_file:
|
|
||||||
try:
|
|
||||||
os.unlink(tmp_password_file)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self) -> int:
|
def run(self) -> int:
|
||||||
_configure_unbuffered_io()
|
_configure_unbuffered_io()
|
||||||
@ -7141,26 +7026,26 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
|||||||
or ""
|
or ""
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
# If runtime env provides a password, reconcile with vault for
|
# If runtime env provides a password, reconcile with 1Password for
|
||||||
# consistency and bootstrap vault when missing.
|
# consistency and bootstrap when missing.
|
||||||
if db_pw and env_db_pw and db_pw == env_db_pw:
|
if db_pw and env_db_pw and db_pw == env_db_pw:
|
||||||
vault_pw = self._load_db_password_from_ansible_vault(log_found=False)
|
vault_pw = self._load_db_password_from_1password(log_found=False)
|
||||||
if vault_pw and vault_pw != db_pw:
|
if vault_pw and vault_pw != db_pw:
|
||||||
self.log(
|
self.log(
|
||||||
"[WARN] Runtime DB password differs from Ansible Vault value; "
|
"[WARN] Runtime DB password differs from 1Password value; "
|
||||||
"using vault password for consistency."
|
"using 1Password value for consistency."
|
||||||
)
|
)
|
||||||
self.inputs["init_password.db_password"] = vault_pw
|
self.inputs["init_password.db_password"] = vault_pw
|
||||||
self.inputs["init_password.db_password_confirm"] = vault_pw
|
self.inputs["init_password.db_password_confirm"] = vault_pw
|
||||||
db_pw = vault_pw
|
db_pw = vault_pw
|
||||||
elif not vault_pw:
|
elif not vault_pw:
|
||||||
self._persist_db_password_to_ansible_vault(db_pw)
|
self._persist_db_password_to_1password(db_pw)
|
||||||
self.log(
|
self.log(
|
||||||
"[CONFIG] Bootstrapped Ansible Vault from KNOE_DB_PASSWORD/DB_PASSWORD."
|
"[CONFIG] Bootstrapped 1Password administrator from KNOE_DB_PASSWORD/DB_PASSWORD."
|
||||||
)
|
)
|
||||||
|
|
||||||
if not db_pw:
|
if not db_pw:
|
||||||
vault_pw = self._load_db_password_from_ansible_vault()
|
vault_pw = self._load_db_password_from_1password()
|
||||||
if vault_pw:
|
if vault_pw:
|
||||||
self.inputs["init_password.db_password"] = vault_pw
|
self.inputs["init_password.db_password"] = vault_pw
|
||||||
self.inputs["init_password.db_password_confirm"] = vault_pw
|
self.inputs["init_password.db_password_confirm"] = vault_pw
|
||||||
@ -7182,14 +7067,14 @@ class KnoeConsoleInstaller(KnoeInstaller):
|
|||||||
)
|
)
|
||||||
if allow_prompt:
|
if allow_prompt:
|
||||||
new_pw = self._prompt_for_master_password()
|
new_pw = self._prompt_for_master_password()
|
||||||
self._persist_db_password_to_ansible_vault(new_pw)
|
self._persist_db_password_to_1password(new_pw)
|
||||||
self.log(
|
self.log(
|
||||||
"[CONFIG] Master password captured interactively and saved for subsequent runs."
|
"[CONFIG] Master password captured interactively and saved to 1Password."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Database master password is missing and prompting is unavailable. "
|
"Database master password is missing and prompting is unavailable. "
|
||||||
"Run once in an interactive terminal to bootstrap Ansible Vault, "
|
"Run once in an interactive terminal to bootstrap the 1Password knoey vault, "
|
||||||
"or provide KNOE_DB_PASSWORD/DB_PASSWORD."
|
"or provide KNOE_DB_PASSWORD/DB_PASSWORD."
|
||||||
)
|
)
|
||||||
self.inputs["init_password.db_password"] = new_pw
|
self.inputs["init_password.db_password"] = new_pw
|
||||||
|
|||||||
167
knoe/core/env.py
167
knoe/core/env.py
@ -345,7 +345,7 @@ def _resolve_k3s_connection(
|
|||||||
if not token and cfg_token:
|
if not token and cfg_token:
|
||||||
token = _normalize_k3s_token(cfg_token)
|
token = _normalize_k3s_token(cfg_token)
|
||||||
|
|
||||||
# Ansible topology fallback
|
# Ansible topology fallback (k3s node discovery only — no vault decryption)
|
||||||
if not ansible_topology:
|
if not ansible_topology:
|
||||||
try:
|
try:
|
||||||
root = project_root or PROJECT_ROOT
|
root = project_root or PROJECT_ROOT
|
||||||
@ -358,6 +358,15 @@ def _resolve_k3s_connection(
|
|||||||
if not token and ansible_topology.get("k3s_token"):
|
if not token and ansible_topology.get("k3s_token"):
|
||||||
token = _normalize_k3s_token(ansible_topology["k3s_token"])
|
token = _normalize_k3s_token(ansible_topology["k3s_token"])
|
||||||
|
|
||||||
|
# 1Password fallback for k3s token
|
||||||
|
if not token:
|
||||||
|
try:
|
||||||
|
from knoe.core.onepassword import get_secret, op_available
|
||||||
|
if op_available():
|
||||||
|
token = _normalize_k3s_token(get_secret("k3s-token", "credential"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if server and not server.startswith("http"):
|
if server and not server.startswith("http"):
|
||||||
server = f"https://{server}"
|
server = f"https://{server}"
|
||||||
return server, token
|
return server, token
|
||||||
@ -525,58 +534,6 @@ def _k3d_knoe_data_volume_args(knoe_data: str | None) -> list[str]:
|
|||||||
return ["--volume", f"{p}:/var/lib/rancher/k3s/storage@all"]
|
return ["--volume", f"{p}:/var/lib/rancher/k3s/storage@all"]
|
||||||
|
|
||||||
|
|
||||||
def _ensure_ansible_vault_credentials(prompt_ui: bool = False, root=None) -> None:
|
|
||||||
password_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
password = (os.environ.get("ANSIBLE_VAULT_PASSWORD") or "").strip()
|
|
||||||
|
|
||||||
if password_file:
|
|
||||||
try:
|
|
||||||
if not Path(password_file).expanduser().is_file():
|
|
||||||
password_file = ""
|
|
||||||
os.environ.pop("ANSIBLE_VAULT_PASSWORD_FILE", None)
|
|
||||||
except Exception:
|
|
||||||
password_file = ""
|
|
||||||
os.environ.pop("ANSIBLE_VAULT_PASSWORD_FILE", None)
|
|
||||||
|
|
||||||
if password or password_file:
|
|
||||||
return
|
|
||||||
|
|
||||||
for base in (Path.cwd(), PROJECT_ROOT):
|
|
||||||
try:
|
|
||||||
candidate = base / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
os.environ["ANSIBLE_VAULT_PASSWORD_FILE"] = str(candidate)
|
|
||||||
print(f"[INFO] Using Ansible Vault password file: {candidate}")
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
vault_password = ""
|
|
||||||
if prompt_ui and root is not None:
|
|
||||||
try:
|
|
||||||
from tkinter import simpledialog
|
|
||||||
|
|
||||||
vault_password = (
|
|
||||||
simpledialog.askstring(
|
|
||||||
"Ansible Vault",
|
|
||||||
"Enter Ansible Vault password:",
|
|
||||||
show="*",
|
|
||||||
parent=root,
|
|
||||||
)
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
vault_password = ""
|
|
||||||
if not vault_password:
|
|
||||||
try:
|
|
||||||
vault_password = getpass.getpass("Ansible Vault password: ")
|
|
||||||
except Exception:
|
|
||||||
vault_password = ""
|
|
||||||
|
|
||||||
if vault_password:
|
|
||||||
os.environ["ANSIBLE_VAULT_PASSWORD"] = vault_password
|
|
||||||
|
|
||||||
|
|
||||||
def _openbao_placeholder(namespace: str, leaf: str, key: str | None = None) -> str:
|
def _openbao_placeholder(namespace: str, leaf: str, key: str | None = None) -> str:
|
||||||
ns = (namespace or "").strip() or "default"
|
ns = (namespace or "").strip() or "default"
|
||||||
path = f"kv/knoe/{ns}/{leaf}"
|
path = f"kv/knoe/{ns}/{leaf}"
|
||||||
@ -1083,104 +1040,6 @@ def _extract_inline_vault_block(text: str, key: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
|
||||||
if not vault_path.exists():
|
|
||||||
return ""
|
|
||||||
# First, try plain YAML parsing (in case the file is not encrypted).
|
|
||||||
try:
|
|
||||||
plain = _parse_yaml_scalar_values(vault_path, {key}).get(key, "")
|
|
||||||
except Exception:
|
|
||||||
plain = ""
|
|
||||||
if (
|
|
||||||
plain
|
|
||||||
and not plain.lower().startswith("!vault")
|
|
||||||
and not plain.startswith("$ANSIBLE_VAULT")
|
|
||||||
):
|
|
||||||
return plain
|
|
||||||
|
|
||||||
password_file = (os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or "").strip()
|
|
||||||
password = (os.environ.get("ANSIBLE_VAULT_PASSWORD") or "").strip()
|
|
||||||
if password_file:
|
|
||||||
try:
|
|
||||||
if not Path(password_file).expanduser().exists():
|
|
||||||
password_file = ""
|
|
||||||
except Exception:
|
|
||||||
password_file = ""
|
|
||||||
if not password_file and not password:
|
|
||||||
for base in (Path.cwd(), PROJECT_ROOT):
|
|
||||||
try:
|
|
||||||
candidate = base / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
password_file = str(candidate)
|
|
||||||
os.environ["ANSIBLE_VAULT_PASSWORD_FILE"] = password_file
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if not password_file and not password:
|
|
||||||
return ""
|
|
||||||
if shutil.which("ansible-vault") is None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
tmp_path = None
|
|
||||||
if password_file:
|
|
||||||
password_file = password_file
|
|
||||||
elif password:
|
|
||||||
try:
|
|
||||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
|
||||||
tmp.write(password.encode("utf-8"))
|
|
||||||
tmp.flush()
|
|
||||||
tmp.close()
|
|
||||||
tmp_path = tmp.name
|
|
||||||
password_file = tmp_path
|
|
||||||
except Exception:
|
|
||||||
tmp_path = None
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _vault_view(path: str) -> str:
|
|
||||||
res = subprocess.run(
|
|
||||||
["ansible-vault", "view", path]
|
|
||||||
+ (["--vault-password-file", password_file] if password_file else []),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
env=os.environ.copy(),
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
if res.returncode != 0:
|
|
||||||
return ""
|
|
||||||
return res.stdout or ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = _vault_view(str(vault_path))
|
|
||||||
if output:
|
|
||||||
return _extract_yaml_scalar_from_text(output, key)
|
|
||||||
|
|
||||||
# Inline vault: extract the block and decrypt separately.
|
|
||||||
try:
|
|
||||||
raw_text = vault_path.read_text()
|
|
||||||
except Exception:
|
|
||||||
raw_text = ""
|
|
||||||
inline_block = _extract_inline_vault_block(raw_text, key)
|
|
||||||
if not inline_block:
|
|
||||||
return ""
|
|
||||||
tmp_inline = tempfile.NamedTemporaryFile(delete=False)
|
|
||||||
tmp_inline.write(inline_block.encode("utf-8"))
|
|
||||||
tmp_inline.flush()
|
|
||||||
tmp_inline.close()
|
|
||||||
output = _vault_view(tmp_inline.name)
|
|
||||||
try:
|
|
||||||
os.unlink(tmp_inline.name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return output.strip()
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
finally:
|
|
||||||
if tmp_path:
|
|
||||||
try:
|
|
||||||
os.unlink(tmp_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_ansible_k3s_settings(
|
def _detect_ansible_k3s_settings(
|
||||||
@ -1235,14 +1094,10 @@ def _detect_ansible_k3s_settings(
|
|||||||
host_for_url = f"{host_for_url}.{domain}"
|
host_for_url = f"{host_for_url}.{domain}"
|
||||||
server_url = f"https://{host_for_url}:6443"
|
server_url = f"https://{host_for_url}:6443"
|
||||||
|
|
||||||
vault_path = inventory_path / "group_vars" / "all" / "vault_k3s.yml"
|
|
||||||
token = _try_read_ansible_vault_value(vault_path, "vault_k3s_token")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"server_url": server_url,
|
"server_url": server_url,
|
||||||
"server_host": server_host,
|
"server_host": server_host,
|
||||||
"token": token,
|
"token": "",
|
||||||
"vault_path": str(vault_path) if vault_path.exists() else "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
124
knoe/core/onepassword.py
Normal file
124
knoe/core/onepassword.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""1Password CLI integration for knoe secret management.
|
||||||
|
|
||||||
|
All secrets are stored in the 'knoey' vault so they are isolated from the
|
||||||
|
user's personal 1Password vaults. The 'administrator' item holds the
|
||||||
|
database master password.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import string
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
_VAULT = "knoey"
|
||||||
|
_ADMIN_ITEM = "administrator"
|
||||||
|
|
||||||
|
|
||||||
|
def op_available() -> bool:
|
||||||
|
return shutil.which("op") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _op(*args: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
if not op_available():
|
||||||
|
raise RuntimeError(
|
||||||
|
"1Password CLI (op) not found. Install: brew install 1password-cli"
|
||||||
|
)
|
||||||
|
return subprocess.run(
|
||||||
|
["op", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_op_signed_in() -> None:
|
||||||
|
"""Ensure the op CLI has an active session; trigger sign-in if not."""
|
||||||
|
if not op_available():
|
||||||
|
raise RuntimeError(
|
||||||
|
"1Password CLI (op) not found. Install: brew install 1password-cli"
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
["op", "whoami"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
subprocess.run(["op", "signin"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_knoey_vault() -> None:
|
||||||
|
"""Create the 'knoey' vault if it does not already exist."""
|
||||||
|
result = _op("vault", "list", "--format", "json", check=True)
|
||||||
|
try:
|
||||||
|
vaults = json.loads(result.stdout or "[]")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
vaults = []
|
||||||
|
names = [v.get("name", "") for v in vaults]
|
||||||
|
if _VAULT not in names:
|
||||||
|
_op("vault", "create", _VAULT)
|
||||||
|
print(f"[INFO] Created 1Password vault '{_VAULT}'")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] 1Password vault '{_VAULT}' already exists")
|
||||||
|
|
||||||
|
|
||||||
|
def get_secret(item: str, field: str = "password") -> str:
|
||||||
|
"""Retrieve a field value from an item in the knoey vault."""
|
||||||
|
result = _op(
|
||||||
|
"item", "get", item,
|
||||||
|
"--vault", _VAULT,
|
||||||
|
"--fields", field,
|
||||||
|
"--reveal",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return ""
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def set_secret(item: str, field: str, value: str) -> None:
|
||||||
|
"""Set a field on an existing item, or create the item if absent."""
|
||||||
|
check_result = _op("item", "get", item, "--vault", _VAULT, check=False)
|
||||||
|
if check_result.returncode == 0:
|
||||||
|
_op(
|
||||||
|
"item", "edit", item,
|
||||||
|
"--vault", _VAULT,
|
||||||
|
f"{field}={value}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_op(
|
||||||
|
"item", "create",
|
||||||
|
"--category", "login",
|
||||||
|
"--title", item,
|
||||||
|
"--vault", _VAULT,
|
||||||
|
f"{field}={value}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_administrator_password() -> str:
|
||||||
|
"""Return the administrator password from the knoey vault."""
|
||||||
|
return get_secret(_ADMIN_ITEM, "password")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_administrator_secret() -> str:
|
||||||
|
"""Return the administrator password, creating the item if absent."""
|
||||||
|
pw = get_secret(_ADMIN_ITEM, "password")
|
||||||
|
if pw:
|
||||||
|
return pw
|
||||||
|
pw = _generate_password()
|
||||||
|
_op(
|
||||||
|
"item", "create",
|
||||||
|
"--category", "login",
|
||||||
|
"--title", _ADMIN_ITEM,
|
||||||
|
"--vault", _VAULT,
|
||||||
|
f"password={pw}",
|
||||||
|
)
|
||||||
|
print(f"[INFO] Created 1Password item '{_ADMIN_ITEM}' in vault '{_VAULT}'")
|
||||||
|
return pw
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_password(length: int = 32) -> str:
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||||
@ -30,10 +30,10 @@ from knoe.config import (
|
|||||||
from knoe.core.controller import KnoeController
|
from knoe.core.controller import KnoeController
|
||||||
from knoe.core.env import (
|
from knoe.core.env import (
|
||||||
_deployment_target_label,
|
_deployment_target_label,
|
||||||
_ensure_ansible_vault_credentials,
|
|
||||||
_safe_str,
|
_safe_str,
|
||||||
_normalize_cluster_env,
|
_normalize_cluster_env,
|
||||||
)
|
)
|
||||||
|
from knoe.core.onepassword import ensure_op_signed_in, ensure_knoey_vault
|
||||||
|
|
||||||
# Helper to set macOS application identity via Objective-C bridge.
|
# Helper to set macOS application identity via Objective-C bridge.
|
||||||
def setup_macos_app_identity():
|
def setup_macos_app_identity():
|
||||||
@ -1236,7 +1236,8 @@ def main():
|
|||||||
args.silent = True
|
args.silent = True
|
||||||
|
|
||||||
if args.silent:
|
if args.silent:
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
ensure_op_signed_in()
|
||||||
|
ensure_knoey_vault()
|
||||||
installer = KnoeConsoleInstaller(
|
installer = KnoeConsoleInstaller(
|
||||||
controller,
|
controller,
|
||||||
args.config,
|
args.config,
|
||||||
@ -1290,7 +1291,8 @@ def main():
|
|||||||
root.geometry(f"{window_width}x{window_height}+{cx}+{cy}")
|
root.geometry(f"{window_width}x{window_height}+{cx}+{cy}")
|
||||||
except Exception:
|
except Exception:
|
||||||
root.geometry(f"{window_width}x{window_height}")
|
root.geometry(f"{window_width}x{window_height}")
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=True, root=root)
|
ensure_op_signed_in()
|
||||||
|
ensure_knoey_vault()
|
||||||
KnoeInstaller(
|
KnoeInstaller(
|
||||||
root,
|
root,
|
||||||
config_path=args.config,
|
config_path=args.config,
|
||||||
@ -1306,7 +1308,8 @@ def main():
|
|||||||
|
|
||||||
run_ncurses_installer(controller)
|
run_ncurses_installer(controller)
|
||||||
else:
|
else:
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
ensure_op_signed_in()
|
||||||
|
ensure_knoey_vault()
|
||||||
from knoe.ncurses_installer import run_ncurses_installer
|
from knoe.ncurses_installer import run_ncurses_installer
|
||||||
|
|
||||||
run_ncurses_installer(controller)
|
run_ncurses_installer(controller)
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
"""Kerberos configuration, secret management and Ansible vault."""
|
"""Kerberos configuration and secret management."""
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, filedialog
|
from tkinter import ttk, messagebox, filedialog
|
||||||
from knoe import screen as ui
|
from knoe import screen as ui
|
||||||
@ -14,7 +10,6 @@ from knoe.core.env import (
|
|||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
_bool_str,
|
_bool_str,
|
||||||
_deployment_target_label,
|
_deployment_target_label,
|
||||||
_ensure_ansible_vault_credentials,
|
|
||||||
)
|
)
|
||||||
from knoe.config import (
|
from knoe.config import (
|
||||||
SECRET_KEY_SPECS,
|
SECRET_KEY_SPECS,
|
||||||
@ -355,69 +350,13 @@ class SecurityScreenMixin:
|
|||||||
return value
|
return value
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _vault_password_files() -> list[Path]:
|
|
||||||
files: list[Path] = []
|
|
||||||
env_pw = os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE", "").strip()
|
|
||||||
if env_pw:
|
|
||||||
files.append(Path(env_pw))
|
|
||||||
for base in (Path.cwd(), PROJECT_ROOT):
|
|
||||||
files.append(base / ".vault_pass")
|
|
||||||
seen: set[Path] = set()
|
|
||||||
unique: list[Path] = []
|
|
||||||
for item in files:
|
|
||||||
if item in seen:
|
|
||||||
continue
|
|
||||||
seen.add(item)
|
|
||||||
unique.append(item)
|
|
||||||
return unique
|
|
||||||
|
|
||||||
def _resolve_samba_admin_password_from_vault(self) -> str:
|
def _resolve_samba_admin_password_from_vault(self) -> str:
|
||||||
vault_path = (
|
|
||||||
PROJECT_ROOT
|
|
||||||
/ "infrastructure"
|
|
||||||
/ "inventory"
|
|
||||||
/ "group_vars"
|
|
||||||
/ "ad_dc"
|
|
||||||
/ "vault.yml"
|
|
||||||
)
|
|
||||||
if not vault_path.exists():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = vault_path.read_text(encoding="utf-8")
|
from knoe.core.onepassword import get_secret, op_available
|
||||||
|
if op_available():
|
||||||
|
return get_secret("samba-dns-admin", "password")
|
||||||
except Exception:
|
except Exception:
|
||||||
raw = ""
|
pass
|
||||||
if raw and "$ANSIBLE_VAULT;" not in raw:
|
|
||||||
return self._yaml_scalar(raw, "vault_samba_dns_admin_pass")
|
|
||||||
|
|
||||||
if shutil.which("ansible-vault") is None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
|
||||||
for password_file in self._vault_password_files():
|
|
||||||
if not password_file.is_file():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
res = subprocess.run(
|
|
||||||
[
|
|
||||||
"ansible-vault",
|
|
||||||
"view",
|
|
||||||
str(vault_path),
|
|
||||||
"--vault-password-file",
|
|
||||||
str(password_file),
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if res.returncode != 0:
|
|
||||||
continue
|
|
||||||
value = self._yaml_scalar(res.stdout or "", "vault_samba_dns_admin_pass")
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _load_secret_cache_from_cfg(self):
|
def _load_secret_cache_from_cfg(self):
|
||||||
@ -636,70 +575,18 @@ class SecurityScreenMixin:
|
|||||||
self.safe_after(self._run_kerberos_init)
|
self.safe_after(self._run_kerberos_init)
|
||||||
|
|
||||||
def _save_ansible_knoe_vault(self, root_password: str):
|
def _save_ansible_knoe_vault(self, root_password: str):
|
||||||
|
"""Store the DB master password in the 1Password knoey vault."""
|
||||||
if not root_password:
|
if not root_password:
|
||||||
return
|
return
|
||||||
|
|
||||||
vault_path = (
|
|
||||||
PROJECT_ROOT
|
|
||||||
/ "infrastructure"
|
|
||||||
/ "inventory"
|
|
||||||
/ "group_vars"
|
|
||||||
/ "all"
|
|
||||||
/ "knoe_vault.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
|
||||||
vault_pass_file = os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE")
|
|
||||||
|
|
||||||
if not vault_pass_file or not Path(vault_pass_file).exists():
|
|
||||||
for base in (Path.cwd(), PROJECT_ROOT):
|
|
||||||
candidate = base / ".vault_pass"
|
|
||||||
if candidate.is_file():
|
|
||||||
vault_pass_file = str(candidate)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not vault_pass_file or not Path(vault_pass_file).exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
if shutil.which("ansible-vault") is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We must decrypt if it is a knoe secret
|
|
||||||
plain_pass = root_password
|
plain_pass = root_password
|
||||||
if _is_knoe_secret(root_password):
|
if _is_knoe_secret(root_password):
|
||||||
try:
|
try:
|
||||||
plain_pass = _decrypt_knoe_secret(root_password)
|
plain_pass = _decrypt_knoe_secret(root_password)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
from knoe.core.onepassword import set_secret, op_available
|
||||||
content = f'knoe_root_password: "{plain_pass}"\n'
|
if op_available():
|
||||||
|
set_secret("administrator", "password", plain_pass)
|
||||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
|
||||||
f.write(content)
|
|
||||||
tmp_name = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.chmod(tmp_name, 0o600)
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"ansible-vault",
|
|
||||||
"encrypt",
|
|
||||||
tmp_name,
|
|
||||||
"--vault-password-file",
|
|
||||||
vault_pass_file,
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
shutil.move(tmp_name, str(vault_path))
|
|
||||||
os.chmod(str(vault_path), 0o600)
|
|
||||||
finally:
|
|
||||||
if os.path.exists(tmp_name):
|
|
||||||
try:
|
|
||||||
os.unlink(tmp_name)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to save ansible vault: {e}")
|
print(f"[ERROR] Failed to save administrator password to 1Password: {e}")
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# init_ansible.sh
|
|
||||||
# Purpose:
|
|
||||||
# - Initialize or update the Ansible vault password file (.vault_pass)
|
|
||||||
# - Should be run after init_openbao.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
|
||||||
knoe_set_mode "${2:-}"
|
|
||||||
shift 2
|
|
||||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
|
||||||
knoe_set_mode "${1#*=}"
|
|
||||||
shift
|
|
||||||
fi
|
|
||||||
|
|
||||||
PROLE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
||||||
VAULT_PASS_FILE="${PROLE_VAULT_PASS_FILE:-${ANSIBLE_VAULT_PASSWORD_FILE:-$PROLE_ROOT/.vault_pass}}"
|
|
||||||
|
|
||||||
ACTION=${1:-initialize}
|
|
||||||
|
|
||||||
case "$ACTION" in
|
|
||||||
initialize|update)
|
|
||||||
# Use PROLE_PASSWD if set, otherwise prompt
|
|
||||||
if [[ -n "${PROLE_PASSWD:-}" ]]; then
|
|
||||||
echo "Using PROLE_PASSWD for Ansible Vault password."
|
|
||||||
echo "$PROLE_PASSWD" > "$VAULT_PASS_FILE"
|
|
||||||
else
|
|
||||||
echo -n "Enter Ansible Vault password: "
|
|
||||||
read -rs vault_pass
|
|
||||||
echo
|
|
||||||
echo "$vault_pass" > "$VAULT_PASS_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 600 "$VAULT_PASS_FILE"
|
|
||||||
echo "Ansible Vault password file $ACTION""d at $VAULT_PASS_FILE"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {initialize|update}" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
||||||
|
|
||||||
# Where k3s keeps the server join token on the first/control-plane server
|
|
||||||
TOKEN_FILE="${TOKEN_FILE:-/var/lib/rancher/k3s/server/node-token}"
|
|
||||||
|
|
||||||
# Pick a dedicated vault file so we don't stomp your existing vault.yml.
|
|
||||||
# Change this if you want it somewhere else.
|
|
||||||
VAULT_FILE="${VAULT_FILE:-infrastructure/inventory/group_vars/all/vault_k3s.yml}"
|
|
||||||
|
|
||||||
# Variable name to store in the vault
|
|
||||||
VAR_NAME="${VAR_NAME:-vault_k3s_token}"
|
|
||||||
|
|
||||||
if [[ ! -r "$TOKEN_FILE" ]]; then
|
|
||||||
echo "ERROR: Cannot read token file: $TOKEN_FILE"
|
|
||||||
echo "Are you running this on a k3s server (pi.knoe.org)?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v ansible-vault >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: ansible-vault not found in PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TOKEN="$(sudo cat "$TOKEN_FILE" | tr -d '\r\n')"
|
|
||||||
|
|
||||||
if [[ -z "$TOKEN" ]]; then
|
|
||||||
echo "ERROR: Token read from $TOKEN_FILE is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$VAULT_FILE")"
|
|
||||||
|
|
||||||
TMP="$(mktemp)"
|
|
||||||
trap 'rm -f "$TMP"' EXIT
|
|
||||||
|
|
||||||
# Build a vault yaml with exactly one variable
|
|
||||||
# (ansible-vault encrypt_string outputs a full YAML block)
|
|
||||||
ansible-vault encrypt_string \
|
|
||||||
--name "$VAR_NAME" \
|
|
||||||
"$TOKEN" > "$TMP"
|
|
||||||
|
|
||||||
# If the vault file already exists, avoid duplicate var definitions:
|
|
||||||
# - if VAR_NAME already present, we replace the whole file (simple + safe)
|
|
||||||
# - otherwise, append
|
|
||||||
if [[ -f "$VAULT_FILE" ]]; then
|
|
||||||
if grep -qE "^\s*${VAR_NAME}:" "$VAULT_FILE"; then
|
|
||||||
echo "Updating existing $VAR_NAME in $VAULT_FILE (replacing file contents)."
|
|
||||||
mv "$TMP" "$VAULT_FILE"
|
|
||||||
else
|
|
||||||
echo "Appending $VAR_NAME to $VAULT_FILE"
|
|
||||||
printf "\n" >> "$VAULT_FILE"
|
|
||||||
cat "$TMP" >> "$VAULT_FILE"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Creating vault file: $VAULT_FILE"
|
|
||||||
mv "$TMP" "$VAULT_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 0600 "$VAULT_FILE"
|
|
||||||
|
|
||||||
echo "Done."
|
|
||||||
echo "Wrote: $VAULT_FILE"
|
|
||||||
echo "Var: $VAR_NAME"
|
|
||||||
echo
|
|
||||||
echo "Verify with:"
|
|
||||||
echo " ansible -i infrastructure/inventory/hosts.ini pi.knoe.org -m assert -a 'that=${VAR_NAME} is defined' --ask-vault-pass"
|
|
||||||
@ -2,31 +2,25 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add project root to path
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||||
sys.path.append(str(ROOT_DIR))
|
sys.path.append(str(ROOT_DIR))
|
||||||
|
|
||||||
from knoe import config as inst_config
|
from knoe import config as inst_config
|
||||||
|
from knoe.core.onepassword import get_secret, op_available
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
vault_path = (
|
token = ""
|
||||||
ROOT_DIR
|
if op_available():
|
||||||
/ "infrastructure"
|
token = get_secret("k3s-token", "credential").strip()
|
||||||
/ "inventory"
|
|
||||||
/ "group_vars"
|
|
||||||
/ "all"
|
|
||||||
/ "vault_k3s.yml"
|
|
||||||
)
|
|
||||||
token = inst_config._try_read_ansible_vault_value(vault_path, "vault_k3s_token")
|
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
print(f"Syncing K3S_TOKEN to conf/knoe.cfg...")
|
print("Syncing K3S_TOKEN to conf/knoe.cfg...")
|
||||||
inst_config._update_knoe_cfg_value("Global", "PROLE_K3S_TOKEN", token)
|
inst_config._update_knoe_cfg_value("Global", "PROLE_K3S_TOKEN", token)
|
||||||
inst_config._update_knoe_cfg_value("Inputs", "init_cluster.k3s_token", token)
|
inst_config._update_knoe_cfg_value("Inputs", "init_cluster.k3s_token", token)
|
||||||
inst_config._update_knoe_cfg_value("Service Cluster (k3s)", "K3S_TOKEN", token)
|
inst_config._update_knoe_cfg_value("Service Cluster (k3s)", "K3S_TOKEN", token)
|
||||||
else:
|
else:
|
||||||
print("Warning: Could not extract vault_k3s_token from Ansible vault.")
|
print("Warning: Could not retrieve k3s-token from 1Password knoey vault.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -418,24 +418,11 @@ resolve_samba_vars() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Try to resolve password from Ansible Vault
|
# 4. Try to resolve password from 1Password
|
||||||
if [[ -z "$SAMBA_ADMIN_PASSWORD" ]]; then
|
if [[ -z "$SAMBA_ADMIN_PASSWORD" ]]; then
|
||||||
local vault_file="${root}/infrastructure/inventory/group_vars/ad_dc/vault.yml"
|
if command -v op >/dev/null 2>&1 && op whoami >/dev/null 2>&1; then
|
||||||
local vault_pass_file=""
|
SAMBA_ADMIN_PASSWORD=$(op item get "samba-dns-admin" --vault knoey --fields password --reveal 2>/dev/null || true)
|
||||||
if [[ -n "${ANSIBLE_VAULT_PASSWORD_FILE:-}" && -f "${ANSIBLE_VAULT_PASSWORD_FILE}" ]]; then
|
[[ -n "$SAMBA_ADMIN_PASSWORD" ]] && echo "Fetched SAMBA_ADMIN_PASSWORD from 1Password knoey vault"
|
||||||
vault_pass_file="${ANSIBLE_VAULT_PASSWORD_FILE}"
|
|
||||||
elif [[ -n "${KNOE_HOME:-}" && -f "${KNOE_HOME}/.vault_pass" ]]; then
|
|
||||||
vault_pass_file="${KNOE_HOME}/.vault_pass"
|
|
||||||
elif [[ -f "${root}/.vault_pass" ]]; then
|
|
||||||
vault_pass_file="${root}/.vault_pass"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$vault_file" && -n "$vault_pass_file" && -x "$(command -v ansible-vault)" ]]; then
|
|
||||||
local vault_out
|
|
||||||
vault_out=$(ansible-vault view "$vault_file" --vault-password-file "$vault_pass_file" 2>/dev/null || true)
|
|
||||||
if [[ -n "$vault_out" ]]; then
|
|
||||||
SAMBA_ADMIN_PASSWORD=$(printf '%s\n' "$vault_out" | awk -F: '/^[[:space:]]*vault_samba_dns_admin_pass[[:space:]]*:/ {sub(/^[^:]+:[[:space:]]*/, "", $0); gsub(/"/, "", $0); print $0; exit}')
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
143
tests/etc/test_init_1password.sh
Normal file
143
tests/etc/test_init_1password.sh
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tests for etc/init_1password.sh using a stub 'op' command.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
INIT_SCRIPT="${REPO_ROOT}/etc/init_1password.sh"
|
||||||
|
|
||||||
|
pass_count=0
|
||||||
|
fail_count=0
|
||||||
|
|
||||||
|
_pass() { echo " [PASS] $*"; pass_count=$((pass_count + 1)); }
|
||||||
|
_fail() { echo " [FAIL] $*"; fail_count=$((fail_count + 1)); }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 1: --min flag skips preflight
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 1: --min mode skips preflight"
|
||||||
|
output=$(bash "$INIT_SCRIPT" --min 2>&1)
|
||||||
|
if echo "$output" | grep -q "Skipping"; then
|
||||||
|
_pass "--min skips 1Password preflight"
|
||||||
|
else
|
||||||
|
_fail "--min did not produce skip message; got: $output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 2: missing op CLI exits with error
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 2: missing op CLI fails"
|
||||||
|
output=$(PATH=/nonexistent bash "$INIT_SCRIPT" 2>&1 || true)
|
||||||
|
if echo "$output" | grep -qi "not found\|install"; then
|
||||||
|
_pass "missing op CLI produces helpful error"
|
||||||
|
else
|
||||||
|
_fail "missing op CLI did not produce helpful error; got: $output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 3: signed-in stub — vault already exists + administrator item exists
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 3: vault exists, administrator item exists — no create calls"
|
||||||
|
|
||||||
|
STUB_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STUB_DIR"' EXIT
|
||||||
|
|
||||||
|
cat > "${STUB_DIR}/op" << 'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
case "${1:-} ${2:-}" in
|
||||||
|
"whoami "*) exit 0 ;;
|
||||||
|
"vault get") exit 0 ;; # knoey vault exists
|
||||||
|
"item get") exit 0 ;; # administrator item exists
|
||||||
|
"vault list") printf '[{"name":"knoey"}]'; exit 0 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
chmod +x "${STUB_DIR}/op"
|
||||||
|
|
||||||
|
op_calls_file="$(mktemp)"
|
||||||
|
cat > "${STUB_DIR}/op" << STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "\$*" >> "${op_calls_file}"
|
||||||
|
case "\${1:-} \${2:-}" in
|
||||||
|
"whoami "*) exit 0 ;;
|
||||||
|
"vault get") exit 0 ;;
|
||||||
|
"item get") exit 0 ;;
|
||||||
|
"vault list") printf '[{"name":"knoey"}]'; exit 0 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
chmod +x "${STUB_DIR}/op"
|
||||||
|
|
||||||
|
PATH="${STUB_DIR}:$PATH" bash "$INIT_SCRIPT" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if ! grep -q "vault create" "${op_calls_file}" 2>/dev/null; then
|
||||||
|
_pass "vault create not called when vault already exists"
|
||||||
|
else
|
||||||
|
_fail "vault create was called unexpectedly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "item create" "${op_calls_file}" 2>/dev/null; then
|
||||||
|
_pass "item create not called when administrator item exists"
|
||||||
|
else
|
||||||
|
_fail "item create was called unexpectedly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 4: vault missing → vault create is called
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 4: vault missing — vault create is called"
|
||||||
|
op_calls_file2="$(mktemp)"
|
||||||
|
cat > "${STUB_DIR}/op" << STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "\$*" >> "${op_calls_file2}"
|
||||||
|
case "\${1:-} \${2:-}" in
|
||||||
|
"whoami "*) exit 0 ;;
|
||||||
|
"vault get") exit 1 ;; # vault does NOT exist
|
||||||
|
"vault list") printf '[]'; exit 0 ;;
|
||||||
|
"vault create") exit 0 ;;
|
||||||
|
"item get") exit 0 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
chmod +x "${STUB_DIR}/op"
|
||||||
|
|
||||||
|
PATH="${STUB_DIR}:$PATH" bash "$INIT_SCRIPT" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if grep -q "vault create" "${op_calls_file2}" 2>/dev/null; then
|
||||||
|
_pass "vault create called when vault is absent"
|
||||||
|
else
|
||||||
|
_fail "vault create was NOT called when vault is absent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 5: administrator item missing → item create is called
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 5: administrator item missing — item create is called"
|
||||||
|
op_calls_file3="$(mktemp)"
|
||||||
|
cat > "${STUB_DIR}/op" << STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "\$*" >> "${op_calls_file3}"
|
||||||
|
case "\${1:-} \${2:-}" in
|
||||||
|
"whoami "*) exit 0 ;;
|
||||||
|
"vault get") exit 0 ;;
|
||||||
|
"item get") exit 1 ;; # administrator does NOT exist
|
||||||
|
"item create") exit 0 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
chmod +x "${STUB_DIR}/op"
|
||||||
|
|
||||||
|
PATH="${STUB_DIR}:$PATH" bash "$INIT_SCRIPT" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if grep -q "item create" "${op_calls_file3}" 2>/dev/null; then
|
||||||
|
_pass "item create called when administrator item is absent"
|
||||||
|
else
|
||||||
|
_fail "item create was NOT called when administrator item is absent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: ${pass_count} passed, ${fail_count} failed"
|
||||||
|
[[ "$fail_count" -eq 0 ]]
|
||||||
@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Unit test for etc/init_ansible.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
KNOE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
|
||||||
ETC_DIR="$KNOE_HOME/etc"
|
|
||||||
SCRIPT_UNDER_TEST="$ETC_DIR/init_ansible.sh"
|
|
||||||
|
|
||||||
TMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Mock tools
|
|
||||||
mock_tool() {
|
|
||||||
cat <<M_EOF > "$TMP_DIR/$1"
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "Mocked $1 called with \$@" >> "$TMP_DIR/mock_calls.log"
|
|
||||||
exit 0
|
|
||||||
M_EOF
|
|
||||||
chmod +x "$TMP_DIR/$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create mocks for common tools
|
|
||||||
mock_tool kubectl
|
|
||||||
mock_tool curl
|
|
||||||
mock_tool docker
|
|
||||||
mock_tool k3d
|
|
||||||
mock_tool ansible-playbook
|
|
||||||
mock_tool tofu
|
|
||||||
mock_tool terraform
|
|
||||||
mock_tool ollama
|
|
||||||
mock_tool jq
|
|
||||||
|
|
||||||
# Mock knoe.cfg
|
|
||||||
mkdir -p "$TMP_DIR/conf"
|
|
||||||
cat <<C_EOF > "$TMP_DIR/conf/knoe.cfg"
|
|
||||||
[globals]
|
|
||||||
knoe.home = $KNOE_HOME
|
|
||||||
knoe.mode = k3d
|
|
||||||
[k3s]
|
|
||||||
server = https://localhost:6443
|
|
||||||
token = test-token
|
|
||||||
C_EOF
|
|
||||||
|
|
||||||
export PATH="$TMP_DIR:$PATH"
|
|
||||||
export KNOE_HOME="$KNOE_HOME"
|
|
||||||
export KNOE_CONF="$TMP_DIR/conf"
|
|
||||||
export NAMESPACE="test-ns"
|
|
||||||
export PROLE_PASSWD="test-password"
|
|
||||||
export PROLE_VAULT_PASS_FILE="$TMP_DIR/.vault_pass"
|
|
||||||
|
|
||||||
# Run the script
|
|
||||||
if [[ "init_ansible.sh" == *.py ]]; then
|
|
||||||
if [[ "init_ansible.sh" == "render_manifest.py" ]]; then
|
|
||||||
echo "apiVersion: v1" > "$TMP_DIR/test.yaml"
|
|
||||||
python3 "$SCRIPT_UNDER_TEST" "$TMP_DIR/test.yaml" > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
else
|
|
||||||
python3 "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
fi
|
|
||||||
RC=$?
|
|
||||||
else
|
|
||||||
# Check for usage() or if it's a script that likely needs arguments
|
|
||||||
if grep -q "usage()" "$SCRIPT_UNDER_TEST"; then
|
|
||||||
bash "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
RC=$?
|
|
||||||
else
|
|
||||||
# Try a few common non-destructive actions
|
|
||||||
bash "$SCRIPT_UNDER_TEST" status > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" || \
|
|
||||||
bash "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" || \
|
|
||||||
bash "$SCRIPT_UNDER_TEST" > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
RC=$?
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $RC -eq 0 || $RC -eq 1 || $RC -eq 2 ]]; then
|
|
||||||
echo "SUCCESS (RC=$RC)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "FAILURE with RC $RC"
|
|
||||||
cat "$TMP_DIR/stderr"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Unit test for etc/set-k3s-token-vault.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
KNOE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
|
||||||
ETC_DIR="$KNOE_HOME/etc"
|
|
||||||
SCRIPT_UNDER_TEST="$ETC_DIR/set-k3s-token-vault.sh"
|
|
||||||
|
|
||||||
TMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Mock tools
|
|
||||||
mock_tool() {
|
|
||||||
cat <<M_EOF > "$TMP_DIR/$1"
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "Mocked $1 called with \$@" >> "$TMP_DIR/mock_calls.log"
|
|
||||||
exit 0
|
|
||||||
M_EOF
|
|
||||||
chmod +x "$TMP_DIR/$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create mocks for common tools
|
|
||||||
mock_tool kubectl
|
|
||||||
mock_tool curl
|
|
||||||
mock_tool docker
|
|
||||||
mock_tool k3d
|
|
||||||
mock_tool ansible-playbook
|
|
||||||
mock_tool tofu
|
|
||||||
mock_tool terraform
|
|
||||||
mock_tool ollama
|
|
||||||
mock_tool jq
|
|
||||||
|
|
||||||
# Mock knoe.cfg
|
|
||||||
mkdir -p "$TMP_DIR/conf"
|
|
||||||
cat <<C_EOF > "$TMP_DIR/conf/knoe.cfg"
|
|
||||||
[globals]
|
|
||||||
knoe.home = $KNOE_HOME
|
|
||||||
knoe.mode = k3d
|
|
||||||
[k3s]
|
|
||||||
server = https://localhost:6443
|
|
||||||
token = test-token
|
|
||||||
C_EOF
|
|
||||||
|
|
||||||
export PATH="$TMP_DIR:$PATH"
|
|
||||||
export KNOE_HOME="$KNOE_HOME"
|
|
||||||
export KNOE_CONF="$TMP_DIR/conf"
|
|
||||||
export NAMESPACE="test-ns"
|
|
||||||
export PROLE_PASSWD="test-password"
|
|
||||||
|
|
||||||
# Run the script
|
|
||||||
if [[ "set-k3s-token-vault.sh" == *.py ]]; then
|
|
||||||
if [[ "set-k3s-token-vault.sh" == "render_manifest.py" ]]; then
|
|
||||||
echo "apiVersion: v1" > "$TMP_DIR/test.yaml"
|
|
||||||
python3 "$SCRIPT_UNDER_TEST" "$TMP_DIR/test.yaml" > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
else
|
|
||||||
python3 "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
fi
|
|
||||||
RC=$?
|
|
||||||
else
|
|
||||||
# Check for usage() or if it's a script that likely needs arguments
|
|
||||||
if grep -q "usage()" "$SCRIPT_UNDER_TEST"; then
|
|
||||||
bash "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
RC=$?
|
|
||||||
else
|
|
||||||
# Try a few common non-destructive actions
|
|
||||||
bash "$SCRIPT_UNDER_TEST" status > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" || \
|
|
||||||
bash "$SCRIPT_UNDER_TEST" --help > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" || \
|
|
||||||
bash "$SCRIPT_UNDER_TEST" > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
|
||||||
RC=$?
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $RC -eq 0 || $RC -eq 1 || $RC -eq 2 ]]; then
|
|
||||||
echo "SUCCESS (RC=$RC)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "FAILURE with RC $RC"
|
|
||||||
cat "$TMP_DIR/stderr"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -3,7 +3,6 @@ Unit tests for previously untested installer/config.py helpers.
|
|||||||
|
|
||||||
Covers:
|
Covers:
|
||||||
_get_secret_key_file, _get_file_key, normalize_version,
|
_get_secret_key_file, _get_file_key, normalize_version,
|
||||||
_extract_inline_vault_block, _try_read_ansible_vault_value,
|
|
||||||
get_dep_info (check_cmd/version_cmd paths), load_dependencies,
|
get_dep_info (check_cmd/version_cmd paths), load_dependencies,
|
||||||
get_ui_icon_image_path, get_ui_background_image_path, get_properties
|
get_ui_icon_image_path, get_ui_background_image_path, get_properties
|
||||||
"""
|
"""
|
||||||
@ -18,8 +17,6 @@ from knoe.config import (
|
|||||||
_get_file_key,
|
_get_file_key,
|
||||||
normalize_version,
|
normalize_version,
|
||||||
_extract_yaml_scalar_from_text,
|
_extract_yaml_scalar_from_text,
|
||||||
_extract_inline_vault_block,
|
|
||||||
_try_read_ansible_vault_value,
|
|
||||||
get_dep_info,
|
get_dep_info,
|
||||||
detect_dependency_platform,
|
detect_dependency_platform,
|
||||||
get_dependency_milestone_title,
|
get_dependency_milestone_title,
|
||||||
@ -116,119 +113,19 @@ def test_extract_yaml_scalar_missing_key():
|
|||||||
assert _extract_yaml_scalar_from_text("a: b\n", "missing") == ""
|
assert _extract_yaml_scalar_from_text("a: b\n", "missing") == ""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def test_extract_yaml_scalar_plain_value():
|
||||||
# _extract_inline_vault_block
|
"""Plain YAML scalar extraction works for config files."""
|
||||||
# ---------------------------------------------------------------------------
|
text = "db_password: supersecret\n"
|
||||||
|
result = _extract_yaml_scalar_from_text(text, "db_password")
|
||||||
def test_extract_inline_vault_block_not_found():
|
|
||||||
text = "key: value\nother: x\n"
|
|
||||||
result = _extract_inline_vault_block(text, "missing")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_inline_vault_block_plain_value():
|
|
||||||
"""Key found but block doesn't start with $ANSIBLE_VAULT → return ''."""
|
|
||||||
text = "mykey:\n just some plain text\n more text\n"
|
|
||||||
result = _extract_inline_vault_block(text, "mykey")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_inline_vault_block_ansible_vault():
|
|
||||||
text = (
|
|
||||||
"mykey:\n"
|
|
||||||
" $ANSIBLE_VAULT;1.1;AES256\n"
|
|
||||||
" 6162636465666768\n"
|
|
||||||
)
|
|
||||||
result = _extract_inline_vault_block(text, "mykey")
|
|
||||||
assert "$ANSIBLE_VAULT" in result
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_inline_vault_block_no_block_lines():
|
|
||||||
"""Key found but indented block is empty → return ''."""
|
|
||||||
text = "mykey:\nother: x\n"
|
|
||||||
result = _extract_inline_vault_block(text, "mykey")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# _try_read_ansible_vault_value
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_file_not_found(tmp_path):
|
|
||||||
result = _try_read_ansible_vault_value(tmp_path / "nonexistent.cfg", "mykey")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_plain_text(tmp_path):
|
|
||||||
vault = tmp_path / "knoe.cfg"
|
|
||||||
vault.write_text("db_password: supersecret\n")
|
|
||||||
result = _try_read_ansible_vault_value(vault, "db_password")
|
|
||||||
assert result == "supersecret"
|
assert result == "supersecret"
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_plain_text_vault_marker_skipped(tmp_path):
|
def test_extract_yaml_scalar_missing_key():
|
||||||
"""Plain value starting with $ANSIBLE_VAULT is treated as encrypted → skip."""
|
text = "other_key: value\n"
|
||||||
vault = tmp_path / "knoe.cfg"
|
result = _extract_yaml_scalar_from_text(text, "db_password")
|
||||||
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
|
|
||||||
# No vault password file → returns ""
|
|
||||||
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \
|
|
||||||
patch("knoe.config.shutil.which", return_value=None):
|
|
||||||
result = _try_read_ansible_vault_value(vault, "db_password")
|
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_no_password_file(tmp_path):
|
|
||||||
vault = tmp_path / "knoe.cfg"
|
|
||||||
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
|
|
||||||
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False), \
|
|
||||||
patch("knoe.config.shutil.which", return_value=None):
|
|
||||||
result = _try_read_ansible_vault_value(vault, "db_password")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_with_vault_success(tmp_path):
|
|
||||||
vault = tmp_path / "knoe.cfg"
|
|
||||||
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
|
|
||||||
pw_file = tmp_path / ".vault_pass"
|
|
||||||
pw_file.write_text("mypassword\n")
|
|
||||||
|
|
||||||
mock_result = MagicMock()
|
|
||||||
mock_result.returncode = 0
|
|
||||||
mock_result.stdout = "db_password: decryptedvalue\n"
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \
|
|
||||||
patch("knoe.config.shutil.which", return_value="/usr/bin/ansible-vault"), \
|
|
||||||
patch("knoe.config.subprocess.run", return_value=mock_result):
|
|
||||||
result = _try_read_ansible_vault_value(vault, "db_password")
|
|
||||||
assert result == "decryptedvalue"
|
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_vault_fails_no_inline(tmp_path):
|
|
||||||
vault = tmp_path / "knoe.cfg"
|
|
||||||
vault.write_text("db_password: $ANSIBLE_VAULT;1.1;AES256\n")
|
|
||||||
pw_file = tmp_path / ".vault_pass"
|
|
||||||
pw_file.write_text("mypassword\n")
|
|
||||||
|
|
||||||
mock_result = MagicMock()
|
|
||||||
mock_result.returncode = 1
|
|
||||||
mock_result.stdout = ""
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": str(pw_file)}, clear=False), \
|
|
||||||
patch("knoe.config.shutil.which", return_value="/usr/bin/ansible-vault"), \
|
|
||||||
patch("knoe.config.subprocess.run", return_value=mock_result):
|
|
||||||
result = _try_read_ansible_vault_value(vault, "db_password")
|
|
||||||
assert result == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_try_read_ansible_vault_value_uses_project_root_vault_pass(tmp_path):
|
|
||||||
"""Falls back to PROJECT_ROOT/.vault_pass if no env var set."""
|
|
||||||
vault = tmp_path / "knoe.cfg"
|
|
||||||
vault.write_text("mykey: plainvalue\n")
|
|
||||||
with patch.dict(os.environ, {"ANSIBLE_VAULT_PASSWORD_FILE": ""}, clear=False):
|
|
||||||
result = _try_read_ansible_vault_value(vault, "mykey")
|
|
||||||
assert result == "plainvalue"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# get_dep_info — check_cmd and version_cmd paths
|
# get_dep_info — check_cmd and version_cmd paths
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -394,7 +394,7 @@ class TestKnoeController:
|
|||||||
with (
|
with (
|
||||||
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_load_db_password_from_ansible_vault", return_value=""
|
installer, "_load_db_password_from_1password", return_value=""
|
||||||
),
|
),
|
||||||
mock.patch.object(installer, "_write_cfg"),
|
mock.patch.object(installer, "_write_cfg"),
|
||||||
mock.patch.object(installer, "_perform_cluster_reset"),
|
mock.patch.object(installer, "_perform_cluster_reset"),
|
||||||
@ -405,7 +405,7 @@ class TestKnoeController:
|
|||||||
|
|
||||||
assert rc == 2
|
assert rc == 2
|
||||||
|
|
||||||
def test_silent_installer_loads_db_password_from_ansible_vault(self, tmp_path):
|
def test_silent_installer_loads_db_password_from_1password(self, tmp_path):
|
||||||
c = KnoeController(tmp_path)
|
c = KnoeController(tmp_path)
|
||||||
installer = KnoeConsoleInstaller(c)
|
installer = KnoeConsoleInstaller(c)
|
||||||
|
|
||||||
@ -413,7 +413,7 @@ class TestKnoeController:
|
|||||||
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer,
|
installer,
|
||||||
"_load_db_password_from_ansible_vault",
|
"_load_db_password_from_1password",
|
||||||
return_value="vaultpw123",
|
return_value="vaultpw123",
|
||||||
),
|
),
|
||||||
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
||||||
@ -434,7 +434,7 @@ class TestKnoeController:
|
|||||||
assert "deployment" in captured["ids"]
|
assert "deployment" in captured["ids"]
|
||||||
assert write_cfg.call_count == 2
|
assert write_cfg.call_count == 2
|
||||||
|
|
||||||
def test_silent_installer_persists_bootstrap_password_to_ansible_vault(self, tmp_path):
|
def test_silent_installer_persists_bootstrap_password_to_1password(self, tmp_path):
|
||||||
c = KnoeController(tmp_path)
|
c = KnoeController(tmp_path)
|
||||||
installer = KnoeConsoleInstaller(c)
|
installer = KnoeConsoleInstaller(c)
|
||||||
fake_stdin = mock.Mock()
|
fake_stdin = mock.Mock()
|
||||||
@ -448,7 +448,7 @@ class TestKnoeController:
|
|||||||
with (
|
with (
|
||||||
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_load_db_password_from_ansible_vault", return_value=""
|
installer, "_load_db_password_from_1password", return_value=""
|
||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer,
|
installer,
|
||||||
@ -456,7 +456,7 @@ class TestKnoeController:
|
|||||||
return_value="bootstrap_pw_123",
|
return_value="bootstrap_pw_123",
|
||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_persist_db_password_to_ansible_vault"
|
installer, "_persist_db_password_to_1password"
|
||||||
) as persist_pw,
|
) as persist_pw,
|
||||||
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
||||||
mock.patch.object(installer, "_perform_cluster_reset"),
|
mock.patch.object(installer, "_perform_cluster_reset"),
|
||||||
@ -488,10 +488,10 @@ class TestKnoeController:
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_load_db_password_from_ansible_vault", return_value=""
|
installer, "_load_db_password_from_1password", return_value=""
|
||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_persist_db_password_to_ansible_vault"
|
installer, "_persist_db_password_to_1password"
|
||||||
) as persist_pw,
|
) as persist_pw,
|
||||||
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
||||||
mock.patch.object(installer, "_perform_cluster_reset"),
|
mock.patch.object(installer, "_perform_cluster_reset"),
|
||||||
@ -522,11 +522,11 @@ class TestKnoeController:
|
|||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer,
|
installer,
|
||||||
"_load_db_password_from_ansible_vault",
|
"_load_db_password_from_1password",
|
||||||
return_value="vault_pw_999",
|
return_value="vault_pw_999",
|
||||||
),
|
),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
installer, "_persist_db_password_to_ansible_vault"
|
installer, "_persist_db_password_to_1password"
|
||||||
) as persist_pw,
|
) as persist_pw,
|
||||||
mock.patch.object(installer, "_write_cfg"),
|
mock.patch.object(installer, "_write_cfg"),
|
||||||
mock.patch.object(installer, "_perform_cluster_reset"),
|
mock.patch.object(installer, "_perform_cluster_reset"),
|
||||||
|
|||||||
@ -21,7 +21,6 @@ from knoe.config import (
|
|||||||
_filter_cfg_values_for_persistence,
|
_filter_cfg_values_for_persistence,
|
||||||
_normalize_cfg_value_for_persistence,
|
_normalize_cfg_value_for_persistence,
|
||||||
_extract_yaml_scalar_from_text as cfg_extract_yaml,
|
_extract_yaml_scalar_from_text as cfg_extract_yaml,
|
||||||
_extract_inline_vault_block as cfg_extract_vault,
|
|
||||||
_load_properties,
|
_load_properties,
|
||||||
_write_k3s_kubeconfig,
|
_write_k3s_kubeconfig,
|
||||||
_merge_kubeconfig,
|
_merge_kubeconfig,
|
||||||
@ -406,10 +405,6 @@ class TestYamlHelpers:
|
|||||||
assert _extract_inline_vault_block("key: value", "key") == ""
|
assert _extract_inline_vault_block("key: value", "key") == ""
|
||||||
assert _extract_inline_vault_block("key: value", "missing") == ""
|
assert _extract_inline_vault_block("key: value", "missing") == ""
|
||||||
|
|
||||||
def test_cfg_extract_vault(self):
|
|
||||||
text = "secret: !vault |\n $ANSIBLE_VAULT;1.1;AES256\n data\nother: x"
|
|
||||||
result = cfg_extract_vault(text, "secret")
|
|
||||||
assert "$ANSIBLE_VAULT" in result
|
|
||||||
|
|
||||||
def test_parse_yaml_scalar_values(self, tmp_path):
|
def test_parse_yaml_scalar_values(self, tmp_path):
|
||||||
f = tmp_path / "test.yaml"
|
f = tmp_path / "test.yaml"
|
||||||
|
|||||||
168
tests/installer/test_onepassword.py
Normal file
168
tests/installer/test_onepassword.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
"""Unit tests for knoe.core.onepassword — mocks the op CLI subprocess calls."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from knoe.core.onepassword import (
|
||||||
|
op_available,
|
||||||
|
ensure_op_signed_in,
|
||||||
|
ensure_knoey_vault,
|
||||||
|
get_secret,
|
||||||
|
set_secret,
|
||||||
|
get_administrator_password,
|
||||||
|
ensure_administrator_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_result(returncode: int = 0, stdout: str = "", stderr: str = "") -> MagicMock:
|
||||||
|
m = MagicMock()
|
||||||
|
m.returncode = returncode
|
||||||
|
m.stdout = stdout
|
||||||
|
m.stderr = stderr
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpAvailable:
|
||||||
|
def test_found(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"):
|
||||||
|
assert op_available() is True
|
||||||
|
|
||||||
|
def test_not_found(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value=None):
|
||||||
|
assert op_available() is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureOpSignedIn:
|
||||||
|
def test_already_signed_in(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", return_value=_make_result(0)):
|
||||||
|
ensure_op_signed_in()
|
||||||
|
|
||||||
|
def test_not_signed_in_triggers_signin(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **_):
|
||||||
|
calls.append(cmd)
|
||||||
|
if "whoami" in cmd:
|
||||||
|
return _make_result(1)
|
||||||
|
return _make_result(0)
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", side_effect=fake_run):
|
||||||
|
ensure_op_signed_in()
|
||||||
|
|
||||||
|
assert any("signin" in c for c in calls)
|
||||||
|
|
||||||
|
def test_raises_when_op_missing(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value=None):
|
||||||
|
with pytest.raises(RuntimeError, match="1Password CLI"):
|
||||||
|
ensure_op_signed_in()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureKnoeyVault:
|
||||||
|
def test_vault_already_exists(self):
|
||||||
|
vaults = json.dumps([{"name": "knoey"}, {"name": "Personal"}])
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run",
|
||||||
|
return_value=_make_result(0, vaults)):
|
||||||
|
ensure_knoey_vault()
|
||||||
|
|
||||||
|
def test_vault_created_when_absent(self):
|
||||||
|
vaults = json.dumps([{"name": "Personal"}])
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **_):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
return _make_result(0, vaults if "list" in cmd else "")
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", side_effect=fake_run):
|
||||||
|
ensure_knoey_vault()
|
||||||
|
|
||||||
|
assert any("create" in c and "knoey" in c for c in calls)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSecret:
|
||||||
|
def test_returns_value(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run",
|
||||||
|
return_value=_make_result(0, "mysecret")):
|
||||||
|
result = get_secret("administrator")
|
||||||
|
assert result == "mysecret"
|
||||||
|
|
||||||
|
def test_returns_empty_on_failure(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run",
|
||||||
|
return_value=_make_result(1, "")):
|
||||||
|
result = get_secret("administrator")
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetSecret:
|
||||||
|
def test_edits_existing_item(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **_):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
return _make_result(0)
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", side_effect=fake_run):
|
||||||
|
set_secret("administrator", "password", "newpassword")
|
||||||
|
|
||||||
|
assert any("edit" in c for c in calls)
|
||||||
|
|
||||||
|
def test_creates_new_item(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **_):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
if "get" in cmd:
|
||||||
|
return _make_result(1)
|
||||||
|
return _make_result(0)
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", side_effect=fake_run):
|
||||||
|
set_secret("administrator", "password", "newpassword")
|
||||||
|
|
||||||
|
assert any("create" in c for c in calls)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetAdministratorPassword:
|
||||||
|
def test_delegates_to_get_secret(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run",
|
||||||
|
return_value=_make_result(0, "adminpass")):
|
||||||
|
result = get_administrator_password()
|
||||||
|
assert result == "adminpass"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureAdministratorSecret:
|
||||||
|
def test_returns_existing(self):
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run",
|
||||||
|
return_value=_make_result(0, "existingpass")):
|
||||||
|
result = ensure_administrator_secret()
|
||||||
|
assert result == "existingpass"
|
||||||
|
|
||||||
|
def test_creates_when_missing(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **_):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
if "get" in cmd and "--fields" in cmd:
|
||||||
|
return _make_result(0, "")
|
||||||
|
if "get" in cmd:
|
||||||
|
return _make_result(1)
|
||||||
|
return _make_result(0)
|
||||||
|
|
||||||
|
with patch("knoe.core.onepassword.shutil.which", return_value="/usr/bin/op"), \
|
||||||
|
patch("knoe.core.onepassword.subprocess.run", side_effect=fake_run):
|
||||||
|
result = ensure_administrator_secret()
|
||||||
|
|
||||||
|
assert isinstance(result, str) and len(result) == 32
|
||||||
|
assert any("create" in c for c in calls)
|
||||||
@ -151,7 +151,8 @@ def test_main_update_flag_dispatches_to_run_update(tmp_path, monkeypatch):
|
|||||||
installer.run.return_value = 0
|
installer.run.return_value = 0
|
||||||
|
|
||||||
monkeypatch.setattr(sys, "argv", ["install.py", "--update", "-c", str(cfg_path)])
|
monkeypatch.setattr(sys, "argv", ["install.py", "--update", "-c", str(cfg_path)])
|
||||||
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
|
monkeypatch.setattr(screens_mod, "ensure_op_signed_in", lambda: None)
|
||||||
|
monkeypatch.setattr(screens_mod, "ensure_knoey_vault", lambda: None)
|
||||||
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
|
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
|
||||||
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", MagicMock(return_value=installer))
|
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", MagicMock(return_value=installer))
|
||||||
|
|
||||||
@ -217,7 +218,8 @@ def test_main_delete_db_is_forwarded_to_console_installer(tmp_path, monkeypatch)
|
|||||||
"argv",
|
"argv",
|
||||||
["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)],
|
["install.py", "--silent", "--reset", "--delete-db", "-c", str(cfg_path)],
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(screens_mod, "_ensure_ansible_vault_credentials", lambda **_: None)
|
monkeypatch.setattr(screens_mod, "ensure_op_signed_in", lambda: None)
|
||||||
|
monkeypatch.setattr(screens_mod, "ensure_knoey_vault", lambda: None)
|
||||||
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
|
monkeypatch.setattr(screens_mod, "KnoeController", MagicMock(return_value=MagicMock()))
|
||||||
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", installer_factory)
|
monkeypatch.setattr(screens_mod, "KnoeConsoleInstaller", installer_factory)
|
||||||
|
|
||||||
|
|||||||
117
update.sh
117
update.sh
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# update.sh — Apply the vault master password to all knoe resources.
|
# update.sh — Apply the master password to all knoe resources.
|
||||||
#
|
#
|
||||||
# Reads vault_knoe_db_master_password from:
|
# Reads the administrator password from:
|
||||||
# infrastructure/inventory/group_vars/all/vault_db_master.yml
|
# 1Password knoey vault → item 'administrator' → field 'password'
|
||||||
#
|
#
|
||||||
# Applies the password to:
|
# Applies the password to:
|
||||||
# - k8s secrets: knoe-db-user, knoe-db-superuser (knoe-cnpg-0, knoe-db-0)
|
# - k8s secrets: knoe-db-user, knoe-db-superuser (knoe-cnpg-0, knoe-db-0)
|
||||||
@ -13,12 +13,11 @@
|
|||||||
# - OpenBao kv secrets (if reachable)
|
# - OpenBao kv secrets (if reachable)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./update.sh [--dry-run] [--prompt] [--vault-pass-file <path>]
|
# ./update.sh [--dry-run] [--prompt] [--skip-db] [--skip-grafana] [--skip-cfg]
|
||||||
# [--skip-db] [--skip-grafana] [--skip-cfg]
|
|
||||||
#
|
#
|
||||||
# --prompt Read password interactively (masked, confirmed twice) and
|
# --prompt Read password interactively (masked, confirmed twice) and
|
||||||
# re-create vault_db_master.yml with ansible-vault encrypt (whole-file).
|
# save it to 1Password knoey/administrator.
|
||||||
# Use this when the vault file is missing or corrupt.
|
# Use this when you need to rotate the master password.
|
||||||
#
|
#
|
||||||
# The script is idempotent — safe to run on every deploy or rotation.
|
# The script is idempotent — safe to run on every deploy or rotation.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@ -26,7 +25,7 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Auto-activate project virtualenv (provides ansible-vault, python3 packages)
|
# Auto-activate project virtualenv (provides python3 packages)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ -z "${VIRTUAL_ENV:-}" && -f "${ROOT_DIR}/bin/activate" ]]; then
|
if [[ -z "${VIRTUAL_ENV:-}" && -f "${ROOT_DIR}/bin/activate" ]]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
@ -41,9 +40,8 @@ PROMPT_MODE=false
|
|||||||
SKIP_DB=false
|
SKIP_DB=false
|
||||||
SKIP_GRAFANA=false
|
SKIP_GRAFANA=false
|
||||||
SKIP_CFG=false
|
SKIP_CFG=false
|
||||||
VAULT_PASS_FILE="${ROOT_DIR}/.vault_pass"
|
OP_VAULT="${OP_VAULT:-knoey}"
|
||||||
VAULT_FILE="${ROOT_DIR}/infrastructure/inventory/group_vars/all/vault_db_master.yml"
|
OP_ITEM="administrator"
|
||||||
VAULT_KEY="vault_knoe_db_master_password"
|
|
||||||
|
|
||||||
# Cluster contexts — override via env or auto-detect from config files
|
# Cluster contexts — override via env or auto-detect from config files
|
||||||
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-}"
|
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-}"
|
||||||
@ -63,7 +61,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--skip-db) SKIP_DB=true; shift ;;
|
--skip-db) SKIP_DB=true; shift ;;
|
||||||
--skip-grafana) SKIP_GRAFANA=true; shift ;;
|
--skip-grafana) SKIP_GRAFANA=true; shift ;;
|
||||||
--skip-cfg) SKIP_CFG=true; shift ;;
|
--skip-cfg) SKIP_CFG=true; shift ;;
|
||||||
--vault-pass-file) VAULT_PASS_FILE="${2:?}"; shift 2 ;;
|
--vault) OP_VAULT="${2:?}"; shift 2 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
grep '^#' "$0" | head -25 | sed 's/^# \?//'
|
grep '^#' "$0" | head -25 | sed 's/^# \?//'
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@ -144,70 +142,49 @@ kapp() { kubectl ${APP_CTX:+--context="$APP_CTX"} "$@"; }
|
|||||||
kdb() { kubectl ${DB_CTX:+--context="$DB_CTX"} "$@"; }
|
kdb() { kubectl ${DB_CTX:+--context="$DB_CTX"} "$@"; }
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 1a: Decrypt master password from Ansible vault
|
# Step 1a: Read master password from 1Password
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
decrypt_vault_password() {
|
read_1password_password() {
|
||||||
log "Decrypting master password from vault..."
|
log "Reading master password from 1Password vault '$OP_VAULT' item '$OP_ITEM'..."
|
||||||
|
|
||||||
if [[ ! -f "$VAULT_FILE" ]]; then
|
if ! command -v op >/dev/null 2>&1; then
|
||||||
fail "Vault file not found: $VAULT_FILE (run with --prompt to create it)"
|
fail "1Password CLI (op) not found. Install: brew install 1password-cli"
|
||||||
fi
|
|
||||||
if [[ ! -f "$VAULT_PASS_FILE" ]]; then
|
|
||||||
fail "Vault password file not found: $VAULT_PASS_FILE (set --vault-pass-file)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v ansible-vault >/dev/null 2>&1; then
|
if ! op whoami >/dev/null 2>&1; then
|
||||||
fail "ansible-vault not found — install ansible or activate the project virtualenv."
|
log "No active 1Password session. Signing in..."
|
||||||
|
op signin || fail "1Password sign-in failed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local decrypted
|
|
||||||
decrypted=$(ansible-vault view \
|
|
||||||
--vault-password-file "$VAULT_PASS_FILE" \
|
|
||||||
"$VAULT_FILE" 2>&1) || fail "ansible-vault view failed — vault file may be corrupt or unencrypted. Run: ./update.sh --prompt"
|
|
||||||
|
|
||||||
# Extract the value from YAML
|
|
||||||
local pw
|
local pw
|
||||||
pw=$(python3 - "$VAULT_KEY" <<PY
|
pw=$(op item get "$OP_ITEM" --vault "$OP_VAULT" --fields password --reveal 2>/dev/null) \
|
||||||
import sys, yaml
|
|| fail "Could not read '$OP_ITEM' from vault '$OP_VAULT'. Run: ./update.sh --prompt"
|
||||||
try:
|
|
||||||
data = yaml.safe_load("""${decrypted}""")
|
|
||||||
except Exception as e:
|
|
||||||
raise SystemExit(f"YAML parse error: {e}")
|
|
||||||
key = sys.argv[1]
|
|
||||||
if not isinstance(data, dict) or key not in data:
|
|
||||||
raise SystemExit(f"Key '{key}' not found in vault file. Available keys: {list(data.keys()) if isinstance(data, dict) else 'none'}")
|
|
||||||
print(data[key])
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ -z "$pw" ]]; then
|
if [[ -z "$pw" ]]; then
|
||||||
fail "Decrypted password is empty — check vault file key: $VAULT_KEY"
|
fail "Password is empty in 1Password vault '$OP_VAULT' item '$OP_ITEM'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s' "$pw"
|
printf '%s' "$pw"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 1b: Prompt for new password + recreate vault file (--prompt mode)
|
# Step 1b: Prompt for new password + save to 1Password (--prompt mode)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
prompt_and_recreate_vault() {
|
prompt_and_save_to_1password() {
|
||||||
log "Creating new master password and recreating vault file..."
|
log "Creating new master password and saving to 1Password..."
|
||||||
|
|
||||||
if [[ ! -f "$VAULT_PASS_FILE" ]]; then
|
if ! command -v op >/dev/null 2>&1; then
|
||||||
fail "Vault password file not found: $VAULT_PASS_FILE (set --vault-pass-file)"
|
fail "1Password CLI (op) not found. Install: brew install 1password-cli"
|
||||||
fi
|
fi
|
||||||
if ! command -v ansible-vault >/dev/null 2>&1; then
|
if ! op whoami >/dev/null 2>&1; then
|
||||||
fail "ansible-vault not found — activate the project virtualenv first."
|
op signin || fail "1Password sign-in failed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Read new password twice with masked echo
|
|
||||||
local pw1 pw2
|
local pw1 pw2
|
||||||
# Use /dev/tty so prompt works even when stdout is redirected
|
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
read -r -s -p "New master password: " pw1 </dev/tty; echo >/dev/tty
|
read -r -s -p "New master password: " pw1 </dev/tty; echo >/dev/tty
|
||||||
read -r -s -p "Confirm master password: " pw2 </dev/tty; echo >/dev/tty
|
read -r -s -p "Confirm master password: " pw2 </dev/tty; echo >/dev/tty
|
||||||
else
|
else
|
||||||
# Non-interactive fallback (e.g. piped input)
|
|
||||||
read -r pw1
|
read -r pw1
|
||||||
pw2="$pw1"
|
pw2="$pw1"
|
||||||
fi
|
fi
|
||||||
@ -219,22 +196,20 @@ prompt_and_recreate_vault() {
|
|||||||
fail "Passwords do not match — please try again."
|
fail "Passwords do not match — please try again."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Write a plain YAML file then encrypt the whole file.
|
if op item get "$OP_ITEM" --vault "$OP_VAULT" >/dev/null 2>&1; then
|
||||||
# We use whole-file encryption (ansible-vault encrypt) NOT encrypt_string,
|
op item edit "$OP_ITEM" --vault "$OP_VAULT" "password=${pw1}" >/dev/null \
|
||||||
# so that ansible-vault view can read it back correctly.
|
|| fail "Failed to update '$OP_ITEM' in vault '$OP_VAULT'."
|
||||||
log "Encrypting password with ansible-vault (whole-file)..."
|
log "Updated existing item '$OP_ITEM' in vault '$OP_VAULT'."
|
||||||
mkdir -p "$(dirname "$VAULT_FILE")"
|
else
|
||||||
local plain_tmp="${VAULT_FILE}.plain.tmp"
|
op item create \
|
||||||
printf -- '---\n%s: %s\n' "$VAULT_KEY" "$pw1" > "$plain_tmp"
|
--category login \
|
||||||
ansible-vault encrypt \
|
--title "$OP_ITEM" \
|
||||||
--vault-password-file "$VAULT_PASS_FILE" \
|
--vault "$OP_VAULT" \
|
||||||
--output "$VAULT_FILE" \
|
"password=${pw1}" >/dev/null \
|
||||||
"$plain_tmp" 2>/dev/null \
|
|| fail "Failed to create '$OP_ITEM' in vault '$OP_VAULT'."
|
||||||
|| { rm -f "$plain_tmp"; fail "ansible-vault encrypt failed — check vault password file."; }
|
log "Created item '$OP_ITEM' in vault '$OP_VAULT'."
|
||||||
rm -f "$plain_tmp"
|
fi
|
||||||
log "Vault file recreated: $VAULT_FILE"
|
|
||||||
|
|
||||||
# Return the plaintext password for immediate use
|
|
||||||
printf '%s' "$pw1"
|
printf '%s' "$pw1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,17 +524,17 @@ verify() {
|
|||||||
main() {
|
main() {
|
||||||
log "=== knoe update.sh — master password rotation ==="
|
log "=== knoe update.sh — master password rotation ==="
|
||||||
[[ "$DRY_RUN" == "true" ]] && log "(DRY-RUN mode — no changes will be made)"
|
[[ "$DRY_RUN" == "true" ]] && log "(DRY-RUN mode — no changes will be made)"
|
||||||
[[ "$PROMPT_MODE" == "true" ]] && log "(PROMPT mode — will recreate vault file)"
|
[[ "$PROMPT_MODE" == "true" ]] && log "(PROMPT mode — will update 1Password item)"
|
||||||
|
|
||||||
resolve_contexts
|
resolve_contexts
|
||||||
|
|
||||||
local MASTER_PW
|
local MASTER_PW
|
||||||
if [[ "$PROMPT_MODE" == "true" ]]; then
|
if [[ "$PROMPT_MODE" == "true" ]]; then
|
||||||
MASTER_PW=$(prompt_and_recreate_vault)
|
MASTER_PW=$(prompt_and_save_to_1password)
|
||||||
log "Vault file recreated and master password ready."
|
log "1Password item updated and master password ready."
|
||||||
else
|
else
|
||||||
MASTER_PW=$(decrypt_vault_password)
|
MASTER_PW=$(read_1password_password)
|
||||||
log "Vault master password decrypted successfully."
|
log "Master password read from 1Password successfully."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$SKIP_DB" != "true" ]]; then
|
if [[ "$SKIP_DB" != "true" ]]; then
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user