386 lines
13 KiB
Bash
Executable File
386 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${REPO_ROOT}/lib/jeannie/core.sh"
|
|
source "${REPO_ROOT}/lib/jeannie/env.sh"
|
|
source "${REPO_ROOT}/lib/jeannie/toolchain.sh"
|
|
source "${REPO_ROOT}/lib/jeannie/pimox.sh"
|
|
BUILDX_CONFIG="/tmp/buildx-config.toml"
|
|
HOMELAB_STATE_DIR="${HOMELAB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/homelab}"
|
|
COSIGN_KEY_PREFIX="${COSIGN_KEY_PREFIX:-${HOMELAB_STATE_DIR}/cosign/cosign}"
|
|
COSIGN_KEY_PATH="${COSIGN_KEY_PATH:-${COSIGN_KEY_PREFIX}.key}"
|
|
COSIGN_PUBLIC_KEY_PATH="${COSIGN_PUBLIC_KEY_PATH:-${COSIGN_KEY_PREFIX}.pub}"
|
|
COSIGN_PASSWORD_FILE="${COSIGN_PASSWORD_FILE:-${HOMELAB_STATE_DIR}/cosign/cosign.password}"
|
|
COSIGN_VERSION="${COSIGN_VERSION:-2.6.3}"
|
|
COSIGN_BIN="${COSIGN_BIN:-}"
|
|
HOMELAB_COSIGN_PUBLIC_KEY_CONFIGMAP="${HOMELAB_COSIGN_PUBLIC_KEY_CONFIGMAP:-homelab-cosign-public-key}"
|
|
HOMELAB_SBOM_PREDICATE_TYPE="${HOMELAB_SBOM_PREDICATE_TYPE:-https://spdx.dev/Document}"
|
|
JEANNIE_LOG_DIR="${JEANNIE_LOG_DIR:-${HOMELAB_STATE_DIR}/logs}"
|
|
JEANNIE_LOG_FILE="${JEANNIE_LOG_FILE:-}"
|
|
JEANNIE_ERROR_LOG_FILE="${JEANNIE_ERROR_LOG_FILE:-}"
|
|
JEANNIE_STEP_INDEX=0
|
|
JEANNIE_STEP_TOTAL=0
|
|
|
|
# shellcheck disable=SC1091
|
|
source "${REPO_ROOT}/scripts/report-ui"
|
|
|
|
trap 'rm -f "${BUILDX_CONFIG}"' EXIT
|
|
|
|
load_homelab_inventory_defaults
|
|
|
|
export_homelab_inventory_tf_vars
|
|
|
|
KUBECONFIG_PATH="${KUBECONFIG_PATH:-${TF_VAR_kubeconfig_path:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}}"
|
|
|
|
adopt_platform_existing_resources() {
|
|
local stack="bootstrap/platform"
|
|
|
|
adopt_tofu_helm_release "${stack}" "helm_release.calico_crds" "tigera-operator" "calico-crds"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.calico" "tigera-operator" "calico"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.openebs" "openebs" "openebs"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.metallb[\"enabled\"]" "metallb-system" "metallb"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.traefik[\"enabled\"]" "traefik" "traefik"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.argocd" "argocd" "argocd"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.kyverno" "kyverno" "kyverno"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.kyverno_policies" "kyverno" "kyverno-policies"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.loki" "monitoring" "loki"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.promtail" "monitoring" "promtail"
|
|
adopt_tofu_helm_release "${stack}" "helm_release.prometheus_stack" "monitoring" "prometheus-stack"
|
|
adopt_tofu_kubernetes_resource \
|
|
"${stack}" \
|
|
"kubernetes_storage_class_v1.openebs_hostpath_retain" \
|
|
"" \
|
|
"storageclass" \
|
|
"openebs-hostpath-retain" \
|
|
"openebs-hostpath-retain"
|
|
adopt_tofu_kubernetes_resource \
|
|
"${stack}" \
|
|
"kubernetes_namespace_v1.monitoring" \
|
|
"" \
|
|
"namespace" \
|
|
"monitoring" \
|
|
"monitoring"
|
|
}
|
|
|
|
adopt_apps_existing_resources() {
|
|
local stack="bootstrap/apps"
|
|
local namespace="${TF_VAR_argocd_namespace:-argocd}"
|
|
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["container-registry"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"container-registry"
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["website-production"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"website-production"
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["demos-static"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"demos-static"
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["heimdall"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"heimdall"
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["n8n"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"n8n"
|
|
adopt_tofu_kubernetes_manifest \
|
|
"${stack}" \
|
|
'kubernetes_manifest.argocd_application["supply-chain-policy"]' \
|
|
"${namespace}" \
|
|
"applications.argoproj.io" \
|
|
"argoproj.io/v1alpha1" \
|
|
"Application" \
|
|
"supply-chain-policy"
|
|
}
|
|
|
|
ensure_homelab_node_labels() {
|
|
local control_plane_node="${LAB_CONTROL_PLANE_NODE_NAME:-debian}"
|
|
local raspberry_node="${LAB_RASPBERRY_NODE_NAME:-raspberry}"
|
|
local prometheus_selector="homelab.dev/node-role=app,homelab.dev/storage=ssd"
|
|
local node
|
|
local target_nodes
|
|
|
|
echo "Applying homelab labels to existing Kubernetes nodes..."
|
|
while IFS= read -r node; do
|
|
[[ -n "${node}" ]] || continue
|
|
|
|
if [[ "${node}" == "${control_plane_node}" ]]; then
|
|
kubectl --kubeconfig "${KUBECONFIG_PATH}" label node "${node}" \
|
|
homelab.dev/node-role=control-plane \
|
|
homelab.dev/storage=local \
|
|
homelab.dev/workload-class=control-plane \
|
|
--overwrite
|
|
continue
|
|
fi
|
|
|
|
kubectl --kubeconfig "${KUBECONFIG_PATH}" label node "${node}" \
|
|
node-role.kubernetes.io/worker=worker \
|
|
--overwrite
|
|
|
|
if [[ "${node}" == pimox-worker-* ]]; then
|
|
kubectl --kubeconfig "${KUBECONFIG_PATH}" label node "${node}" \
|
|
homelab.dev/node-role=app \
|
|
homelab.dev/storage=ssd \
|
|
homelab.dev/workload-class=platform \
|
|
--overwrite
|
|
elif [[ "${node}" == "${raspberry_node}" ]]; then
|
|
kubectl --kubeconfig "${KUBECONFIG_PATH}" label node "${node}" \
|
|
homelab.dev/node-role=edge-app \
|
|
homelab.dev/storage=local \
|
|
homelab.dev/workload-class=edge \
|
|
--overwrite
|
|
fi
|
|
done < <(kubectl --kubeconfig "${KUBECONFIG_PATH}" get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
|
|
|
|
target_nodes="$(kubectl --kubeconfig "${KUBECONFIG_PATH}" get nodes -l "${prometheus_selector}" -o name)"
|
|
if [[ -z "${target_nodes}" ]]; then
|
|
echo "No nodes match ${prometheus_selector}; refusing to move prometheus-stack." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
delete_prometheus_stack_storage() {
|
|
local namespace="${1:-monitoring}"
|
|
local pattern='(^|-)prometheus-stack-(prometheus|alertmanager|grafana)(-|$)|^prometheus-prometheus-stack|^alertmanager-prometheus-stack|^storage-prometheus-stack-grafana'
|
|
local pvc_names
|
|
local pv_names
|
|
|
|
pvc_names="$(kubectl --kubeconfig "${KUBECONFIG_PATH}" -n "${namespace}" get pvc -o name 2>/dev/null |
|
|
awk -F/ -v pattern="${pattern}" '$2 ~ pattern {print $2}')"
|
|
pv_names="$(kubectl --kubeconfig "${KUBECONFIG_PATH}" get pv \
|
|
-o jsonpath='{range .items[?(@.spec.claimRef.namespace=="'"${namespace}"'")]}{.metadata.name}{"\t"}{.spec.claimRef.name}{"\n"}{end}' 2>/dev/null |
|
|
awk -v pattern="${pattern}" '$2 ~ pattern {print $1}')"
|
|
|
|
if [[ -n "${pvc_names}" ]]; then
|
|
echo "Deleting old prometheus-stack PVCs in ${namespace}; saved Prometheus, Alertmanager, and Grafana data will be discarded..."
|
|
printf '%s\n' "${pvc_names}" |
|
|
xargs -r kubectl --kubeconfig "${KUBECONFIG_PATH}" -n "${namespace}" delete pvc --wait=true --timeout=180s
|
|
fi
|
|
|
|
if [[ -n "${pv_names}" ]]; then
|
|
echo "Deleting old prometheus-stack retained PV objects..."
|
|
printf '%s\n' "${pv_names}" |
|
|
xargs -r kubectl --kubeconfig "${KUBECONFIG_PATH}" delete pv --wait=false
|
|
fi
|
|
}
|
|
|
|
run_tofu_stack() {
|
|
local stack="$1"
|
|
local auto_approve="${L
|
|
|
|
... [OUTPUT TRUNCATED - 267,570 chars omitted out of 317,495 total] ...
|
|
|
|
s not installed. Run ./jeannie secrets-init first." >&2
|
|
return 1
|
|
fi
|
|
|
|
key_file="$(sops_age_key_file)"
|
|
if [[ ! -s "${key_file}" ]]; then
|
|
echo "Missing SOPS age key at ${key_file}. Run ./jeannie secrets-init, or copy the age key to this machine." >&2
|
|
return 1
|
|
fi
|
|
|
|
out="$(SOPS_AGE_KEY_FILE="${key_file}" sops --decrypt "${file}")" || {
|
|
echo "SOPS decryption failed for ${file}." >&2
|
|
return 1
|
|
}
|
|
|
|
# Export every bare `KEY: value` line (ignore comments/blank).
|
|
local line key val syn="0"
|
|
while IFS= read -r line; do
|
|
[[ "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*: ]] || continue
|
|
key="${line%%:*}"
|
|
val="${line#*: }"
|
|
# Strip surrounding double quotes, if sops kept them.
|
|
val="${val%\"}"; val="${val#\"}"
|
|
export "${key}=${val}"
|
|
syn="$((syn + 1))"
|
|
done <<<"${out}"
|
|
|
|
if ((syn == 0)); then
|
|
echo "No KEY: value pairs found in decrypted ${file}." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
tailnet_policy_check() {
|
|
"${REPO_ROOT}/scripts/validate-tailnet-policy"
|
|
}
|
|
|
|
ai_index() {
|
|
local index_dir="${LAB_AI_KNOWLEDGE_INDEX_DIR:-/data/homelab-ai/index}"
|
|
|
|
require_debian_server "ai-index"
|
|
ensure_python3
|
|
if [[ "${index_dir}" == /data/* ]]; then
|
|
sudo mkdir -p "${index_dir}"
|
|
sudo chown "${USER}:$(id -gn)" "${index_dir}"
|
|
fi
|
|
"${REPO_ROOT}/scripts/build-homelab-ai-index" --index-dir "${index_dir}"
|
|
}
|
|
|
|
ai_check() {
|
|
local index_dir="${LAB_AI_KNOWLEDGE_INDEX_DIR:-/data/homelab-ai/index}"
|
|
local endpoint="${LAB_AI_GATEWAY_URL:-${LAB_OLLAMA_URL:-http://127.0.0.1:11434}}"
|
|
local model="${LAB_AI_GATEWAY_MODEL:-qwen2.5:0.5b}"
|
|
local failures=0
|
|
|
|
require_debian_server "ai-check"
|
|
|
|
status_section "AI Knowledge"
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
printf '%-28s ok\n' "python3"
|
|
else
|
|
printf '%-28s missing\n' "python3"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
if [[ -s "${index_dir}/index.json" ]]; then
|
|
printf '%-28s %s\n' "knowledge index" "${index_dir}/index.json"
|
|
else
|
|
printf '%-28s missing - run ./jeannie ai-index\n' "knowledge index"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
if "${REPO_ROOT}/scripts/query-homelab-ai-index" --index-dir "${index_dir}" --context-only --limit 2 "gitea edge traefik" >/dev/null 2>&1; then
|
|
printf '%-28s ok\n' "retrieval smoke test"
|
|
else
|
|
printf '%-28s failed\n' "retrieval smoke test"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
if backstage_brain_enabled; then
|
|
if curl -fsS --max-time 5 "${endpoint%/}/api/tags" >/dev/null 2>&1; then
|
|
printf '%-28s ok - %s\n' "ollama endpoint" "${endpoint}"
|
|
if python3 "${REPO_ROOT}/scripts/ai-model-check.py" "${endpoint}" "${model}"
|
|
then
|
|
printf '%-28s ok - %s\n' "ollama model" "${model}"
|
|
else
|
|
printf '%-28s missing - run: ollama pull %s\n' "ollama model" "${model}"
|
|
failures=$((failures + 1))
|
|
fi
|
|
else
|
|
printf '%-28s unreachable - %s\n' "ollama endpoint" "${endpoint}"
|
|
failures=$((failures + 1))
|
|
fi
|
|
else
|
|
printf '%-28s disabled\n' "backstage helper"
|
|
fi
|
|
|
|
if ((failures > 0)); then
|
|
echo "AI check failed with ${failures} issue(s)." >&2
|
|
exit 1
|
|
fi
|
|
echo "AI check passed."
|
|
}
|
|
|
|
ask_homelab() {
|
|
require_debian_server "ask"
|
|
"${REPO_ROOT}/scripts/ask" "${@:2}"
|
|
}
|
|
|
|
ai_evals() {
|
|
"${REPO_ROOT}/scripts/ai-evals" "${@:2}"
|
|
}
|
|
|
|
impact() {
|
|
"${REPO_ROOT}/scripts/impact" "${@:2}"
|
|
}
|
|
|
|
review_last_change() {
|
|
"${REPO_ROOT}/scripts/review-last-change" "${@:2}"
|
|
}
|
|
|
|
security_scan() {
|
|
require_debian_server "security-scan"
|
|
"${REPO_ROOT}/scripts/security-scan" all
|
|
}
|
|
|
|
security_prepare() {
|
|
require_debian_server "security-prepare"
|
|
"${REPO_ROOT}/scripts/security-scan" prepare
|
|
}
|
|
|
|
security_zap() {
|
|
require_debian_server "security-zap"
|
|
"${REPO_ROOT}/scripts/security-scan" zap
|
|
}
|
|
|
|
security_k8s() {
|
|
require_debian_server "security-k8s"
|
|
"${REPO_ROOT}/scripts/security-scan" kube-bench
|
|
}
|
|
|
|
security_host() {
|
|
require_debian_server "security-host"
|
|
"${REPO_ROOT}/scripts/security-scan" host
|
|
}
|
|
|
|
security_trivy() {
|
|
require_debian_server "security-trivy"
|
|
"${REPO_ROOT}/scripts/security-scan" trivy
|
|
}
|
|
|
|
security_secrets() {
|
|
require_debian_server "security-secrets"
|
|
"${REPO_ROOT}/scripts/security-scan" secrets
|
|
}
|
|
|
|
security_nuclei() {
|
|
require_debian_server "security-nuclei"
|
|
"${REPO_ROOT}/scripts/security-scan" nuclei
|
|
}
|
|
|
|
security_web() {
|
|
require_debian_server "security-web"
|
|
"${REPO_ROOT}/scripts/security-scan" web
|
|
}
|
|
|
|
security_logs() {
|
|
require_debian_server "security-logs"
|
|
"${REPO_ROOT}/scripts/security-logs"
|
|
}
|
|
|
|
security_runtime() {
|
|
require_debian_server "security-runtime"
|
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
echo "kubectl is required for security-runtime." >&2
|
|
exit 1
|
|
fi
|
|
|
|
kubectl --kubeconfig "${KUBECONFIG}" -n kube-system rollout status daemonset/tetragon --timeout=30s
|
|
kubectl --kubeconfig "${KUBECONFIG}" -n kube-system get pods -l app.kubernetes.io/name=tetragon -o wide
|
|
cat <<'EOF'
|
|
|
|
Watch recent Tetragon events:
|
|
kubectl -n kube-system logs -l app.kubernetes.io/name=tetragon -c export-stdout --tail=100 -f
|
|
|
|
Useful practice trigger:
|
|
kubectl -n security-lab exec deploy/juice-shop -- sh -c 'id; uname -a'
|
|
EOF
|
|
}
|