mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
Fix k3s CNPG image registry resolution and import flows
- In k3s mode, resolve CNPG images to a cluster-reachable internal registry and ignore k3d/localhost-ish registry values. - Avoid any k3d cluster/containerd interactions when PROLE_MODE=k3s; add a preflight-image action. - Extend init scripts and Ansible k3s/mariadb import tasks/playbooks for the updated k3s flow. - Add/extend installer UI + tests around services and action/milestone helpers. - Add merlin MariaDB provisioning playbook, services init-script test, and a k3s datastore SQL snapshot. Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
d2295f4c2b
commit
125da5669b
@ -1,5 +1,5 @@
|
||||
; Prole Master Configuration File
|
||||
; Generated by install.py on 2026-03-06 00:36:32
|
||||
; Generated by install.py on 2026-03-06 23:44:26
|
||||
; This file is used as input for Ansible deployment and k8s cluster creation.
|
||||
|
||||
[User]
|
||||
@ -110,7 +110,7 @@ ANSIBLE_DOMAIN = prole.org
|
||||
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
|
||||
ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory
|
||||
ANSIBLE_REALM = PROLE.ORG
|
||||
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.3","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org","merlin.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.3","merlin.prole.org":"10.0.0.3","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["mariadb_primary","mariadb_replica"]}
|
||||
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.3","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"","server_host":"","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_servers":["myrddin.prole.org","pi.prole.org","merlin.prole.org"],"k3s_agents":[],"k3s_hosts:children":["k3s_servers","k3s_agents"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.3","merlin.prole.org":"10.0.0.3","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["k3s_agents","k3s_servers","mariadb_primary","mariadb_replica"]}
|
||||
KDC_ANSIBLE_DETECTED = 10.0.0.3
|
||||
KDC_AUTO_DETECTED = 10.0.0.3
|
||||
KERBEROS_AUTO_ENABLED = True
|
||||
|
||||
@ -156,9 +156,63 @@ resolve_cnpg_image() {
|
||||
# Using LOCAL_REGISTRY (e.g., localhost:5000) breaks inside cluster and may
|
||||
# also prefer IPv6 ::1, leading to connection refused. Avoid it.
|
||||
local registry=""
|
||||
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
|
||||
registry="${LOCAL_REGISTRY_INTERNAL}"
|
||||
elif [[ -n "${LOCAL_REGISTRY:-}" ]]; then
|
||||
|
||||
_cnpg_registry_looks_like_k3d() {
|
||||
local r="${1:-}"
|
||||
[[ -n "$r" ]] || return 1
|
||||
case "$r" in
|
||||
k3d-*|*"/k3d-"*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
_cnpg_registry_is_localhostish() {
|
||||
local r="${1:-}"
|
||||
[[ -n "$r" ]] || return 1
|
||||
case "$r" in
|
||||
localhost:5000|127.0.0.1:5000|*.localhost|*.localhost:5000) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
_cnpg_k3s_internal_registry() {
|
||||
# For k3s, never use k3d registry names or localhost-ish endpoints.
|
||||
local r="${LOCAL_REGISTRY_INTERNAL:-}"
|
||||
if _cnpg_registry_looks_like_k3d "$r" || _cnpg_registry_is_localhostish "$r"; then
|
||||
r=""
|
||||
fi
|
||||
if [[ -n "$r" ]]; then
|
||||
printf '%s' "$r"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local host=""
|
||||
if command -v _prole_host_from_url >/dev/null 2>&1; then
|
||||
host=$(_prole_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")
|
||||
fi
|
||||
if [[ -n "${host:-}" ]]; then
|
||||
printf '%s' "${host}:5000"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local ns="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}"
|
||||
printf '%s' "registry.${ns}.svc.cluster.local:5000"
|
||||
}
|
||||
|
||||
local _mode
|
||||
_mode=$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-}}")
|
||||
case "${_mode}" in
|
||||
k3s)
|
||||
registry=$(_cnpg_k3s_internal_registry)
|
||||
;;
|
||||
*)
|
||||
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
|
||||
registry="${LOCAL_REGISTRY_INTERNAL}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$registry" && -n "${LOCAL_REGISTRY:-}" ]]; then
|
||||
# Only fall back to LOCAL_REGISTRY when INTERNAL is not available
|
||||
# and it's not pointing at localhost (which is invalid for cluster pulls).
|
||||
if [[ "${LOCAL_REGISTRY}" != "localhost:5000" && "${LOCAL_REGISTRY}" != "127.0.0.1:5000" ]]; then
|
||||
@ -642,12 +696,141 @@ _push_to_k3s_registry() {
|
||||
local push_host="${LOCAL_REGISTRY:-${LOCAL_REGISTRY_INTERNAL:-myrddin.prole.org:5000}}"
|
||||
local plain_image="${image##*/}"
|
||||
|
||||
# Normalize push_host (strip scheme if provided)
|
||||
push_host="${push_host#http://}"
|
||||
push_host="${push_host#https://}"
|
||||
|
||||
_import_image_to_k3s_nodes() {
|
||||
local import_image="$1"
|
||||
|
||||
if [[ "${PROLE_MODE:-}" != "k3s" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "${PROLE_HOME:-}" || ! -d "${PROLE_HOME}/infrastructure/playbooks" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if ! command -v docker >/dev/null 2>&1 || ! command -v ansible-playbook >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tar_path vars_path
|
||||
tar_path="$(mktemp -t prole-db-image.XXXXXX).tar"
|
||||
vars_path="$(mktemp -t prole-k3s-import.XXXXXX).yml"
|
||||
|
||||
echo " Exporting image '$import_image' to '$tar_path' for direct k3s import ..."
|
||||
if ! docker save "$import_image" -o "$tar_path" >/dev/null 2>&1; then
|
||||
rm -f "$tar_path" "$vars_path" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
cat >"$vars_path" <<EOF
|
||||
k3s_import_images:
|
||||
- path: "$tar_path"
|
||||
image: "$import_image"
|
||||
EOF
|
||||
|
||||
local vault_arg=()
|
||||
if [[ -f "${PROLE_HOME}/.vault_pass" ]]; then
|
||||
vault_arg+=(--vault-password-file "${PROLE_HOME}/.vault_pass")
|
||||
fi
|
||||
|
||||
echo " Importing image into k3s nodes via Ansible (playbook: k3s_import_images.yml) ..."
|
||||
if ANSIBLE_CONFIG="${PROLE_HOME}/ansible.cfg" ansible-playbook "${vault_arg[@]}" \
|
||||
"${PROLE_HOME}/infrastructure/playbooks/k3s_import_images.yml" \
|
||||
-l k3s_hosts \
|
||||
-e "@${vars_path}"; then
|
||||
echo " ✓ Image imported into k3s nodes via containerd import."
|
||||
rm -f "$tar_path" "$vars_path" >/dev/null 2>&1 || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$tar_path" "$vars_path" >/dev/null 2>&1 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
if command -v skopeo >/dev/null 2>&1; then
|
||||
# Avoid long partial uploads when the host registry endpoint is unreachable.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if ! curl -fsS -m 2 "http://${push_host}/v2/" >/dev/null 2>&1; then
|
||||
# Prefer direct containerd import in k3s when the registry endpoint isn't reachable.
|
||||
if _import_image_to_k3s_nodes "$image"; then
|
||||
return 0
|
||||
fi
|
||||
echo " WARN: Registry endpoint 'http://${push_host}/v2/' is not reachable; will try port-forward fallback." >&2
|
||||
else
|
||||
echo " Pushing to k3s registry at '${push_host}' using skopeo ..."
|
||||
if skopeo copy --dest-tls-verify=false docker-daemon:"$image" docker://"${push_host}/${plain_image}"; then
|
||||
echo " ✓ Image pushed to registry at '${push_host}'."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " Pushing to k3s registry at '${push_host}' using skopeo ..."
|
||||
if skopeo copy --dest-tls-verify=false docker-daemon:"$image" docker://"${push_host}/${plain_image}"; then
|
||||
echo " ✓ Image pushed to registry at '${push_host}'."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: port-forward the in-cluster registry service and push via localhost.
|
||||
# This avoids relying on hostPort / firewall rules for ${push_host}.
|
||||
if command -v kubectl >/dev/null 2>&1 && command -v skopeo >/dev/null 2>&1; then
|
||||
local reg_ns="${REGISTRY_NAMESPACE:-}"
|
||||
# In k3s mode, the registry is a service-layer component and should live in
|
||||
# SERVICE_NAMESPACE; normalize legacy/default config to that namespace.
|
||||
if [[ "${PROLE_MODE:-}" == "k3s" && ( -z "$reg_ns" || "$reg_ns" == "default" ) ]]; then
|
||||
reg_ns="${SERVICE_NAMESPACE:-}"
|
||||
fi
|
||||
if [[ -z "$reg_ns" ]]; then
|
||||
reg_ns="${SERVICE_NAMESPACE:-default}"
|
||||
fi
|
||||
local pf_port="${PROLE_REGISTRY_PORT_FORWARD_LOCAL:-55000}"
|
||||
local pf_log
|
||||
pf_log="$(mktemp -t prole-registry-pf.XXXXXX)"
|
||||
|
||||
echo " Trying registry push via kubectl port-forward (namespace='${reg_ns}', local=127.0.0.1:${pf_port} -> svc/registry:5000) ..."
|
||||
kubectl -n "$reg_ns" port-forward --address 127.0.0.1 svc/registry "${pf_port}:5000" >"$pf_log" 2>&1 &
|
||||
local pf_pid=$!
|
||||
|
||||
local ready=0
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
for _i in {1..40}; do
|
||||
if curl -fsS -m 1 "http://127.0.0.1:${pf_port}/v2/" >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$pf_pid" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
else
|
||||
# Without curl, best-effort short delay before attempting push.
|
||||
sleep 2
|
||||
ready=1
|
||||
fi
|
||||
|
||||
local rc=1
|
||||
if [[ "$ready" == "1" ]]; then
|
||||
if skopeo copy --dest-tls-verify=false docker-daemon:"$image" docker://"127.0.0.1:${pf_port}/${plain_image}"; then
|
||||
echo " ✓ Image pushed to registry via port-forward."
|
||||
rc=0
|
||||
fi
|
||||
else
|
||||
echo " WARN: registry port-forward did not become ready (see $pf_log)." >&2
|
||||
fi
|
||||
|
||||
kill "$pf_pid" >/dev/null 2>&1 || true
|
||||
wait "$pf_pid" >/dev/null 2>&1 || true
|
||||
rm -f "$pf_log" >/dev/null 2>&1 || true
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if _import_image_to_k3s_nodes "$image"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to docker push if skopeo is missing or fails (might fail if daemon not configured)
|
||||
@ -708,34 +891,37 @@ _ensure_prole_db_image() {
|
||||
fi
|
||||
fi
|
||||
image=$(resolve_cnpg_image "$image")
|
||||
|
||||
# Auto-detect active k3d cluster name
|
||||
local cluster_name="${K3D_CLUSTER_NAME:-}"
|
||||
if [[ -z "$cluster_name" ]]; then
|
||||
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
|
||||
fi
|
||||
cluster_name="${cluster_name:-knoe-dev-cluster}"
|
||||
|
||||
local prole_db_dir="${PROLE_HOME:-$SCRIPT_DIR/..}/prole-db"
|
||||
local plain_image="${image##*/}" # e.g. prole-db:18-088
|
||||
|
||||
echo "Pre-flight: verifying image '$image' is available in k3d cluster '$cluster_name' ..."
|
||||
|
||||
# Step 1: check if already present in k3d containerd with matching digest
|
||||
local containerd_digest local_digest containerd_sha
|
||||
containerd_digest=$(docker exec "k3d-${cluster_name}-server-0" \
|
||||
ctr images ls -q 2>/dev/null | grep -F "$image" | head -1 || true)
|
||||
if [[ -n "$containerd_digest" ]]; then
|
||||
local_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null \
|
||||
| awk -F@ '{print $2}' || true)
|
||||
containerd_sha=$(docker exec "k3d-${cluster_name}-server-0" \
|
||||
ctr images ls 2>/dev/null | grep -F "$image" | awk '{print $3}' | head -1 || true)
|
||||
if [[ -z "$local_digest" || "$containerd_sha" == "$local_digest" ]]; then
|
||||
echo " ✓ Image '$image' already in k3d containerd (digest match); no import needed."
|
||||
return 0
|
||||
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
|
||||
echo "Pre-flight: ensuring image '$image' is available in k3s registry/import path ..."
|
||||
else
|
||||
# Auto-detect active k3d cluster name
|
||||
local cluster_name="${K3D_CLUSTER_NAME:-}"
|
||||
if [[ -z "$cluster_name" ]]; then
|
||||
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
|
||||
fi
|
||||
cluster_name="${cluster_name:-knoe-dev-cluster}"
|
||||
|
||||
echo "Pre-flight: verifying image '$image' is available in k3d cluster '$cluster_name' ..."
|
||||
|
||||
# Step 1: check if already present in k3d containerd with matching digest
|
||||
local containerd_digest local_digest containerd_sha
|
||||
containerd_digest=$(docker exec "k3d-${cluster_name}-server-0" \
|
||||
ctr images ls -q 2>/dev/null | grep -F "$image" | head -1 || true)
|
||||
if [[ -n "$containerd_digest" ]]; then
|
||||
local_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null \
|
||||
| awk -F@ '{print $2}' || true)
|
||||
containerd_sha=$(docker exec "k3d-${cluster_name}-server-0" \
|
||||
ctr images ls 2>/dev/null | grep -F "$image" | awk '{print $3}' | head -1 || true)
|
||||
if [[ -z "$local_digest" || "$containerd_sha" == "$local_digest" ]]; then
|
||||
echo " ✓ Image '$image' already in k3d containerd (digest match); no import needed."
|
||||
return 0
|
||||
fi
|
||||
echo " Image '$image' in containerd but digest mismatch (local: ${local_digest:-unknown}, containerd: ${containerd_sha:-unknown}); re-importing ..."
|
||||
docker exec "k3d-${cluster_name}-server-0" ctr images rm "$image" 2>/dev/null || true
|
||||
fi
|
||||
echo " Image '$image' in containerd but digest mismatch (local: ${local_digest:-unknown}, containerd: ${containerd_sha:-unknown}); re-importing ..."
|
||||
docker exec "k3d-${cluster_name}-server-0" ctr images rm "$image" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Step 2a: registry-tagged image in local Docker daemon → push + import
|
||||
@ -795,7 +981,11 @@ _ensure_prole_db_image() {
|
||||
echo "ERROR: Dockerfile not found in '$prole_db_dir'; cannot build prole-db image." >&2
|
||||
return 1
|
||||
fi
|
||||
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
|
||||
if [[ "${PROLE_MODE:-}" == "k3s" ]]; then
|
||||
echo " Image '$image' not found in Docker daemon or docker-import dir."
|
||||
else
|
||||
echo " Image '$image' not found in k3d, Docker daemon, or docker-import dir."
|
||||
fi
|
||||
echo " Building prole-db image from '$prole_db_dir' ..."
|
||||
if ! docker build -t "$plain_image" "$prole_db_dir"; then
|
||||
echo "ERROR: docker build failed for image '$plain_image'." >&2
|
||||
@ -1003,13 +1193,8 @@ wait_for_cnpg_pods() {
|
||||
fail_image=$(kubectl -n "$NAMESPACE" get pod "$fail_pod" \
|
||||
-o jsonpath='{.spec.initContainers[0].image}' 2>/dev/null || true)
|
||||
fi
|
||||
echo "WARN: Pod '$fail_pod' cannot pull image '${fail_image:-unknown}' (ErrImagePull/ImagePullBackOff in k3d mode)." >&2
|
||||
echo "WARN: Pod '$fail_pod' cannot pull image '${fail_image:-unknown}' (ErrImagePull/ImagePullBackOff)." >&2
|
||||
import_dir="${DOCKER_IMPORT_DIR:-${PROLE_DATA:+${PROLE_DATA}/docker-import}}"
|
||||
cluster_name="${K3D_CLUSTER_NAME:-}"
|
||||
if [[ -z "$cluster_name" ]]; then
|
||||
cluster_name=$(k3d cluster list --no-headers 2>/dev/null | awk '{print $1}' | head -1)
|
||||
fi
|
||||
cluster_name="${cluster_name:-knoe-dev-cluster}"
|
||||
# Re-run full image ensure logic to push/import the failing image
|
||||
echo " Attempting image remediation via _ensure_prole_db_image ..." >&2
|
||||
_ensure_prole_db_image >&2 || true
|
||||
@ -1816,7 +2001,16 @@ deploy_cluster() {
|
||||
fi
|
||||
}
|
||||
|
||||
preflight_image() {
|
||||
ensure_tools
|
||||
ensure_namespace
|
||||
_ensure_prole_db_image
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
preflight-image|image-preflight|preflight_image)
|
||||
preflight_image
|
||||
;;
|
||||
recreate)
|
||||
ensure_tools
|
||||
"$0" delete "$CNPG_CLUSTER_NAME"
|
||||
|
||||
@ -110,13 +110,34 @@ import_image_k3s() {
|
||||
fi
|
||||
|
||||
echo "Importing $DB_MANAGER_IMAGE into k3s nodes ..."
|
||||
if [[ -f "$ansible_cfg" ]]; then
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { echo "ERROR: Failed to copy image to k3s nodes." >&2; return 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
else
|
||||
"${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { echo "ERROR: Failed to copy image to k3s nodes." >&2; return 1; }
|
||||
"${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
|
||||
if ! command -v ansible-playbook >/dev/null 2>&1; then
|
||||
echo "ERROR: ansible-playbook not available; cannot import image into k3s nodes." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local playbook="$PROLE_HOME/infrastructure/playbooks/k3s_import_images.yml"
|
||||
if [[ ! -f "$playbook" ]]; then
|
||||
echo "ERROR: Ansible playbook not found: $playbook" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local images_json
|
||||
images_json=$(printf '[{"path": "%s", "image": "%s"}]' "$tar_path" "$DB_MANAGER_IMAGE")
|
||||
|
||||
local ansible_cmd=(ansible-playbook -f 1)
|
||||
if [[ -n "$vault_file" ]]; then
|
||||
ansible_cmd+=("--vault-password-file" "$vault_file")
|
||||
fi
|
||||
ansible_cmd+=(-e "{\"k3s_import_images\": $images_json}")
|
||||
ansible_cmd+=("$playbook")
|
||||
|
||||
if [[ -f "$ansible_cfg" ]]; then
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_cmd[@]}" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
else
|
||||
"${ansible_cmd[@]}" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
fi
|
||||
|
||||
echo "Image imported to k3s nodes successfully."
|
||||
}
|
||||
|
||||
|
||||
@ -384,12 +384,32 @@ build_prole_kdc_image() {
|
||||
fi
|
||||
|
||||
log "Importing authority image into k3s nodes (group: k3s_hosts)"
|
||||
|
||||
if ! command -v ansible-playbook >/dev/null 2>&1; then
|
||||
err "ERROR: ansible-playbook not available; cannot import authority image to k3s nodes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local playbook="${SCRIPT_DIR}/../infrastructure/playbooks/k3s_import_images.yml"
|
||||
if [[ ! -f "$playbook" ]]; then
|
||||
err "ERROR: Ansible playbook not found: $playbook"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local images_json
|
||||
images_json=$(printf '[{"path": "%s", "image": "%s"}]' "$tar_path" "$local_tag")
|
||||
|
||||
local ansible_cmd=(ansible-playbook -f 1)
|
||||
if [[ -n "$vault_file" ]]; then
|
||||
ansible_cmd+=("--vault-password-file" "$vault_file")
|
||||
fi
|
||||
ansible_cmd+=(-e "{\"k3s_import_images\": $images_json}")
|
||||
ansible_cmd+=("$playbook")
|
||||
|
||||
if [[ -f "$ansible_cfg" ]]; then
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { err "ERROR: Failed to copy authority image to k3s nodes."; exit 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_cmd[@]}" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
else
|
||||
"${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { err "ERROR: Failed to copy authority image to k3s nodes."; exit 1; }
|
||||
"${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
"${ansible_cmd[@]}" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
fi
|
||||
else
|
||||
err "ERROR: ansible not available; cannot import authority image to k3s nodes."
|
||||
|
||||
@ -25,6 +25,10 @@ unset _has_config _arg
|
||||
|
||||
common_core_preparse_config "$@"
|
||||
|
||||
# Remove -c/--config from the remaining args now that PROLE_CONF is set.
|
||||
# `common_core_preparse_config` exposes the stripped args as `COMMON_CORE_ARGS`.
|
||||
set -- "${COMMON_CORE_ARGS[@]}"
|
||||
|
||||
# Load environment and config via prole_cfg.sh
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
@ -70,15 +74,6 @@ EOF
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c|--config)
|
||||
shift
|
||||
# Already handled by common_core_preparse_config if we use common_core_lib.sh
|
||||
# but since we don't, we can just skip it here as prole_cfg.sh
|
||||
# will pick it up if we set PROLE_CONF?
|
||||
# Actually prole_cfg.sh uses PROLE_CONF.
|
||||
# Let's just consume it.
|
||||
shift
|
||||
;;
|
||||
-m|--mode)
|
||||
shift
|
||||
prole_set_mode "${1:-}"
|
||||
@ -169,6 +164,7 @@ ARGOCD_SERVER_SERVICE=${ARGOCD_SERVER_SERVICE:-argocd-server}
|
||||
ARGOCD_PORT_FORWARD_LOCAL=${ARGOCD_PORT_FORWARD_LOCAL:-8081}
|
||||
ARGOCD_PORT_FORWARD_REMOTE=${ARGOCD_PORT_FORWARD_REMOTE:-80}
|
||||
ARGOCD_NODE_SELECTOR=${ARGOCD_NODE_SELECTOR:-}
|
||||
REGISTRY_NODE_SELECTOR=${REGISTRY_NODE_SELECTOR:-}
|
||||
|
||||
current_mode() {
|
||||
if command -v prole_normalize_mode >/dev/null 2>&1; then
|
||||
@ -196,6 +192,33 @@ resolve_argocd_node_selector() {
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_registry_node_selector() {
|
||||
if [[ -n "$REGISTRY_NODE_SELECTOR" ]]; then
|
||||
printf '%s' "$REGISTRY_NODE_SELECTOR"
|
||||
return 0
|
||||
fi
|
||||
# Default: pin to the control-plane node so hostPort:5000 is reachable via
|
||||
# the k3s server host (e.g. myrddin.prole.org:5000).
|
||||
resolve_argocd_node_selector
|
||||
}
|
||||
|
||||
apply_registry_node_selector() {
|
||||
local selector
|
||||
selector="$(resolve_registry_node_selector)"
|
||||
if [[ -z "$selector" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local key="${selector%%=*}"
|
||||
local val="${selector#*=}"
|
||||
if [[ -z "$key" || -z "$val" ]]; then
|
||||
echo "WARN: invalid REGISTRY_NODE_SELECTOR '$selector' (expected key=value); skipping." >&2
|
||||
return 0
|
||||
fi
|
||||
kubectl -n "$REGISTRY_NAMESPACE" patch deployment/registry \
|
||||
--type merge \
|
||||
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"$key\":\"$val\"}}}}}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
apply_argocd_node_selector() {
|
||||
local selector
|
||||
selector="$(resolve_argocd_node_selector)"
|
||||
@ -352,6 +375,7 @@ apply_registry() {
|
||||
|
||||
echo "Applying registry manifest to namespace '$REGISTRY_NAMESPACE' ..."
|
||||
render_registry_manifest | kubectl apply --server-side --force-conflicts --field-manager=prole-installer --validate=false -n "$REGISTRY_NAMESPACE" -f -
|
||||
apply_registry_node_selector
|
||||
kubectl rollout status deploy/registry -n "$REGISTRY_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
||||
}
|
||||
|
||||
|
||||
@ -4,5 +4,7 @@
|
||||
# Remote TCP admin user created by `mariadb_primary` for management.
|
||||
k3s_mariadb_admin_user: prole_admin
|
||||
|
||||
# Keep the k3s datastore password vaulted; by default reuse the vaulted admin password.
|
||||
k3s_datastore_mariadb_password: "{{ k3s_mariadb_admin_password }}"
|
||||
# Keep the k3s datastore password vaulted.
|
||||
# For consistency with existing k3s server configuration, reuse the AD DC's vaulted Samba DNS admin password.
|
||||
# (This ensures k3s can authenticate to the datastore after migration.)
|
||||
k3s_datastore_mariadb_password: "{{ hostvars[groups['ad_dc'][0]].vault_samba_dns_admin_pass }}"
|
||||
@ -17,8 +17,9 @@ k3s_enabled: false
|
||||
k3s_role: server
|
||||
k3s_cluster_init: false
|
||||
|
||||
# Keep the current cluster datastore until the MariaDB cutover to merlin is performed.
|
||||
k3s_datastore_mariadb_host: synology.prole.org
|
||||
# k3s datastore now lives on merlin.
|
||||
# NOTE: Use IP to avoid DNS drift during migration/cutover.
|
||||
k3s_datastore_mariadb_host: 10.0.0.6
|
||||
k3s_datastore_mariadb_port: 3306
|
||||
k3s_datastore_mariadb_db: k3s
|
||||
k3s_datastore_mariadb_user: prole_k3s
|
||||
|
||||
@ -70,7 +70,7 @@ k3s_enabled: true
|
||||
k3s_cluster_init: false
|
||||
k3s_role: server
|
||||
k3s_token: "{{ vault_k3s_token | default('') }}" # store this in vault
|
||||
k3s_datastore_mariadb_host: synology.prole.org
|
||||
k3s_datastore_mariadb_host: 10.0.0.6
|
||||
k3s_datastore_mariadb_port: 3306
|
||||
k3s_datastore_mariadb_db: k3s
|
||||
k3s_datastore_mariadb_user: prole_k3s
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
ansible.builtin.setup:
|
||||
gather_subset:
|
||||
- min
|
||||
|
||||
- name: Skip import on hosts where k3s is disabled
|
||||
ansible.builtin.meta: end_host
|
||||
when: not (k3s_enabled | default(true) | bool)
|
||||
tasks:
|
||||
- name: Import image tarballs
|
||||
ansible.builtin.import_role:
|
||||
|
||||
15
infrastructure/playbooks/merlin_mariadb_provision.yml
Normal file
15
infrastructure/playbooks/merlin_mariadb_provision.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: Provision Merlin MariaDB primary (users/grants/datadir)
|
||||
hosts: mariadb_primary
|
||||
gather_facts: false
|
||||
become: true
|
||||
serial: 1
|
||||
|
||||
pre_tasks:
|
||||
- name: Gather minimal facts
|
||||
ansible.builtin.setup:
|
||||
gather_subset:
|
||||
- min
|
||||
|
||||
roles:
|
||||
- mariadb_primary
|
||||
@ -1,10 +1,28 @@
|
||||
---
|
||||
|
||||
- name: Wait for k3s containerd socket to be present
|
||||
ansible.builtin.wait_for:
|
||||
path: /run/k3s/containerd/containerd.sock
|
||||
state: present
|
||||
timeout: "{{ k3s_containerd_socket_wait_timeout | default(120) }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Verify k3s container runtime is available
|
||||
ansible.builtin.command: k3s ctr -n k8s.io version
|
||||
register: _k3s_ctr_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: _k3s_ctr_version.rc == 0
|
||||
|
||||
- name: Fail if k3s container runtime is not available
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
k3s/containerd is not ready on {{ inventory_hostname }}.
|
||||
Expected /run/k3s/containerd/containerd.sock and a working `k3s ctr`.
|
||||
Check the k3s service status and node networking, then retry the import.
|
||||
when: _k3s_ctr_version.rc != 0
|
||||
|
||||
- name: Import image tarballs into k3s containerd
|
||||
ansible.builtin.include_tasks: import_image.yml
|
||||
@ -13,5 +31,4 @@
|
||||
loop_var: k3s_import_image
|
||||
label: "{{ k3s_import_image.path | basename }}"
|
||||
when:
|
||||
- _k3s_ctr_version.rc == 0
|
||||
- (k3s_import_images | length) > 0
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
mysql --protocol=socket --user=root
|
||||
--execute="
|
||||
CREATE USER IF NOT EXISTS '{{ k3s_mariadb_admin_user }}'@'%' IDENTIFIED BY '{{ k3s_mariadb_admin_password }}';
|
||||
ALTER USER '{{ k3s_mariadb_admin_user }}'@'%' IDENTIFIED BY '{{ k3s_mariadb_admin_password }}';
|
||||
GRANT ALL PRIVILEGES ON *.* TO '{{ k3s_mariadb_admin_user }}'@'%' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;"
|
||||
when: not ansible_check_mode
|
||||
@ -139,6 +140,7 @@
|
||||
--execute="
|
||||
CREATE DATABASE IF NOT EXISTS `{{ k3s_datastore_mariadb_db | default('k3s') }}`;
|
||||
CREATE USER IF NOT EXISTS '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%' IDENTIFIED BY '{{ k3s_datastore_mariadb_password }}';
|
||||
ALTER USER '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%' IDENTIFIED BY '{{ k3s_datastore_mariadb_password }}';
|
||||
GRANT ALL PRIVILEGES ON `{{ k3s_datastore_mariadb_db | default('k3s') }}`.* TO '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%';
|
||||
FLUSH PRIVILEGES;"
|
||||
when: not ansible_check_mode
|
||||
|
||||
@ -34,32 +34,39 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Refuse dumpfiles that appear to reference other databases
|
||||
ansible.builtin.command: >-
|
||||
{{ ansible_playbook_python }} -c
|
||||
"import re,sys
|
||||
dump=sys.argv[1]
|
||||
target_db=sys.argv[2]
|
||||
bad=set()
|
||||
with open(dump,'r',encoding='utf-8',errors='ignore') as f:
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
python3 - "{{ mariadb_import_dumpfile }}" "{{ k3s_datastore_mariadb_db }}" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
dump = sys.argv[1]
|
||||
target_db = sys.argv[2]
|
||||
bad = set()
|
||||
|
||||
with open(dump, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for ln in f:
|
||||
if ln.startswith('CREATE DATABASE'):
|
||||
bad.add('CREATE DATABASE')
|
||||
if ln.startswith('USE '):
|
||||
m=re.match(r'^USE\\s+`?([^` ;]+)`?;?', ln.strip())
|
||||
m = re.match(r'^USE\s+`?([^` ;]+)`?;?', ln.strip())
|
||||
if m and m.group(1) != target_db:
|
||||
bad.add(m.group(1))
|
||||
|
||||
if bad:
|
||||
print('Dumpfile references non-target DB(s) or contains CREATE DATABASE:', ','.join(sorted(bad)))
|
||||
sys.exit(2)
|
||||
print('Dumpfile scope OK')"
|
||||
{{ mariadb_import_dumpfile }}
|
||||
{{ k3s_datastore_mariadb_db }}
|
||||
|
||||
print('Dumpfile scope OK')
|
||||
PY
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
|
||||
- name: Import dumpfile into k3s datastore database (deliberate operator action)
|
||||
ansible.builtin.shell: >-
|
||||
set -euo pipefail;
|
||||
mysql --protocol=socket --user=root {{ k3s_datastore_mariadb_db }} < {{ mariadb_import_dumpfile }}
|
||||
mysql --protocol=socket --user=root "{{ k3s_datastore_mariadb_db }}" < "{{ mariadb_import_dumpfile }}"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: true
|
||||
|
||||
@ -13,7 +13,7 @@ network:
|
||||
{% if (netplan_static_nameservers_effective | default([])) | length > 0 %}
|
||||
nameservers:
|
||||
addresses:
|
||||
{% for ns in netplan_static_nameservers_effective %}
|
||||
{% for ns in (netplan_static_nameservers_effective | map('trim') | reject('equalto','') | unique | sort) %}
|
||||
- {{ ns }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -4,6 +4,7 @@ Installer actions and unattended workflow helpers.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import shlex
|
||||
@ -2296,30 +2297,34 @@ class ProleSilentInstaller(ProleInstallerBase):
|
||||
vault_file = str(candidate)
|
||||
|
||||
host_group = "k3s_hosts"
|
||||
ansible_base = ["ansible", host_group]
|
||||
if vault_file:
|
||||
ansible_base += ["--vault-password-file", vault_file]
|
||||
|
||||
dest_path = f"/tmp/{tar_path.name}"
|
||||
self.log(f"[INFO] Importing {image_name} into k3s nodes (group: {host_group})")
|
||||
rc = self._run_cmd(
|
||||
ansible_base
|
||||
+ ["-m", "copy", "-a", f"src={tar_path} dest={dest_path} mode=0644"],
|
||||
cwd=str(self.project_root),
|
||||
env=ansible_env,
|
||||
playbook = (
|
||||
self.project_root
|
||||
/ "infrastructure"
|
||||
/ "playbooks"
|
||||
/ "k3s_import_images.yml"
|
||||
)
|
||||
if rc != 0:
|
||||
self.err(f"[WARN] Failed to copy {tar_path} to k3s nodes (code {rc})")
|
||||
if not playbook.exists():
|
||||
self.err(f"[WARN] Ansible playbook not found: {playbook}")
|
||||
return False
|
||||
|
||||
extra_vars = json.dumps(
|
||||
{
|
||||
"k3s_import_images": [
|
||||
{"path": str(tar_path), "image": image_name}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
cmd = ["ansible-playbook", "-f", "1"]
|
||||
if vault_file:
|
||||
cmd += ["--vault-password-file", vault_file]
|
||||
cmd += ["-e", extra_vars, str(playbook)]
|
||||
|
||||
self.log(
|
||||
f"[INFO] Importing {image_name} into k3s nodes via Ansible playbook (group: {host_group})"
|
||||
)
|
||||
rc = self._run_cmd(
|
||||
ansible_base
|
||||
+ [
|
||||
"-m",
|
||||
"shell",
|
||||
"-a",
|
||||
f"ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack {dest_path}",
|
||||
],
|
||||
cmd,
|
||||
cwd=str(self.project_root),
|
||||
env=ansible_env,
|
||||
)
|
||||
@ -3563,6 +3568,7 @@ class ProleSilentInstaller(ProleInstallerBase):
|
||||
svc_args = ["-k", "update"]
|
||||
steps = [
|
||||
("init_common_services.sh", svc_args, False),
|
||||
("init_certmgr.sh", ["initialize"], False),
|
||||
("init_cloudnative_pg.sh", ["initialize"], False),
|
||||
]
|
||||
if kerberos_enabled:
|
||||
@ -3994,16 +4000,12 @@ class ProleSilentInstaller(ProleInstallerBase):
|
||||
try:
|
||||
self.inputs = {**self._default_inputs(), **self._load_inputs_from_cfg()}
|
||||
|
||||
# Silent mode: ensure we have a concrete master password before proceeding.
|
||||
# Requirement: prompt if missing or an unresolvable reference in silent mode.
|
||||
# Silent mode: ensure we have a master password source before proceeding.
|
||||
# If the value is an OpenBao reference or an encrypted `${PROLE_SECRET:...}`
|
||||
# placeholder, allow downstream secret-management steps to resolve it.
|
||||
# Only prompt/generate when the value is truly missing.
|
||||
db_pw = self._get_input("init_password.db_password", "")
|
||||
needs_prompt = False
|
||||
if not db_pw:
|
||||
needs_prompt = True
|
||||
elif _is_openbao_ref(db_pw):
|
||||
resolved = self._resolve_secret_value(db_pw)
|
||||
if not resolved or _is_openbao_ref(resolved):
|
||||
needs_prompt = True
|
||||
needs_prompt = not bool(db_pw)
|
||||
|
||||
if needs_prompt:
|
||||
# In silent installs, never block on an interactive prompt in environments
|
||||
|
||||
@ -369,16 +369,29 @@ class DockerBuildMilestone(Milestone):
|
||||
self.logger.info("Docker build disabled.")
|
||||
return
|
||||
|
||||
if progress:
|
||||
progress("Building Prole database image...", 0.1)
|
||||
|
||||
tag = state.controller.get_prole_db_version()
|
||||
# In k3s/service installs, `init_cloudnative_pg.sh` already has a robust
|
||||
# pre-flight that ensures the `prole-db` image is present/pushed (and can
|
||||
# build it if missing). Building a multi-platform image here is expensive
|
||||
# and redundant when initialization scripts are enabled.
|
||||
from installer.core.env import _normalize_cluster_env
|
||||
|
||||
env_key = (
|
||||
_normalize_cluster_env(state.inputs.get("init_cluster.cluster_env", "dev"))
|
||||
or "dev"
|
||||
)
|
||||
run_init_scripts = self._parse_bool(
|
||||
state.inputs.get("init_scripts.run_scripts", "True")
|
||||
)
|
||||
if env_key != "dev" and run_init_scripts:
|
||||
self.logger.info(
|
||||
"Skipping Docker DB image build for service env; init scripts will ensure/push the image."
|
||||
)
|
||||
return
|
||||
|
||||
if progress:
|
||||
progress("Building Prole database image...", 0.1)
|
||||
|
||||
tag = state.controller.get_prole_db_version()
|
||||
|
||||
args = ["--tag", tag]
|
||||
if env_key != "dev":
|
||||
|
||||
@ -156,6 +156,7 @@ class ServicesScreenMixin:
|
||||
("Kubectl Status", self.KUBECTL_STATUS_TAB),
|
||||
("Common Services", "init_common_services.sh"),
|
||||
("Common Services Log", "common_services_log"),
|
||||
("Cert-Manager", "init_certmgr.sh"),
|
||||
("CloudNative-PG", "init_cloudnative_pg.sh"),
|
||||
]
|
||||
scripts.append(("Prole DB Backup", "init_cnpg_backup.sh"))
|
||||
@ -318,6 +319,59 @@ class ServicesScreenMixin:
|
||||
"Skipping common services initialization because previous steps failed.\n"
|
||||
)
|
||||
|
||||
# 2. init_certmgr.sh initialize
|
||||
if overall_success:
|
||||
script = "init_certmgr.sh"
|
||||
_select_tab(script)
|
||||
self.script_consoles[script].clear()
|
||||
self.script_consoles[script].write(f"Running {script} initialize...\n")
|
||||
self.script_consoles[script].write(
|
||||
"> bash etc/init_certmgr.sh initialize\n"
|
||||
)
|
||||
|
||||
log_path = _log_path_for(script)
|
||||
self._record_install_log(log_path)
|
||||
try:
|
||||
log_fp = log_path.open("a", encoding="utf-8")
|
||||
except Exception:
|
||||
log_fp = None
|
||||
|
||||
def _certmgr_line(line):
|
||||
self.script_consoles[script].write(line)
|
||||
self._process_script_output_line(line)
|
||||
if log_fp:
|
||||
try:
|
||||
log_fp.write(line)
|
||||
log_fp.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
rc_cm = self.controller.run_script(
|
||||
script,
|
||||
args=mode_args + ["initialize"],
|
||||
env=env,
|
||||
on_line=_certmgr_line,
|
||||
)
|
||||
if log_fp:
|
||||
try:
|
||||
log_fp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if rc_cm != 0:
|
||||
self.script_consoles[script].write(
|
||||
f"\nERROR: {script} initialize failed with code {rc_cm}\n"
|
||||
)
|
||||
overall_success = False
|
||||
else:
|
||||
self.script_consoles[script].write(
|
||||
f"\n{script} completed successfully.\n"
|
||||
)
|
||||
else:
|
||||
self.script_consoles["init_certmgr.sh"].write(
|
||||
"Skipping cert-manager initialization because previous steps failed.\n"
|
||||
)
|
||||
|
||||
# 3. init_cloudnative_pg.sh initialize
|
||||
if overall_success:
|
||||
script = "init_cloudnative_pg.sh"
|
||||
|
||||
@ -5,7 +5,7 @@ metadata:
|
||||
name: prole-db
|
||||
spec:
|
||||
instances: 3
|
||||
imageName: prole-db:18-102
|
||||
imageName: myrddin.prole.org:5000/prole-db:18-105
|
||||
postgresUID: 100
|
||||
postgresGID: 101
|
||||
maxSyncReplicas: 1
|
||||
|
||||
@ -4,7 +4,7 @@ metadata:
|
||||
name: prole-db
|
||||
spec:
|
||||
instances: 3
|
||||
imageName: prole-db:18-102
|
||||
imageName: myrddin.prole.org:5000/prole-db:18-105
|
||||
postgresUID: 100
|
||||
postgresGID: 101
|
||||
maxSyncReplicas: 1
|
||||
|
||||
@ -110,12 +110,31 @@ import_image_k3s() {
|
||||
fi
|
||||
|
||||
echo "Importing $DB_MANAGER_IMAGE into k3s nodes ..."
|
||||
if ! command -v ansible-playbook >/dev/null 2>&1; then
|
||||
echo "ERROR: ansible-playbook not available; cannot import image into k3s nodes." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local playbook="$PROLE_HOME/infrastructure/playbooks/k3s_import_images.yml"
|
||||
if [[ ! -f "$playbook" ]]; then
|
||||
echo "ERROR: Ansible playbook not found: $playbook" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local images_json
|
||||
images_json=$(printf '[{"path": "%s", "image": "%s"}]' "$tar_path" "$DB_MANAGER_IMAGE")
|
||||
|
||||
local ansible_cmd=(ansible-playbook -f 1)
|
||||
if [[ -n "$vault_file" ]]; then
|
||||
ansible_cmd+=("--vault-password-file" "$vault_file")
|
||||
fi
|
||||
ansible_cmd+=(-e "{\"k3s_import_images\": $images_json}")
|
||||
ansible_cmd+=("$playbook")
|
||||
|
||||
if [[ -f "$ansible_cfg" ]]; then
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { echo "ERROR: Failed to copy image to k3s nodes." >&2; return 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_cmd[@]}" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
else
|
||||
"${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { echo "ERROR: Failed to copy image to k3s nodes." >&2; return 1; }
|
||||
"${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
"${ansible_cmd[@]}" || { echo "ERROR: Failed to import image into k3s nodes." >&2; return 1; }
|
||||
fi
|
||||
echo "Image imported to k3s nodes successfully."
|
||||
}
|
||||
|
||||
@ -383,12 +383,31 @@ build_prole_kdc_image() {
|
||||
fi
|
||||
|
||||
log "Importing authority image into k3s nodes (group: k3s_hosts)"
|
||||
if ! command -v ansible-playbook >/dev/null 2>&1; then
|
||||
err "ERROR: ansible-playbook not available; cannot import authority image to k3s nodes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local playbook="${SCRIPT_DIR}/../infrastructure/playbooks/k3s_import_images.yml"
|
||||
if [[ ! -f "$playbook" ]]; then
|
||||
err "ERROR: Ansible playbook not found: $playbook"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local images_json
|
||||
images_json=$(printf '[{"path": "%s", "image": "%s"}]' "$tar_path" "$local_tag")
|
||||
|
||||
local ansible_cmd=(ansible-playbook -f 1)
|
||||
if [[ -n "$vault_file" ]]; then
|
||||
ansible_cmd+=("--vault-password-file" "$vault_file")
|
||||
fi
|
||||
ansible_cmd+=(-e "{\"k3s_import_images\": $images_json}")
|
||||
ansible_cmd+=("$playbook")
|
||||
|
||||
if [[ -f "$ansible_cfg" ]]; then
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { err "ERROR: Failed to copy authority image to k3s nodes."; exit 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
ANSIBLE_CONFIG="$ansible_cfg" "${ansible_cmd[@]}" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
else
|
||||
"${ansible_args[@]}" -m copy -a "src=$tar_path dest=$dest mode=0644" || { err "ERROR: Failed to copy authority image to k3s nodes."; exit 1; }
|
||||
"${ansible_args[@]}" -m shell -a "ctr -n k8s.io -a /run/k3s/containerd/containerd.sock images import --no-unpack $dest" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
"${ansible_cmd[@]}" || { err "ERROR: Failed to import authority image into k3s nodes."; exit 1; }
|
||||
fi
|
||||
else
|
||||
err "ERROR: ansible not available; cannot import authority image to k3s nodes."
|
||||
|
||||
@ -1 +1 @@
|
||||
104
|
||||
105
|
||||
@ -9,43 +9,96 @@ SCRIPT_UNDER_TEST="$ETC_DIR/init_cloudnative_pg.sh"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
BIN_DIR="$TMP_DIR/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
# Mock tools
|
||||
mock_tool() {
|
||||
cat <<M_EOF > "$TMP_DIR/$1"
|
||||
cat <<M_EOF > "$BIN_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"
|
||||
chmod +x "$BIN_DIR/$1"
|
||||
}
|
||||
|
||||
# Custom kubectl mock: return minimal outputs so initialize() doesn't hang.
|
||||
cat <<K_EOF > "$BIN_DIR/kubectl"
|
||||
#!/usr/bin/env bash
|
||||
|
||||
_log_file="${TMP_DIR}/mock_calls.log"
|
||||
echo "Mocked kubectl called with \$@" >> "${_log_file}"
|
||||
|
||||
args="\$*"
|
||||
|
||||
# CNPG webhook wait: return an endpoint IP so wait passes quickly.
|
||||
if [[ "${args}" == *"get endpoints"*"cnpg-webhook-service"* ]]; then
|
||||
echo "10.42.0.10"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# CNPG pods listing (no-headers) used to detect pull errors.
|
||||
if [[ "${args}" == *"get pods"*"--no-headers"*"cnpg.io/cluster="* ]]; then
|
||||
echo "prole-db-1 1/1 Running 0 1m"
|
||||
echo "prole-db-2 1/1 Running 0 1m"
|
||||
echo "prole-db-3 1/1 Running 0 1m"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ready pod count (jsonpath) used by wait_for_cnpg_pods.
|
||||
if [[ "${args}" == *"get pods"*"jsonpath="*"Ready"*"True"* || "${args}" == *"get pods"*"jsonpath="*"status.conditions"*"Ready"* ]]; then
|
||||
printf "True\nTrue\nTrue\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Cluster instances; avoid defaulting logic differences.
|
||||
if [[ "${args}" == *"get cluster"*"jsonpath="*".spec.instances"* ]]; then
|
||||
echo "3"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
K_EOF
|
||||
chmod +x "$BIN_DIR/kubectl"
|
||||
|
||||
# Create mocks for common tools
|
||||
mock_tool kubectl
|
||||
mock_tool curl
|
||||
mock_tool docker
|
||||
mock_tool k3d
|
||||
mock_tool skopeo
|
||||
mock_tool ansible-playbook
|
||||
mock_tool tofu
|
||||
mock_tool terraform
|
||||
mock_tool ollama
|
||||
mock_tool jq
|
||||
|
||||
# Mock prole.cfg
|
||||
# Mock prole.cfg (k3s mode, but with a k3d-looking LOCAL_REGISTRY_INTERNAL to reproduce the bug).
|
||||
mkdir -p "$TMP_DIR/conf"
|
||||
cat <<C_EOF > "$TMP_DIR/conf/prole.cfg"
|
||||
[globals]
|
||||
prole.home = $PROLE_HOME
|
||||
prole.mode = k3d
|
||||
[k3s]
|
||||
server = https://localhost:6443
|
||||
token = test-token
|
||||
[User]
|
||||
NAMESPACE = test-ns
|
||||
SERVICE_NAMESPACE = test-system
|
||||
PROLE_HOME = $PROLE_HOME
|
||||
PROLE_SERVICE = $TMP_DIR/service
|
||||
|
||||
[Global]
|
||||
DEPLOYMENT_MODE = k3s
|
||||
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
|
||||
|
||||
[Docker Build]
|
||||
LOCAL_REGISTRY = localhost:5000
|
||||
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
|
||||
C_EOF
|
||||
|
||||
export PATH="$TMP_DIR:$PATH"
|
||||
mkdir -p "$TMP_DIR/service/secrets"
|
||||
printf '%s' "dummy-private-key" > "$TMP_DIR/service/secrets/admin.key"
|
||||
printf '%s' "dummy-public-key" > "$TMP_DIR/service/secrets/admin.pub"
|
||||
|
||||
export PATH="$BIN_DIR:$PATH"
|
||||
export PROLE_HOME="$PROLE_HOME"
|
||||
export PROLE_CONF="$TMP_DIR/conf"
|
||||
export NAMESPACE="test-ns"
|
||||
export PROLE_PASSWD="test-password"
|
||||
export PROLE_SERVICE="$TMP_DIR/service"
|
||||
|
||||
# Run the script
|
||||
if [[ "init_cloudnative_pg.sh" == *.py ]]; then
|
||||
@ -57,17 +110,26 @@ if [[ "init_cloudnative_pg.sh" == *.py ]]; then
|
||||
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
|
||||
# Run a path that triggers image resolution + preflight without long waits.
|
||||
bash "$SCRIPT_UNDER_TEST" --mode k3s preflight-image > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
|
||||
RC=$?
|
||||
fi
|
||||
|
||||
# Assertions: in k3s mode we must never touch k3d.
|
||||
if grep -q "Mocked k3d called" "$TMP_DIR/mock_calls.log"; then
|
||||
echo "FAILURE: init_cloudnative_pg.sh invoked k3d in k3s mode"
|
||||
sed -n '1,200p' "$TMP_DIR/mock_calls.log" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# And we must not produce k3d registry image names.
|
||||
if grep -q "k3d-prole-registry" "$TMP_DIR/stdout" "$TMP_DIR/stderr" 2>/dev/null; then
|
||||
echo "FAILURE: init_cloudnative_pg.sh used k3d registry prefix in k3s mode"
|
||||
echo "--- stdout ---"
|
||||
sed -n '1,200p' "$TMP_DIR/stdout" || true
|
||||
echo "--- stderr ---"
|
||||
sed -n '1,200p' "$TMP_DIR/stderr" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $RC -eq 0 || $RC -eq 1 || $RC -eq 2 ]]; then
|
||||
|
||||
@ -397,3 +397,34 @@ class TestRunScript:
|
||||
"args", call_kwargs[0][1] if len(call_kwargs[0]) > 1 else []
|
||||
)
|
||||
assert "--mode" not in args_passed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _step_init_scripts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestStepInitScripts:
|
||||
def test_includes_init_certmgr(self, monkeypatch):
|
||||
inst = _TestableSilentInstaller(
|
||||
inputs={
|
||||
"init_cluster.cluster_env": "service",
|
||||
"init_password.db_namespace": "default",
|
||||
"init_password.db_password": "pw",
|
||||
"kerberos_config.enabled": "false",
|
||||
"init_scripts.run_scripts": "true",
|
||||
}
|
||||
)
|
||||
|
||||
inst.controller.run_script.return_value = 0
|
||||
monkeypatch.setattr(inst, "_deployment_mode", lambda: "k3s")
|
||||
monkeypatch.setattr(inst, "_run_cmd", lambda *a, **k: 0, raising=False)
|
||||
|
||||
inst._step_init_scripts()
|
||||
|
||||
scripts = [c.args[0] for c in inst.controller.run_script.call_args_list]
|
||||
assert "init_common_services.sh" in scripts
|
||||
assert "init_certmgr.sh" in scripts
|
||||
assert scripts.index("init_certmgr.sh") == scripts.index("init_common_services.sh") + 1
|
||||
assert inst.prole_cfg_data["Initialization Scripts"]["STATUS"] == "Completed"
|
||||
assert inst._scripts_success is True
|
||||
|
||||
@ -345,6 +345,40 @@ class TestProleController:
|
||||
assert "supabase" in captured["ids"]
|
||||
assert captured["ids"].index("supabase_images_preload") < captured["ids"].index("supabase")
|
||||
|
||||
def test_silent_installer_does_not_generate_db_password_when_openbao_ref_present(
|
||||
self, tmp_path
|
||||
):
|
||||
c = ProleController(tmp_path)
|
||||
installer = ProleSilentInstaller(c)
|
||||
|
||||
openbao_ref = "${OPENBAO:kv/prole/test/db#password}"
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
installer,
|
||||
"_load_inputs_from_cfg",
|
||||
return_value={
|
||||
"init_password.db_password": openbao_ref,
|
||||
"init_password.db_password_confirm": openbao_ref,
|
||||
},
|
||||
),
|
||||
mock.patch.object(installer, "_write_cfg") as write_cfg,
|
||||
mock.patch.object(installer, "_perform_cluster_reset"),
|
||||
mock.patch.object(installer, "_close_log_file"),
|
||||
):
|
||||
|
||||
def fake_run_milestones(milestones, progress_callback=None):
|
||||
# No-op: we only care about the silent installer preflight logic.
|
||||
return None
|
||||
|
||||
c.run_milestones = fake_run_milestones
|
||||
rc = installer.run()
|
||||
|
||||
assert rc == 0
|
||||
assert installer.inputs.get("init_password.db_password") == openbao_ref
|
||||
# No early cfg write for generated password; only the normal writes.
|
||||
assert write_cfg.call_count == 2
|
||||
|
||||
|
||||
# ===== Milestone concrete subclasses (from core/milestones.py) =====
|
||||
|
||||
|
||||
@ -187,6 +187,9 @@ def test_docker_build_non_dev_env_adds_registry():
|
||||
"init_db_build.run_build": "True",
|
||||
"init_cluster.cluster_env": "prod",
|
||||
"init_cluster.k3s_server_url": "https://myrddin.prole.org:6443",
|
||||
# When init scripts are disabled, the DB build milestone is responsible
|
||||
# for pushing the image to the registry in non-dev envs.
|
||||
"init_scripts.run_scripts": "False",
|
||||
})
|
||||
milestone = DockerBuildMilestone()
|
||||
with patch("installer.core.env._normalize_cluster_env", return_value="prod"):
|
||||
@ -198,6 +201,19 @@ def test_docker_build_non_dev_env_adds_registry():
|
||||
assert "--push" in build_args
|
||||
|
||||
|
||||
def test_docker_build_non_dev_env_skips_when_init_scripts_enabled():
|
||||
state = _make_state(**{
|
||||
"init_db_build.run_build": "True",
|
||||
"init_cluster.cluster_env": "prod",
|
||||
"init_cluster.k3s_server_url": "https://myrddin.prole.org:6443",
|
||||
"init_scripts.run_scripts": "True",
|
||||
})
|
||||
milestone = DockerBuildMilestone()
|
||||
with patch("installer.core.env._normalize_cluster_env", return_value="prod"):
|
||||
milestone.execute(state)
|
||||
state.controller.run_script.assert_not_called()
|
||||
|
||||
|
||||
def test_docker_build_failed_rc():
|
||||
state = _make_state(**{"init_db_build.run_build": "True"})
|
||||
state.controller.run_script.return_value = 1
|
||||
|
||||
132
tests/installer/test_services_init_scripts.py
Normal file
132
tests/installer/test_services_init_scripts.py
Normal file
@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
def test_tk_init_scripts_runner_invokes_init_certmgr(tmp_path, monkeypatch):
|
||||
import installer.ui.screens.services as services
|
||||
from installer.ui.screens.services import ServicesScreenMixin
|
||||
|
||||
class _Var:
|
||||
def __init__(self, value=""):
|
||||
self._value = value
|
||||
|
||||
def get(self):
|
||||
return self._value
|
||||
|
||||
class _Console:
|
||||
def __init__(self):
|
||||
self.buf: list[str] = []
|
||||
|
||||
def clear(self):
|
||||
self.buf.clear()
|
||||
|
||||
def write(self, content: str):
|
||||
self.buf.append(content)
|
||||
|
||||
class _Widget:
|
||||
def winfo_exists(self):
|
||||
return False
|
||||
|
||||
def configure(self, **_kwargs):
|
||||
return None
|
||||
|
||||
class _Canvas:
|
||||
def winfo_exists(self):
|
||||
return False
|
||||
|
||||
def itemconfig(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
class _ImmediateThread:
|
||||
def __init__(self, target, daemon=True):
|
||||
self._target = target
|
||||
|
||||
def start(self):
|
||||
self._target()
|
||||
|
||||
monkeypatch.setattr(services.threading, "Thread", _ImmediateThread)
|
||||
|
||||
class _DummyScreen(ServicesScreenMixin):
|
||||
KUBECTL_STATUS_TAB = "__kubectl_status__"
|
||||
|
||||
def __init__(self):
|
||||
self._action_flags = {}
|
||||
self.controller = MagicMock()
|
||||
self.controller.run_script.return_value = 0
|
||||
|
||||
self._init_scripts_button = _Widget()
|
||||
self.bg_canvas = _Canvas()
|
||||
self._init_scripts_status_label = 0
|
||||
|
||||
self._script_tab_index = {}
|
||||
self.script_consoles = {
|
||||
"init_common_services.sh": _Console(),
|
||||
"common_services_log": _Console(),
|
||||
"init_certmgr.sh": _Console(),
|
||||
"init_cloudnative_pg.sh": _Console(),
|
||||
"init_cnpg_backup.sh": _Console(),
|
||||
"init_kong.sh": _Console(),
|
||||
"init_monitoring.sh": _Console(),
|
||||
"init_nginx_ingress.sh": _Console(),
|
||||
"init_port_forwards.sh": _Console(),
|
||||
self.KUBECTL_STATUS_TAB: _Console(),
|
||||
}
|
||||
|
||||
self.db_username = _Var("prole")
|
||||
self.db_password = _Var("pw")
|
||||
self.db_namespace = _Var("default")
|
||||
|
||||
self.kerberos_enabled = _Var(False)
|
||||
self.kerberos_realm = _Var("")
|
||||
self.kerberos_kdc = _Var("")
|
||||
self.kerberos_user = _Var("")
|
||||
self.kerberos_password = _Var("")
|
||||
self.cluster_env = _Var("prole-service-cluster")
|
||||
|
||||
self.prole_cfg_data = {"Initialization Scripts": {"STATUS": ""}}
|
||||
self._scripts_success = False
|
||||
|
||||
def safe_after(self, fn, delay: int = 0):
|
||||
return fn()
|
||||
|
||||
def _deployment_mode(self):
|
||||
return "k3s"
|
||||
|
||||
def _resolve_prole_logs_dir(self):
|
||||
return tmp_path
|
||||
|
||||
def _get_service_namespace(self):
|
||||
return "default"
|
||||
|
||||
def _sync_port_forward_mappings(self):
|
||||
return None
|
||||
|
||||
def _update_legacy_port_mapping(self):
|
||||
return None
|
||||
|
||||
def _record_install_log(self, _path):
|
||||
return None
|
||||
|
||||
def _process_script_output_line(self, _line: str):
|
||||
return None
|
||||
|
||||
def _run_cmd_capture(self, _args):
|
||||
return 0, ""
|
||||
|
||||
def _save_prole_cfg(self):
|
||||
return None
|
||||
|
||||
def update_footer(self):
|
||||
return None
|
||||
|
||||
def check_services_status_async(self):
|
||||
return None
|
||||
|
||||
screen = _DummyScreen()
|
||||
screen.run_init_scripts()
|
||||
|
||||
called_scripts = [c.args[0] for c in screen.controller.run_script.call_args_list]
|
||||
assert "init_certmgr.sh" in called_scripts
|
||||
assert screen.prole_cfg_data["Initialization Scripts"]["STATUS"] == "Completed"
|
||||
Loading…
Reference in New Issue
Block a user