1305 lines
49 KiB
Fish
1305 lines
49 KiB
Fish
### ------- helpers ---------------------------------------------------------
|
|
|
|
function _need --description "ensure a command exists"
|
|
for c in $argv
|
|
if not type -q $c
|
|
echo (set_color red)"Error:"(set_color normal)" missing required command: $c" >&2
|
|
return 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function _banner --description "pretty section header" -a MSG
|
|
set_color cyan
|
|
echo
|
|
echo "==> $MSG"
|
|
set_color normal
|
|
end
|
|
|
|
# 32-char alphanumeric generator (fast, no shell-specials)
|
|
function _rand_alnum32
|
|
if type -q openssl
|
|
openssl rand -base64 48 2>/dev/null | tr -dc 'A-Za-z0-9' | head -c 32
|
|
else
|
|
dd if=/dev/urandom bs=48 count=1 2>/dev/null | base64 | tr -dc 'A-Za-z0-9' | head -c 32
|
|
end
|
|
end
|
|
|
|
# Convert any string to a k8s-safe name (RFC-1123 label-ish)
|
|
function _k8s_name --description "sanitize to RFC-1123 label" -a S
|
|
set s (string lower -- $S)
|
|
set s (string replace -ar -- '[^a-z0-9-]' '-' $s)
|
|
set s (string replace -r -- '^-+' '' $s)
|
|
set s (string replace -r -- '-+$' '' $s)
|
|
echo $s
|
|
end
|
|
|
|
# Simple non-empty check
|
|
function _require --description "require a non-empty value" -a WHAT VAL
|
|
if test -z "$VAL"
|
|
echo (set_color red)"Error:"(set_color normal)" $WHAT cannot be empty. Aborting." >&2
|
|
return 1
|
|
end
|
|
end
|
|
|
|
# List SCs in a stable way
|
|
function _sc_list --description "list storageclasses as menu"
|
|
kubectl get storageclass -o json | jq -r '
|
|
.items[]
|
|
| {name: .metadata.name,
|
|
provisioner: .provisioner,
|
|
reclaim: (.reclaimPolicy // "Delete"),
|
|
default: ( .metadata.annotations["storageclass.kubernetes.io/is-default-class"] == "true"
|
|
or .metadata.annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true") }
|
|
| "\(.name)\t\(.provisioner)\t\(.reclaim)\t\((.default|tostring))"
|
|
' 2>/dev/null
|
|
end
|
|
|
|
# convert XMR -> piconero (atomic units) with Python (exact
|
|
function _xmr_to_atomic -a AMT
|
|
_need python3; or return 1
|
|
printf '%s' 'from decimal import Decimal, getcontext
|
|
import sys
|
|
getcontext().prec = 50
|
|
amt = Decimal(sys.argv[1])
|
|
atomic = int((amt * Decimal("1000000000000")).to_integral_value(rounding="ROUND_HALF_UP"))
|
|
print(atomic)
|
|
' | python3 - "$AMT"
|
|
end
|
|
|
|
# Choose SC (menu -> stderr; selection -> stdout)
|
|
function _choose_sc --description "interactive StorageClass picker"
|
|
set lines (_sc_list)
|
|
if test (count $lines) -eq 0
|
|
echo (set_color red)"Error:"(set_color normal)" No StorageClasses found." >&2
|
|
return 1
|
|
end
|
|
|
|
echo "" >&2
|
|
echo "==> Available StorageClasses" >&2
|
|
echo " # | name | provisioner | reclaim | default" >&2
|
|
echo "----+----------------------+------------------------+---------+--------" >&2
|
|
set i 1
|
|
for l in $lines
|
|
set name (echo $l | awk -F'\t' '{print $1}')
|
|
set prov (echo $l | awk -F'\t' '{print $2}')
|
|
set rec (echo $l | awk -F'\t' '{print $3}')
|
|
set def (echo $l | awk -F'\t' '{print $4}')
|
|
printf " %2d | %-20s | %-22s | %-7s | %s\n" $i $name $prov $rec $def >&2
|
|
set i (math $i + 1)
|
|
end
|
|
|
|
set prefer (printf "%s\n" $lines | awk -F'\t' '$3=="Retain"{print $1}' | head -n1)
|
|
if test -z "$prefer"
|
|
set prefer (printf "%s\n" $lines | awk -F'\t' '$4=="true"{print $1}' | head -n1)
|
|
end
|
|
if test -z "$prefer"
|
|
set prefer (printf "%s\n" $lines | awk -F'\t' '{print $1}' | head -n1)
|
|
end
|
|
|
|
read -P "Pick StorageClass by number or name (blank = $prefer): " choice
|
|
|
|
if test -z "$choice"
|
|
echo "Using StorageClass: $prefer" >&2
|
|
echo $prefer
|
|
return 0
|
|
end
|
|
|
|
if string match -qr '^[0-9]+$' -- $choice
|
|
set idx (math $choice)
|
|
if test $idx -lt 1 -o $idx -gt (count $lines)
|
|
echo (set_color red)"Error:"(set_color normal)" Choice out of range." >&2
|
|
return 1
|
|
end
|
|
set sel (printf "%s\n" $lines[$idx] | awk -F'\t' '{print $1}')
|
|
echo "Using StorageClass: $sel" >&2
|
|
echo $sel
|
|
return 0
|
|
end
|
|
|
|
set found (printf "%s\n" $lines | awk -F'\t' -v want="$choice" '$1==want{print $1}')
|
|
if test -z "$found"
|
|
echo (set_color red)"Error:"(set_color normal)" No StorageClass named '$choice'." >&2
|
|
return 1
|
|
end
|
|
echo "Using StorageClass: $found" >&2
|
|
echo $found
|
|
end
|
|
|
|
# ---------- port-forward helpers (stderr logs) ----------
|
|
function _pf_start --description "start port-forward in bg: sets global PF_PID" -a NS SVC LOCAL REMOTE
|
|
set -g PF_PID ""
|
|
if test -z "$LOCAL"; set LOCAL 18083; end
|
|
if test -z "$REMOTE"; set REMOTE 18083; end
|
|
echo "Starting port-forward: svc/$SVC $LOCAL:$REMOTE (ns=$NS)…" >&2
|
|
kubectl -n $NS port-forward svc/$SVC $LOCAL:$REMOTE >/dev/null 2>&1 &
|
|
set -g PF_PID $last_pid
|
|
sleep 1
|
|
end
|
|
|
|
function _pf_stop --description "stop port-forward if running"
|
|
if test -n "$PF_PID"
|
|
echo "Stopping port-forward (pid $PF_PID)…" >&2
|
|
kill $PF_PID 2>/dev/null; or true
|
|
set -e PF_PID
|
|
end
|
|
end
|
|
|
|
# Minimal JSON-RPC caller for wallet RPC
|
|
function _rpc_call --description "call wallet JSON-RPC via localhost pf" -a RPCUSER RPCPASS METHOD PARAMS
|
|
set url "http://127.0.0.1:18083/json_rpc"
|
|
if test -z "$PARAMS"
|
|
set payload (printf '{"jsonrpc":"2.0","id":"0","method":"%s"}' $METHOD)
|
|
else
|
|
set payload (printf '{"jsonrpc":"2.0","id":"0","method":"%s","params":%s}' $METHOD "$PARAMS")
|
|
end
|
|
curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" "$url"
|
|
end
|
|
|
|
# Probe a monerod and assert it looks like Monero mainnet (not test/stage), not "qubic".
|
|
# Usage: _probe_monerod host:port
|
|
function _probe_monerod -a ADDR
|
|
if test -z "$ADDR"
|
|
echo (set_color red)"Error:"(set_color normal)" _probe_monerod needs host:port" >&2
|
|
return 1
|
|
end
|
|
|
|
if string match -qi '*qubic*' -- $ADDR
|
|
echo (set_color red)"Refusing daemon '$ADDR' (contains 'qubic'). This is NOT a Monero node."(set_color normal) >&2
|
|
return 1
|
|
end
|
|
|
|
# Try up to 3 short attempts
|
|
for t in (seq 1 3)
|
|
set ok 0
|
|
set raw (curl -s --max-time 7 "http://$ADDR/get_info" 2>/dev/null)
|
|
if test -n "$raw"
|
|
set mstatus (echo $raw | jq -r '.status // "OK"')
|
|
set testnet (echo $raw | jq -r '.testnet // false')
|
|
set stagenet (echo $raw | jq -r '.stagenet // false')
|
|
set height (echo $raw | jq -r '.height // .target_height // 0')
|
|
if test "$mstatus" = "OK"
|
|
set ok 1
|
|
end
|
|
end
|
|
|
|
if test $ok -eq 0
|
|
set raw (curl -s --max-time 7 -H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_info"}' "http://$ADDR/json_rpc" \
|
|
| jq -r '.result? | @json' 2>/dev/null)
|
|
if test -n "$raw"
|
|
set mstatus (echo $raw | jq -r '.status // "OK"')
|
|
set testnet (echo $raw | jq -r '.testnet // false')
|
|
set stagenet (echo $raw | jq -r '.stagenet // false')
|
|
set height (echo $raw | jq -r '.height // .target_height // 0')
|
|
if test "$mstatus" = "OK"
|
|
set ok 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if test $ok -eq 1
|
|
if test "$testnet" = "true" -o "$stagenet" = "true"
|
|
echo (set_color red)"Refusing non-mainnet daemon (testnet=$testnet stagenet=$stagenet) at $ADDR."(set_color normal) >&2
|
|
return 1
|
|
end
|
|
echo "Daemon OK: Monero mainnet, height=$height"
|
|
return 0
|
|
end
|
|
sleep 1
|
|
end
|
|
|
|
echo (set_color red)"Could not speak Monero /get_info to $ADDR. Not a monerod (or blocked)."(set_color normal) >&2
|
|
echo "Tip: run your own monerod or set WALLETSVC_SKIP_DAEMON_CHECK=1 to bypass." >&2
|
|
return 1
|
|
end
|
|
|
|
# Wait for wallet RPC to accept connections (digest auth get_version)
|
|
function walletsvc_wait_ready -a NS SVC SECS
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$SECS"; set SECS 60; end
|
|
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d 2>/dev/null)
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d 2>/dev/null)
|
|
if test -z "$RPCUSER" -o -z "$RPCPASS"
|
|
echo (set_color red)"Error:"(set_color normal)" missing RPC creds in secret {$SVC}-rpc-auth" >&2
|
|
return 1
|
|
end
|
|
|
|
_pf_start $NS $SVC 18083 18083
|
|
set tries (math "ceil($SECS/2)")
|
|
for i in (seq 1 $tries)
|
|
set code (curl -s -o /dev/null -w "%{http_code}" --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
if test "$code" = "200"
|
|
_pf_stop
|
|
return 0
|
|
end
|
|
sleep 2
|
|
end
|
|
_pf_stop
|
|
echo (set_color red)"RPC not ready after $SECS seconds."(set_color normal) >&2
|
|
return 1
|
|
end
|
|
|
|
### ------- main workflow ---------------------------------------------------
|
|
|
|
function walletsvc_bootstrap --description "Interactive setup of monero-wallet-rpc with PVC+secrets; creates default wallet and prints recovery info"
|
|
_need kubectl jq curl awk; or return 1
|
|
|
|
_banner "Monero wallet RPC bootstrap"
|
|
|
|
# --- Namespace
|
|
read -P "Namespace [crypto]: " ns_raw
|
|
if test -z "$ns_raw"; set ns_raw crypto; end
|
|
set ns (_k8s_name $ns_raw)
|
|
_require "Namespace" $ns; or return 1
|
|
|
|
echo "Ensuring namespace '$ns' exists…"
|
|
if kubectl get ns $ns >/dev/null 2>&1
|
|
echo "Namespace $ns already exists."
|
|
else
|
|
kubectl create ns $ns; or return 1
|
|
echo "Created namespace $ns."
|
|
end
|
|
|
|
# --- StorageClass
|
|
kubectl get storageclass
|
|
echo "TIP: Prefer a class with reclaimPolicy=Retain so a PVC delete won't delete the PV."
|
|
set sc (_choose_sc); or return 1
|
|
|
|
# --- Unified name (prefix wallet-monero-)
|
|
read -P "Wallet name (no spaces, e.g. 'brad', base will be wallet-monero-brad): " wallet_raw
|
|
_require "Wallet name" $wallet_raw; or return 1
|
|
if string match -rq '\s' -- $wallet_raw
|
|
echo (set_color red)"Error:"(set_color normal)" Wallet name cannot contain spaces." >&2
|
|
return 1
|
|
end
|
|
set base (_k8s_name $wallet_raw)
|
|
set base (string replace -r '^wallet-monero-' '' -- $base)
|
|
set name "wallet-monero-$base"
|
|
|
|
# Bind all k8s object names to the unified base
|
|
set pvc_name $name
|
|
set app_name $name
|
|
set svc_name $name
|
|
set rpc_secret_name "$app_name-rpc-auth"
|
|
set wpass_secret_name "$app_name-wallet-pass"
|
|
|
|
# --- Detect existing resources (all of them)
|
|
set exists_any 0
|
|
set found_list
|
|
for r in "deploy/$app_name" "svc/$svc_name" "netpol/{$app_name}-ingress"
|
|
if kubectl -n $ns get $r >/dev/null 2>&1
|
|
set -a found_list $r
|
|
set exists_any 1
|
|
end
|
|
end
|
|
if kubectl -n $ns get pvc $pvc_name >/dev/null 2>&1
|
|
set -a found_list "pvc/$pvc_name"
|
|
set exists_any 1
|
|
end
|
|
if kubectl -n $ns get secret $rpc_secret_name >/dev/null 2>&1
|
|
set -a found_list "secret/$rpc_secret_name"
|
|
set exists_any 1
|
|
end
|
|
if kubectl -n $ns get secret $wpass_secret_name >/dev/null 2>&1
|
|
set -a found_list "secret/$wpass_secret_name"
|
|
set exists_any 1
|
|
end
|
|
|
|
if test $exists_any -eq 1
|
|
_banner "Existing resources detected for $name"
|
|
for r in $found_list
|
|
echo " - $ns/$r"
|
|
end
|
|
read -P "Remove EVERYTHING (deploy/svc/netpol/PVC/SECRETS) and recreate? (y/N): " wipe
|
|
if string match -qi 'y*' -- $wipe
|
|
kubectl -n $ns delete deploy $app_name --ignore-not-found --wait=true >/dev/null 2>&1; or true
|
|
kubectl -n $ns delete svc $svc_name --ignore-not-found --wait=true >/dev/null 2>&1; or true
|
|
kubectl -n $ns delete netpol {$app_name}-ingress --ignore-not-found --wait=true >/dev/null 2>&1; or true
|
|
kubectl -n $ns delete pvc $pvc_name --ignore-not-found --wait=true >/dev/null 2>&1; or true
|
|
kubectl -n $ns delete secret $rpc_secret_name --ignore-not-found >/dev/null 2>&1; or true
|
|
kubectl -n $ns delete secret $wpass_secret_name --ignore-not-found >/dev/null 2>&1; or true
|
|
echo "Cleaned up all previous resources."
|
|
else
|
|
echo "Reusing existing resources; they will be applied in-place."
|
|
end
|
|
end
|
|
|
|
# --- Node scheduling (worker-only by default)
|
|
read -P "Schedule only on worker nodes? (y/N) [y]: " pin_workers
|
|
if test -z "$pin_workers"; set pin_workers y; end
|
|
set use_pin 0
|
|
if string match -qi 'y*' -- $pin_workers
|
|
set use_pin 1
|
|
read -P "Worker label key [node-role.kubernetes.io/worker]: " node_label_key
|
|
if test -z "$node_label_key"; set node_label_key node-role.kubernetes.io/worker; end
|
|
read -P "Worker label value [true]: " node_label_val
|
|
if test -z "$node_label_val"; set node_label_val true; end
|
|
end
|
|
|
|
# --- Daemon (auto-use local monerod)
|
|
echo
|
|
echo "Monero daemon: using your cluster-local monerod by default."
|
|
if test "$ns" = "crypto"
|
|
set daemon_addr monerod:18081
|
|
else
|
|
set daemon_addr monerod.crypto.svc.cluster.local:18081
|
|
end
|
|
echo "Daemon address (for the Deployment): $daemon_addr"
|
|
read -P "Override daemon address for this deployment? (host:port, blank to keep): " daemon_override
|
|
if test -n "$daemon_override"
|
|
set daemon_addr $daemon_override
|
|
end
|
|
|
|
if test -z "$WALLETSVC_SKIP_DAEMON_CHECK"
|
|
_banner "Probing daemon via temporary port-forward"
|
|
set PF_LOCAL 28081
|
|
_pf_start crypto monerod $PF_LOCAL 18081
|
|
if not _probe_monerod 127.0.0.1:$PF_LOCAL
|
|
_pf_stop
|
|
return 1
|
|
end
|
|
_pf_stop
|
|
else
|
|
echo "Skipping daemon probe due to WALLETSVC_SKIP_DAEMON_CHECK=1"
|
|
end
|
|
|
|
# Use your private image by default (in Zot)
|
|
read -P "Container image for wallet RPC [registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1]: " image
|
|
if test -z "$image"; set image registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1; end
|
|
_require "Container image" $image; or return 1
|
|
|
|
# --- Secrets (defaults: RPC user=wallet name, passwords auto if missing)
|
|
set existing_rpc_user (kubectl -n $ns get secret $rpc_secret_name -o jsonpath='{.data.username}' 2>/dev/null | base64 -d 2>/dev/null)
|
|
set existing_rpc_pass (kubectl -n $ns get secret $rpc_secret_name -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null)
|
|
set existing_wpass (kubectl -n $ns get secret $wpass_secret_name -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null)
|
|
|
|
set rpc_user_default $base
|
|
if test -n "$existing_rpc_user"
|
|
set rpc_user_default $existing_rpc_user
|
|
end
|
|
read -P "RPC username [$rpc_user_default]: " rpc_user
|
|
if test -z "$rpc_user"; set rpc_user $rpc_user_default; end
|
|
_require "RPC username" $rpc_user; or return 1
|
|
|
|
read -s -P "RPC password [leave blank to keep current or auto-generate if none]: " rpc_pass
|
|
echo
|
|
if test -z "$rpc_pass"
|
|
if test -n "$existing_rpc_pass"
|
|
set rpc_pass $existing_rpc_pass
|
|
else
|
|
set rpc_pass (_rand_alnum32)
|
|
end
|
|
end
|
|
|
|
read -P "Wallet filename (no path) [main]: " wallet_file_raw
|
|
if test -z "$wallet_file_raw"; set wallet_file_raw main; end
|
|
set wallet_file (_k8s_name $wallet_file_raw)
|
|
_require "Wallet filename" $wallet_file; or return 1
|
|
|
|
read -s -P "Wallet password [leave blank to keep current or auto-generate if none]: " wallet_pass
|
|
echo
|
|
if test -z "$wallet_pass"
|
|
if test -n "$existing_wpass"
|
|
set wallet_pass $existing_wpass
|
|
else
|
|
set wallet_pass (_rand_alnum32)
|
|
end
|
|
end
|
|
|
|
_banner "Summary"
|
|
echo " Namespace: $ns"
|
|
echo " StorageClass: $sc"
|
|
echo " Base name: $name"
|
|
echo " PVC: $pvc_name"
|
|
echo " App/Deployment: $app_name"
|
|
echo " Service: $svc_name"
|
|
echo " Daemon address: $daemon_addr"
|
|
echo " Image: $image"
|
|
echo " RPC user: $rpc_user"
|
|
echo " Wallet file: /data/$wallet_file"
|
|
if test $use_pin -eq 1
|
|
echo " NodeSelector: $node_label_key=$node_label_val"
|
|
end
|
|
read -P "Proceed? [y/N]: " proceed
|
|
if not string match -qi 'y*' -- $proceed
|
|
echo (set_color yellow)"Aborted by user at confirmation step."(set_color normal)
|
|
return 1
|
|
end
|
|
|
|
_banner "Applying secrets"
|
|
kubectl -n $ns create secret generic $rpc_secret_name \
|
|
--from-literal=username="$rpc_user" \
|
|
--from-literal=password="$rpc_pass" \
|
|
--dry-run=client -o yaml | kubectl -n $ns apply -f -; or return 1
|
|
|
|
kubectl -n $ns create secret generic $wpass_secret_name \
|
|
--from-literal=password="$wallet_pass" \
|
|
--dry-run=client -o yaml | kubectl -n $ns apply -f -; or return 1
|
|
|
|
set ts (date -Is)
|
|
kubectl -n $ns annotate secret $rpc_secret_name walletsvc.titan/update-ts="$ts" --overwrite >/dev/null 2>&1
|
|
kubectl -n $ns annotate secret $wpass_secret_name walletsvc.titan/update-ts="$ts" --overwrite >/dev/null 2>&1
|
|
|
|
echo "RPC creds: user='$rpc_user' pass='$rpc_pass'"
|
|
echo "Wallet pass: $wallet_pass"
|
|
|
|
_banner "Applying PVC"
|
|
if kubectl -n $ns get pvc $pvc_name >/dev/null 2>&1
|
|
echo "PVC $ns/$pvc_name exists; keeping as-is."
|
|
else
|
|
set pvcfile (mktemp -t {$pvc_name}-pvc.XXXX.yaml)
|
|
begin
|
|
echo "apiVersion: v1"
|
|
echo "kind: PersistentVolumeClaim"
|
|
echo "metadata:"
|
|
echo " name: $pvc_name"
|
|
echo " namespace: $ns"
|
|
echo "spec:"
|
|
echo " accessModes: [\"ReadWriteOnce\"]"
|
|
echo " storageClassName: $sc"
|
|
echo " resources:"
|
|
echo " requests:"
|
|
echo " storage: 5Gi"
|
|
end > $pvcfile
|
|
echo "--- PVC manifest ($pvcfile) ---"
|
|
cat $pvcfile
|
|
kubectl apply -f $pvcfile; or return 1
|
|
end
|
|
|
|
_banner "Applying Deployment"
|
|
set dply (mktemp -t {$app_name}-deploy.XXXX.yaml)
|
|
begin
|
|
echo "apiVersion: apps/v1"
|
|
echo "kind: Deployment"
|
|
echo "metadata:"
|
|
echo " name: $app_name"
|
|
echo " namespace: $ns"
|
|
echo " labels: { app: $app_name }"
|
|
echo "spec:"
|
|
echo " replicas: 1"
|
|
echo " strategy:"
|
|
echo " type: Recreate"
|
|
echo " selector:"
|
|
echo " matchLabels: { app: $app_name }"
|
|
echo " template:"
|
|
echo " metadata:"
|
|
echo " labels: { app: $app_name }"
|
|
echo " spec:"
|
|
if test $use_pin -eq 1
|
|
echo " nodeSelector:"
|
|
echo " $node_label_key: \"$node_label_val\""
|
|
end
|
|
echo " securityContext:"
|
|
echo " fsGroup: 1000"
|
|
echo " fsGroupChangePolicy: OnRootMismatch"
|
|
echo " initContainers:"
|
|
echo " - name: volume-permissions"
|
|
echo " image: busybox:1.36"
|
|
echo " command: [\"/bin/sh\",\"-lc\",\"chown :1000 /data && chmod 0770 /data\"]"
|
|
echo " securityContext:"
|
|
echo " runAsUser: 0"
|
|
echo " volumeMounts:"
|
|
echo " - name: data"
|
|
echo " mountPath: /data"
|
|
echo " containers:"
|
|
echo " - name: wallet-rpc"
|
|
echo " image: $image"
|
|
echo " imagePullPolicy: Always"
|
|
echo " command: [\"/bin/sh\",\"-lc\"]"
|
|
echo " args:"
|
|
echo " - |"
|
|
echo " RPCU=\$(cat /run/monero-secrets/rpc-user);"
|
|
echo " RPCP=\$(cat /run/monero-secrets/rpc-pass);"
|
|
echo " exec monero-wallet-rpc \\"
|
|
echo " --wallet-dir /data \\"
|
|
echo " --daemon-address $daemon_addr \\"
|
|
echo " --rpc-bind-ip 0.0.0.0 --rpc-bind-port 18083 \\"
|
|
echo " --rpc-login \"\${RPCU}:\${RPCP}\" \\"
|
|
echo " --confirm-external-bind"
|
|
echo " ports:"
|
|
echo " - containerPort: 18083"
|
|
echo " volumeMounts:"
|
|
echo " - name: data"
|
|
echo " mountPath: /data"
|
|
echo " - name: rpc-auth"
|
|
echo " mountPath: /run/monero-secrets"
|
|
echo " readOnly: true"
|
|
echo " resources:"
|
|
echo " requests: { cpu: \"100m\", memory: \"128Mi\" }"
|
|
echo " limits: { cpu: \"1\", memory: \"512Mi\" }"
|
|
echo " volumes:"
|
|
echo " - name: data"
|
|
echo " persistentVolumeClaim: { claimName: $pvc_name }"
|
|
echo " - name: rpc-auth"
|
|
echo " secret:"
|
|
echo " secretName: $rpc_secret_name"
|
|
echo " items:"
|
|
echo " - key: username"
|
|
echo " path: rpc-user"
|
|
echo " - key: password"
|
|
echo " path: rpc-pass"
|
|
end > $dply
|
|
echo "--- Deployment manifest ($dply) ---"
|
|
head -n 70 $dply
|
|
echo "..."
|
|
kubectl apply -f $dply; or return 1
|
|
|
|
_banner "Applying Service"
|
|
set svc (mktemp -t {$app_name}-svc.XXXX.yaml)
|
|
begin
|
|
echo "apiVersion: v1"
|
|
echo "kind: Service"
|
|
echo "metadata:"
|
|
echo " name: $svc_name"
|
|
echo " namespace: $ns"
|
|
echo " labels: { app: $app_name }"
|
|
echo "spec:"
|
|
echo " type: ClusterIP"
|
|
echo " selector: { app: $app_name }"
|
|
echo " ports:"
|
|
echo " - name: rpc"
|
|
echo " port: 18083"
|
|
echo " targetPort: 18083"
|
|
end > $svc
|
|
echo "--- Service manifest ($svc) ---"
|
|
cat $svc
|
|
kubectl apply -f $svc; or return 1
|
|
|
|
_banner "Applying NetworkPolicy (same-namespace-only)"
|
|
set np (mktemp -t {$app_name}-netpol.XXXX.yaml)
|
|
begin
|
|
echo "apiVersion: networking.k8s.io/v1"
|
|
echo "kind: NetworkPolicy"
|
|
echo "metadata:"
|
|
printf " name: %s-ingress\n" $app_name
|
|
echo " namespace: $ns"
|
|
echo "spec:"
|
|
echo " podSelector: { matchLabels: { app: $app_name } }"
|
|
echo " policyTypes: [\"Ingress\"]"
|
|
echo " ingress:"
|
|
echo " - from:"
|
|
echo " - namespaceSelector:"
|
|
echo " matchLabels:"
|
|
echo " kubernetes.io/metadata.name: $ns"
|
|
echo " ports:"
|
|
echo " - port: 18083"
|
|
echo " protocol: TCP"
|
|
end > $np
|
|
echo "--- NetworkPolicy manifest ($np) ---"
|
|
cat $np
|
|
kubectl apply -f $np; or return 1
|
|
|
|
_banner "Ensuring pod picks up latest secrets"
|
|
kubectl -n $ns rollout restart deploy/$app_name >/dev/null 2>&1; or true
|
|
|
|
_banner "Waiting for rollout"
|
|
if not kubectl -n $ns rollout status deploy/$app_name --timeout=180s
|
|
echo (set_color red)"Rollout did not finish in time. Recent events:"(set_color normal) >&2
|
|
kubectl -n $ns get events --sort-by=.lastTimestamp | tail -n 40
|
|
echo (set_color yellow)"Hint:"(set_color normal)" if you see Multi-Attach for the PVC, delete any older pod."
|
|
return 1
|
|
end
|
|
|
|
_banner "Waiting for wallet RPC readiness"
|
|
walletsvc_wait_ready $ns $svc_name 60; or return 1
|
|
|
|
_banner "Creating/opening wallet via JSON-RPC"
|
|
if not walletsvc_check_write $ns $app_name >/dev/null
|
|
echo (set_color red)"PVC is not writable; aborting."(set_color normal)
|
|
return 1
|
|
end
|
|
|
|
# Always attempt create; if it exists, open it.
|
|
if not walletsvc_create_wallet $ns $svc_name $wallet_file $wallet_pass
|
|
echo (set_color yellow)"create_wallet returned non-zero; attempting open anyway…"(set_color normal)
|
|
end
|
|
if not walletsvc_open $ns $svc_name $wallet_file $wallet_pass
|
|
echo (set_color red)"Failed to open wallet $wallet_file. If this wallet already exists, the on-disk password may differ from the Secret. Use walletsvc_open with the correct password or walletsvc_change_wallet_password."(set_color normal)
|
|
return 1
|
|
end
|
|
|
|
# --- Gather summary data (address, seed if enabled)
|
|
set PRIMARY (walletsvc_primary_address $ns $svc_name 2>/dev/null)
|
|
set NEWADDR (walletsvc_new_address $ns $svc_name "bootstrap-deposit" 0 2>/dev/null | awk '/New address:/{print $3}' )
|
|
set SEEDRAW (walletsvc_show_seed $ns $svc_name 2>&1)
|
|
set SEED (echo $SEEDRAW | awk '/Mnemonic seed:/,0{if($0!~/Mnemonic seed:/)print}' )
|
|
|
|
_banner "Done"
|
|
echo "Namespace: $ns"
|
|
echo "Base name: $name"
|
|
echo "Service (in-cluster): $svc_name:18083"
|
|
echo "PVC: $pvc_name via StorageClass '$sc'"
|
|
echo "RPC auth: user='$rpc_user'"
|
|
echo "RPC pass: $rpc_pass"
|
|
echo "Wallet pass: $wallet_pass"
|
|
echo "Wallet file: /data/$wallet_file"
|
|
echo "Primary address (acct 0):"
|
|
echo " $PRIMARY"
|
|
if test -n "$NEWADDR"
|
|
echo "Fresh deposit subaddress (acct 0):"
|
|
echo " $NEWADDR"
|
|
end
|
|
if string match -qi '*Mnemonic seed:*' -- $SEEDRAW
|
|
echo (set_color yellow)"Mnemonic seed (WRITE THIS DOWN, OFFLINE):"(set_color normal)
|
|
echo $SEED
|
|
else
|
|
echo "Mnemonic seed not retrievable via RPC (image may disable it)."
|
|
echo "If needed and supported by your image: walletsvc_show_seed $ns $svc_name"
|
|
end
|
|
echo
|
|
echo "Access locally: walletsvc_portforward $ns $svc_name # then http://127.0.0.1:18083/json_rpc"
|
|
echo "Quick overview: walletsvc_overview $ns $svc_name"
|
|
end
|
|
|
|
|
|
|
|
### ------- utilities -------------------------------------------------------
|
|
|
|
function walletsvc_portforward --description "port-forward RPC to localhost:18083" -a NS SVC
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
echo "Forwarding $NS/$SVC → http://127.0.0.1:18083 (Ctrl+C to stop)…"
|
|
kubectl -n $NS port-forward svc/$SVC 18083:18083
|
|
end
|
|
|
|
function walletsvc_status --description "show pod/service status" -a NS APP
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
kubectl -n $NS get deploy,po,svc,netpol -l app=$APP -o wide
|
|
end
|
|
|
|
function walletsvc_stop --description "stop the RPC but keep PVC + secrets" -a NS APP SVC
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
if test -z "$SVC"; set SVC $APP; end
|
|
kubectl -n $NS delete deploy $APP --ignore-not-found
|
|
kubectl -n $NS delete svc $SVC --ignore-not-found
|
|
echo "Stopped $NS/$APP. PVC and secrets retained."
|
|
end
|
|
|
|
function walletsvc_purge --description "IRREVERSIBLY delete PVC + secrets (keys on disk!)" -a NS APP PVC SVC
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
if test -z "$PVC"; set PVC wallet-data; end
|
|
if test -z "$SVC"; set SVC $APP; end
|
|
echo "Type to confirm: PURGE $NS $PVC"
|
|
read CONFIRM
|
|
if test "$CONFIRM" != "PURGE $NS $PVC"
|
|
echo "Aborted."
|
|
return 1
|
|
end
|
|
kubectl -n $NS delete deploy $APP --ignore-not-found
|
|
kubectl -n $NS delete svc $SVC --ignore-not-found
|
|
kubectl -n $NS delete pvc $PVC --ignore-not-found --wait=true
|
|
kubectl -n $NS delete secret $APP-wallet-pass --ignore-not-found
|
|
kubectl -n $NS delete secret $APP-rpc-auth --ignore-not-found
|
|
echo "Purged. Ensure you have the mnemonic backed up if you ever need to restore."
|
|
end
|
|
|
|
function walletsvc_show_seed --description "Try to fetch mnemonic via RPC query_key (wallet must be open)" -a NS SVC RPCUSER RPCPASS
|
|
_need jq curl; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$RPCUSER"
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d)
|
|
end
|
|
if test -z "$RPCPASS"
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d)
|
|
end
|
|
_pf_start $NS $SVC 18083 18083
|
|
set res (_rpc_call $RPCUSER $RPCPASS query_key '{"key_type":"mnemonic"}')
|
|
_pf_stop
|
|
set err (echo $res | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
echo "query_key error: $err"
|
|
echo "Some images disable returning mnemonics via RPC. Consider backing up offline."
|
|
return 1
|
|
end
|
|
echo "Mnemonic seed:"
|
|
echo $res | jq -r '.result.key'
|
|
end
|
|
|
|
# ---------- core JSON-RPC wrapper (raw JSON to stdout) ----------
|
|
function walletsvc_rpc_call --description "RPC call with secrets (auto port-forward)" -a NS SVC METHOD PARAMS_JSON
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$METHOD"
|
|
echo "Usage: walletsvc_rpc_call [ns] [svc] <method> [params_json]" >&2
|
|
return 1
|
|
end
|
|
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d)
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d)
|
|
|
|
_pf_start $NS $SVC 18083 18083
|
|
if test -n "$PARAMS_JSON"
|
|
set payload (printf '{"jsonrpc":"2.0","id":"0","method":"%s","params":%s}' $METHOD "$PARAMS_JSON")
|
|
else
|
|
set payload (printf '{"jsonrpc":"2.0","id":"0","method":"%s"}' $METHOD)
|
|
end
|
|
|
|
set tmp (mktemp)
|
|
set code (curl -s --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-o $tmp -w "%{http_code}" \
|
|
-d "$payload" "http://127.0.0.1:18083/json_rpc")
|
|
set rc $status
|
|
_pf_stop
|
|
|
|
set body (cat $tmp); rm -f $tmp
|
|
|
|
if test $rc -ne 0
|
|
echo "RPC transport error ($METHOD): curl exit $rc" >&2
|
|
echo $body
|
|
return $rc
|
|
end
|
|
|
|
if test "$code" != "200"
|
|
echo "RPC HTTP $code ($METHOD)" >&2
|
|
echo $body
|
|
return 1
|
|
end
|
|
|
|
echo $body
|
|
return 0
|
|
end
|
|
|
|
function walletsvc_rpc_test --description "get_version using secrets" -a NS SVC
|
|
walletsvc_rpc_call $NS $SVC get_version | jq .
|
|
end
|
|
|
|
# Rotate RPC username/password (updates Secret & restarts Deploy)
|
|
function walletsvc_set_rpc_credentials --description "rotate RPC Basic creds" -a NS APP
|
|
_need kubectl; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
set SNAME {$APP}-rpc-auth
|
|
|
|
read -P "New RPC username [leave blank to keep current]: " NEWUSER
|
|
if test -z "$NEWUSER"
|
|
set NEWUSER (kubectl -n $NS get secret $SNAME -o jsonpath='{.data.username}' | base64 -d)
|
|
end
|
|
read -s -P "New RPC password [blank = auto-generate 32 alnum]: " NEWPASS
|
|
echo
|
|
if test -z "$NEWPASS"; set NEWPASS (_rand_alnum32); end
|
|
|
|
echo "Updating secret $NS/$SNAME and restarting $APP…"
|
|
kubectl -n $NS create secret generic $SNAME \
|
|
--from-literal=username="$NEWUSER" \
|
|
--from-literal=password="$NEWPASS" \
|
|
--dry-run=client -o yaml | kubectl -n $NS apply -f -; or return 1
|
|
kubectl -n $NS annotate secret $SNAME walletsvc.titan/update-ts=(date -Is) --overwrite >/dev/null 2>&1
|
|
kubectl -n $NS rollout restart deploy/$APP
|
|
kubectl -n $NS rollout status deploy/$APP --timeout=180s; or return 1
|
|
|
|
echo "New RPC creds:"
|
|
echo " user: $NEWUSER"
|
|
echo " pass: $NEWPASS"
|
|
end
|
|
|
|
# Change wallet file password via RPC
|
|
function walletsvc_change_wallet_password --description "change password of a wallet file via RPC" -a NS SVC
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
|
|
read -P "Wallet filename (under /data): " WFILE
|
|
if test -z "$WFILE"
|
|
echo "Wallet filename required." >&2
|
|
return 1
|
|
end
|
|
read -s -P "Current wallet password: " OLD
|
|
echo
|
|
read -s -P "New wallet password [blank = auto-generate 32 alnum]: " NEW
|
|
echo
|
|
if test -z "$NEW"; set NEW (_rand_alnum32); end
|
|
|
|
set res (walletsvc_rpc_call $NS $SVC open_wallet (printf '{"filename":"%s","password":"%s"}' $WFILE $OLD))
|
|
walletsvc_rpc_call $NS $SVC change_wallet_password (printf '{"old_password":"%s","new_password":"%s"}' $OLD $NEW)
|
|
echo "New wallet password: $NEW"
|
|
end
|
|
|
|
# --- logs from the deployment (quick tail) ---
|
|
function walletsvc_logs -a NS APP LINES
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
if test -z "$LINES"; set LINES 200; end
|
|
kubectl -n $NS logs deploy/$APP --tail=$LINES
|
|
end
|
|
|
|
# --- list files in /data inside the pod ---
|
|
function walletsvc_wallet_ls -a NS APP
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
echo "Listing /data in $NS/$APP…" >&2
|
|
kubectl -n $NS exec deploy/$APP -- sh -lc 'ls -la /data || true'
|
|
end
|
|
|
|
# --- create a wallet (uses Secret password if PASS omitted) ---
|
|
function walletsvc_create_wallet -a NS SVC FILE PASS LANG
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$FILE"
|
|
echo "Usage: walletsvc_create_wallet [ns] [svc] <filename> [pass] [language]" >&2
|
|
return 1
|
|
end
|
|
if test -z "$PASS"
|
|
set PASS (kubectl -n $NS get secret {$SVC}-wallet-pass -o jsonpath='{.data.password}' | base64 -d 2>/dev/null)
|
|
end
|
|
if test -z "$LANG"; set LANG English; end
|
|
|
|
set payload (printf '{"filename":"%s","password":"%s","language":"%s"}' $FILE $PASS $LANG)
|
|
set res (walletsvc_rpc_call $NS $SVC create_wallet $payload)
|
|
set rc $status
|
|
if test $rc -ne 0
|
|
echo (set_color red)"create_wallet RPC transport failed (rc=$rc)."(set_color normal) >&2
|
|
return $rc
|
|
end
|
|
|
|
set err (echo $res | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
if string match -q "*already exists*" -- $err
|
|
echo "Wallet $FILE already exists."
|
|
return 0
|
|
end
|
|
echo "create_wallet error: $err" >&2
|
|
return 1
|
|
end
|
|
|
|
echo "Created wallet: $FILE"
|
|
end
|
|
|
|
# --- improved opener: closes first, checks files, hints if password mismatch ---
|
|
function walletsvc_open -a NS SVC FILE PASS
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$FILE"
|
|
echo "Usage: walletsvc_open [ns] [svc] <filename> [password]" >&2
|
|
return 1
|
|
end
|
|
if test -z "$PASS"
|
|
set PASS (kubectl -n $NS get secret {$SVC}-wallet-pass -o jsonpath='{.data.password}' | base64 -d 2>/dev/null)
|
|
end
|
|
if test -z "$PASS"
|
|
read -s -P "Wallet password for $FILE: " PASS; echo
|
|
end
|
|
|
|
walletsvc_rpc_call $NS $SVC close_wallet >/dev/null 2>&1; or true
|
|
|
|
set res (walletsvc_rpc_call $NS $SVC open_wallet (printf '{"filename":"%s","password":"%s"}' $FILE $PASS))
|
|
set rc $status
|
|
if test $rc -ne 0
|
|
echo (set_color red)"open_wallet RPC transport failed (rc=$rc)."(set_color normal) >&2
|
|
return $rc
|
|
end
|
|
|
|
set exists (kubectl -n $NS exec deploy/$SVC -- sh -lc (printf 'test -e /data/%s -o -e /data/%s.keys && echo yes || echo no' $FILE $FILE) 2>/dev/null)
|
|
if test "$exists" = "no"
|
|
echo "No wallet files at /data/$FILE{,.keys}. If this should be a new wallet, run:" >&2
|
|
echo " walletsvc_create_wallet $NS $SVC $FILE" >&2
|
|
return 1
|
|
end
|
|
|
|
set tmp (mktemp)
|
|
walletsvc_rpc_call $NS $SVC open_wallet (printf '{"filename":"%s","password":"%s"}' $FILE $PASS) > $tmp
|
|
set rc $status
|
|
set res (cat $tmp); rm -f $tmp
|
|
|
|
if test $rc -ne 0
|
|
echo "open_wallet transport/HTTP error (rc=$rc)" >&2
|
|
echo $res | jq . >&2
|
|
return $rc
|
|
end
|
|
|
|
set err (echo $res | jq -r '.error.message // empty' 2>/dev/null)
|
|
if test -n "$err"
|
|
echo "open_wallet error: $err" >&2
|
|
if string match -q "*password*" -- (string lower -- $err)
|
|
echo "Hint: Secret {$SVC}-wallet-pass may not match the files on disk. Try supplying the pass explicitly." >&2
|
|
end
|
|
return 1
|
|
end
|
|
|
|
echo $res | jq .
|
|
end
|
|
|
|
function walletsvc_check_write -a NS APP
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$APP"; set APP wallet-rpc; end
|
|
kubectl -n $NS exec deploy/$APP -- sh -lc 'echo test-$(date +%s) > /data/.write-test && ls -l /data/.write-test && rm -f /data/.write-test'
|
|
end
|
|
|
|
# --- ensure wallet is open (create if missing) ---
|
|
function walletsvc_ensure_open -a NS SVC FILE
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$FILE"; set FILE main; end
|
|
|
|
set exists (kubectl -n $NS exec deploy/$SVC -- sh -lc (printf 'if [ -e "/data/%s" ] || [ -e "/data/%s.keys" ]; then echo yes; else echo no; fi' $FILE $FILE) 2>/dev/null)
|
|
if test "$exists" = "no"
|
|
echo "No /data/$FILE found. Creating…" >&2
|
|
walletsvc_create_wallet $NS $SVC $FILE; or return 1
|
|
end
|
|
|
|
walletsvc_open $NS $SVC $FILE; or return 1
|
|
end
|
|
|
|
# --- quick primary address print (account 0)
|
|
function walletsvc_primary_address -a NS SVC
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
_pf_start $NS $SVC 18083 18083
|
|
set resp (curl -s --fail --digest \
|
|
-u (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d):(kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d) \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_address","params":{"account_index":0}}' \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
_pf_stop
|
|
echo $resp | jq -r '.result.address'
|
|
end
|
|
|
|
# --- show unlocked balance quickly
|
|
function walletsvc_unlocked -a NS SVC
|
|
_need python3; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
set res (walletsvc_rpc_call $NS $SVC get_balance '{"account_index":0}')
|
|
set bal (echo $res | jq -r '.result.balance // 0')
|
|
set unl (echo $res | jq -r '.result.unlocked_balance // 0')
|
|
|
|
# Convert piconero -> XMR using Python (fish-safe)
|
|
set balx (python3 -c 'from decimal import Decimal; import sys; print(Decimal(sys.argv[1]) / Decimal("1e12"))' -- $bal)
|
|
set unlx (python3 -c 'from decimal import Decimal; import sys; print(Decimal(sys.argv[1]) / Decimal("1e12"))' -- $unl)
|
|
|
|
echo "balance=$balx XMR unlocked=$unlx XMR"
|
|
end
|
|
|
|
# --- wait until unlocked balance >= threshold (XMR), timeout seconds
|
|
function walletsvc_wait_unlocked -a NS SVC XMR_MIN TIMEOUT
|
|
_need python3; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$XMR_MIN"; set XMR_MIN 0; end
|
|
if test -z "$TIMEOUT"; set TIMEOUT 300; end
|
|
set start (date +%s)
|
|
while true
|
|
set res (walletsvc_rpc_call $NS $SVC get_balance '{"account_index":0}')
|
|
set unl (echo $res | jq -r '.result.unlocked_balance // 0')
|
|
set unlx (python3 -c 'from decimal import Decimal; import sys; print(Decimal(sys.argv[1]) / Decimal("1e12"))' -- $unl)
|
|
set ge (python3 -c 'from decimal import Decimal; import sys; print(1 if Decimal(sys.argv[1]) >= Decimal(sys.argv[2]) else 0)' -- $unlx $XMR_MIN)
|
|
if test $ge -eq 1
|
|
echo "Unlocked: $unlx XMR (≥ $XMR_MIN)"
|
|
return 0
|
|
end
|
|
if test (math (date +%s) - $start) -ge $TIMEOUT
|
|
echo "Timed out waiting for unlocked balance ≥ $XMR_MIN XMR (current $unlx XMR)."
|
|
return 1
|
|
end
|
|
sleep 6
|
|
end
|
|
end
|
|
|
|
# ---------- friendly overview ----------
|
|
function walletsvc_overview -a NS SVC ACCOUNT
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$ACCOUNT"; set ACCOUNT 0; end
|
|
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d)
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d)
|
|
|
|
_pf_start $NS $SVC 18083 18083
|
|
echo "== Wallet RPC overview (ns=$NS svc=$SVC acct=$ACCOUNT)"
|
|
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
echo $resp | jq -r '"-- Version:\n version=\(.result.version) release=\(.result.release)"'
|
|
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_height"}' \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
echo "-- Height:"
|
|
if test -n "$err"
|
|
echo " ERROR: $err"
|
|
echo " (Tip: open your wallet: walletsvc_open $NS $SVC main)"
|
|
else
|
|
echo $resp | jq -r '" wallet_height=\(.result.height)"'
|
|
end
|
|
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":"0","method":"get_accounts"}' \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
echo "-- Accounts:"
|
|
if test -n "$err"
|
|
echo " ERROR: $err"
|
|
else
|
|
echo $resp | jq -r '.result.subaddress_accounts[]
|
|
| " [\(.account_index)] \(.label) total=\(.balance/1000000000000) XMR unlocked=\(.unlocked_balance/1000000000000) XMR"'
|
|
end
|
|
|
|
set BODY (printf '{"account_index":%s}' $ACCOUNT)
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_balance","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
echo "-- Account balance:"
|
|
if test -n "$err"
|
|
echo " ERROR: $err"
|
|
else
|
|
echo $resp | jq -r '" balance=\(.result.balance/1000000000000) XMR unlocked=\(.result.unlocked_balance/1000000000000) XMR"'
|
|
end
|
|
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_address","params":{"account_index":%s}}' $ACCOUNT) \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
echo "-- Primary address (account base):"
|
|
if test -n "$err"
|
|
echo " ERROR: $err"
|
|
else
|
|
echo $resp | jq -r '.result.address'
|
|
end
|
|
|
|
echo "-- First 5 subaddresses:"
|
|
if test -n "$err"
|
|
echo " (skipped due to error above)"
|
|
else
|
|
echo $resp | jq -r '.result.addresses[:5][] | " [\(.address_index)] \(.address) label=\(.label) used=\(.used)"'
|
|
end
|
|
|
|
set BODY (printf '{"account_index":%s,"in":true}' $ACCOUNT)
|
|
set in (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_transfers","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
echo "-- Recent transfers (incoming, last 5):"
|
|
echo $in | jq -r '.result.in // [] | (.[-5:] // [])[] | " + \(.amount/1000000000000) XMR conf=\(.confirmations) tx=\(.txid)"'
|
|
|
|
set BODY (printf '{"account_index":%s,"out":true}' $ACCOUNT)
|
|
set out (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_transfers","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
echo "-- Recent transfers (outgoing, last 5):"
|
|
echo $out | jq -r '.result.out // [] | (.[-5:] // [])[] | " - \(.amount/1000000000000) XMR conf=\(.confirmations) tx=\(.txid)"'
|
|
|
|
set BODY (printf '{"account_index":%s,"pool":true}' $ACCOUNT)
|
|
set pool (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_transfers","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
echo "-- Mempool (unconfirmed incoming):"
|
|
echo $pool | jq -r '.result.pool // [] | .[] | " ~ \(.amount/1000000000000) XMR tx=\(.txid)"'
|
|
|
|
_pf_stop
|
|
end
|
|
|
|
# ---------- list addresses ----------
|
|
function walletsvc_list_addresses -a NS SVC ACCOUNT
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$ACCOUNT"; set ACCOUNT 0; end
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d)
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d)
|
|
_pf_start $NS $SVC 18083 18083
|
|
set BODY (printf '{"account_index":%s}' $ACCOUNT)
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"get_address","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
_pf_stop
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
echo "ERROR: $err" >&2
|
|
return 1
|
|
end
|
|
echo $resp | jq -r '.result.addresses[] | "[\(.address_index)] \(.address) label=\(.label) used=\(.used)"'
|
|
end
|
|
|
|
# ---------- new subaddress ----------
|
|
function walletsvc_new_address -a NS SVC LABEL ACCOUNT
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$ACCOUNT"; set ACCOUNT 0; end
|
|
if test -z "$LABEL"; set LABEL (printf "deposit-%s" (date +%Y%m%d-%H%M%S)); end
|
|
set RPCUSER (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.username}' | base64 -d)
|
|
set RPCPASS (kubectl -n $NS get secret {$SVC}-rpc-auth -o jsonpath='{.data.password}' | base64 -d)
|
|
_pf_start $NS $SVC 18083 18083
|
|
set BODY (printf '{"account_index":%s,"label":"%s"}' $ACCOUNT $LABEL)
|
|
set resp (curl -s --fail --digest -u "$RPCUSER:$RPCPASS" \
|
|
-H 'Content-Type: application/json' \
|
|
-d (printf '{"jsonrpc":"2.0","id":"0","method":"create_address","params":%s}' "$BODY") \
|
|
http://127.0.0.1:18083/json_rpc)
|
|
_pf_stop
|
|
set err (echo $resp | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
echo "ERROR: $err" >&2
|
|
return 1
|
|
end
|
|
echo $resp | jq -r --arg L "$LABEL" '"New address: \(.result.address) [index=\(.result.address_index)] label=" + $L'
|
|
end
|
|
|
|
# --- send a specific amount XMR to a single address (optionally payment_id)
|
|
# Usage: walletsvc_send [ns] [svc] <to_address> <amount_xmr> [payment_id_hex]
|
|
function walletsvc_send -a NS SVC TO AMT_XMR PID
|
|
_need jq curl python3; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$TO" -o -z "$AMT_XMR"
|
|
echo "Usage: walletsvc_send [ns] [svc] <to_address> <amount_xmr> [payment_id_hex]" >&2
|
|
return 1
|
|
end
|
|
|
|
# Convert amount to atomic units
|
|
set ATOMIC (_xmr_to_atomic $AMT_XMR)
|
|
if test -z "$ATOMIC"
|
|
echo "Failed to convert amount '$AMT_XMR' to piconero." >&2
|
|
return 1
|
|
end
|
|
|
|
# Build JSON
|
|
set body_base (printf '{"destinations":[{"amount":%s,"address":"%s"}],"account_index":0,"priority":1,"get_tx_key":true,"do_not_relay":false}' $ATOMIC $TO)
|
|
if test -n "$PID"
|
|
set body (string replace '}' ',"payment_id":"'$PID'"}' -- $body_base)
|
|
else
|
|
set body $body_base
|
|
end
|
|
|
|
set res (walletsvc_rpc_call $NS $SVC transfer $body)
|
|
set err (echo $res | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
echo "ERROR: $err" >&2
|
|
return 1
|
|
end
|
|
|
|
set fee (echo $res | jq -r '.result.fee // 0')
|
|
set tx (echo $res | jq -r '.result.tx_hash // (.result.tx_hash_list[0] // "")')
|
|
|
|
# piconero -> XMR without heredoc
|
|
set fee_xmr (python3 -c 'from decimal import Decimal; import sys; print(Decimal(sys.argv[1]) / Decimal("1e12"))' -- $fee)
|
|
|
|
echo "Sent transaction:"
|
|
echo " tx: $tx"
|
|
echo " fee: $fee_xmr XMR ($fee piconero)"
|
|
end
|
|
|
|
# --- sweep all unlocked funds to an address (account 0)
|
|
# Usage: walletsvc_sweep_all [ns] [svc] <to_address>
|
|
function walletsvc_sweep_all -a NS SVC TO
|
|
_need jq curl; or return 1
|
|
if test -z "$NS"; set NS crypto; end
|
|
if test -z "$SVC"; set SVC wallet-rpc; end
|
|
if test -z "$TO"
|
|
echo "Usage: walletsvc_sweep_all [ns] [svc] <to_address>" >&2
|
|
return 1
|
|
end
|
|
|
|
set body (printf '{"address":"%s","account_index":0,"priority":1,"do_not_relay":false,"get_tx_keys":true}' $TO)
|
|
set res (walletsvc_rpc_call $NS $SVC sweep_all $body)
|
|
set err (echo $res | jq -r '.error.message // empty')
|
|
if test -n "$err"
|
|
echo "ERROR: $err" >&2
|
|
return 1
|
|
end
|
|
|
|
echo "Sweep transactions:"
|
|
echo $res | jq -r '.result.tx_hash_list[]? | " tx: " + .'
|
|
end
|
|
|
|
# Optional: non-sensitive cluster snapshot to help tune manifests.
|
|
function cluster_probe
|
|
set OUT cluster-snapshot.txt
|
|
printf "# Cluster snapshot %s\n" (date -Is) > $OUT
|
|
|
|
printf "\n## kubectl version\n" >> $OUT
|
|
kubectl version --short >> $OUT 2>&1
|
|
|
|
printf "\n## Nodes\n" >> $OUT
|
|
kubectl get nodes -o wide >> $OUT
|
|
|
|
printf "\n### Node resources\n" >> $OUT
|
|
kubectl describe nodes | egrep -i 'Name:|Roles:|Capacity:|Allocatable:|cpu|memory|ephemeral' >> $OUT
|
|
|
|
printf "\n## Namespaces\n" >> $OUT
|
|
kubectl get ns >> $OUT
|
|
|
|
printf "\n## StorageClasses (summary)\n" >> $OUT
|
|
kubectl get storageclass >> $OUT
|
|
printf "\n## StorageClasses (yaml)\n" >> $OUT
|
|
kubectl get storageclass -o yaml >> $OUT
|
|
|
|
printf "\n## PV/PVC\n" >> $OUT
|
|
kubectl get pv >> $OUT 2>/dev/null
|
|
kubectl get pvc -A >> $OUT 2>/dev/null
|
|
|
|
printf "\n## Ingress / Services\n" >> $OUT
|
|
kubectl get ingressclass >> $OUT 2>/dev/null
|
|
kubectl get ingress -A >> $OUT 2>/dev/null
|
|
kubectl get svc -A | egrep -i 'NAMESPACE|LoadBalancer|EXTERNAL-IP|traefik|ingress' >> $OUT
|
|
|
|
printf "\n## kube-system pods (CNI hint)\n" >> $OUT
|
|
kubectl -n kube-system get pods -o wide >> $OUT 2>/dev/null
|
|
|
|
printf "\n## Default limits/quotas\n" >> $OUT
|
|
kubectl -n default get limitrange,resourcequota >> $OUT 2>/dev/null
|
|
|
|
printf "\n## Tools present on your machine\n" >> $OUT
|
|
for t in kubectl helm jq curl awk envsubst fish
|
|
if type -q $t
|
|
printf "%s: OK\n" $t >> $OUT
|
|
else
|
|
printf "%s: MISSING\n" $t >> $OUT
|
|
end
|
|
end
|
|
|
|
echo "Wrote $OUT — paste the contents here."
|
|
end
|
|
|
|
# --- one-line command reference (keep in sync) ---
|
|
function walletsvc_help
|
|
echo "walletsvc_bootstrap # interactive deploy + secrets + PVC + first wallet (multiwallet RPC), idempotent"
|
|
echo "walletsvc_status [ns] [app] # show deploy, pods, service, netpol (default ns=crypto)"
|
|
echo "walletsvc_portforward [ns] [svc] # port-forward RPC to localhost:18083 (CTRL+C to stop)"
|
|
echo "walletsvc_logs [ns] [app] [N] # tail N (default 200) lines of RPC pod logs"
|
|
echo "walletsvc_wallet_ls [ns] [app] # list files under /data in the RPC pod (check wallet files)"
|
|
echo "walletsvc_rpc_test [ns] [svc] # JSON-RPC get_version (sanity check)"
|
|
echo "walletsvc_rpc_call [ns] [svc] <method> [params_json] # raw JSON (stdout) with digest auth"
|
|
echo "walletsvc_open [ns] [svc] <file> [pass] # close_wallet (ignore) -> open_wallet"
|
|
echo "walletsvc_create_wallet [ns] [svc] <file> [pass] [lang]# create wallet (defaults: pass from Secret, lang=English)"
|
|
echo "walletsvc_ensure_open [ns] [svc] [file] # create if missing, then open (uses Secret pass)"
|
|
echo "walletsvc_overview [ns] [svc] [acct=0] # version, height, balances, a few addresses, recent transfers"
|
|
echo "walletsvc_list_addresses [ns] [svc] [acct=0] # list all subaddresses for account"
|
|
echo "walletsvc_new_address [ns] [svc] [label] [acct=0] # create labeled subaddress"
|
|
echo "walletsvc_set_rpc_credentials [ns] [app] # rotate RPC basic auth (secret + rollout)"
|
|
echo "walletsvc_change_wallet_password [ns] [svc] # change wallet file password via RPC"
|
|
echo "walletsvc_stop [ns] [app] [svc] # delete deployment+service only"
|
|
echo "walletsvc_purge [ns] [app] [pvc] [svc] # delete deploy+svc+PVC+secrets (danger!)"
|
|
end
|
|
|
|
function walletsvc_help_detailed
|
|
echo "Commands (defaults: ns=crypto, svc/app=wallet-rpc)"
|
|
echo
|
|
echo "walletsvc_bootstrap"
|
|
echo " Interactive deploy: creates/updates secrets, PVC, Deployment (fsGroup+init perms), Service, NetPolicy."
|
|
echo " Idempotent: if deploy/svc/netpol already exist, you can clean them up (PVC+secrets kept) and continue."
|
|
echo " Uses your in-cluster monerod by default (monerod:18081 in ns=crypto)."
|
|
echo " Probes it via a temporary port-forward so it works from your workstation."
|
|
echo " Set WALLETSVC_SKIP_DAEMON_CHECK=1 to bypass the daemon probe (not recommended)."
|
|
end
|
|
|