6 Commits

Author SHA1 Message Date
Paul Payne
8a569a1720 Separates cluster service config from install. 2025-09-02 16:24:38 -07:00
Paul Payne
af60d0c744 Full cloud setup test run #1 fixes. 2025-09-02 02:53:52 -07:00
Paul Payne
61460b63a3 Adds more to wild-backup. 2025-08-31 15:02:35 -07:00
Paul Payne
9f302e0e29 Update backup/restore docs. 2025-08-31 15:00:00 -07:00
Paul Payne
f83763b070 Fix immich secret key ref. 2025-08-31 14:49:35 -07:00
Paul Payne
c9ab5a31d8 Discourse app fixes. 2025-08-31 14:46:42 -07:00
47 changed files with 1304 additions and 771 deletions

View File

@@ -11,33 +11,21 @@ data:
DISCOURSE_SITE_NAME: "{{ .apps.discourse.siteName }}"
DISCOURSE_USERNAME: "{{ .apps.discourse.adminUsername }}"
DISCOURSE_EMAIL: "{{ .apps.discourse.adminEmail }}"
DISCOURSE_REDIS_HOST: "{{ .apps.discourse.redisHostname }}"
DISCOURSE_REDIS_PORT_NUMBER: "6379"
DISCOURSE_DATABASE_HOST: "{{ .apps.discourse.dbHostname }}"
DISCOURSE_DATABASE_PORT_NUMBER: "5432"
DISCOURSE_DATABASE_NAME: "{{ .apps.discourse.dbName }}"
DISCOURSE_DATABASE_USER: "{{ .apps.discourse.dbUsername }}"
# DISCOURSE_SMTP_ADDRESS: "{{ .apps.discourse.smtp.host }}"
# DISCOURSE_SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
# DISCOURSE_SMTP_USER_NAME: "{{ .apps.discourse.smtp.user }}"
# DISCOURSE_SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
# DISCOURSE_SMTP_AUTHENTICATION: "login"
# Bitnami specific environment variables (diverges from the original)
# https://techdocs.broadcom.com/us/en/vmware-tanzu/bitnami-secure-images/bitnami-secure-images/services/bsi-app-doc/apps-containers-discourse-index.html
DISCOURSE_SMTP_HOST: "{{ .apps.discourse.smtp.host }}"
DISCOURSE_SMTP_PORT_NUMBER: "{{ .apps.discourse.smtp.port }}"
DISCOURSE_SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
DISCOURSE_SMTP_USER: "{{ .apps.discourse.smtp.user }}"
DISCOURSE_SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
DISCOURSE_SMTP_AUTH: "login"
DISCOURSE_SMTP_PROTOCOL: "tls"
DISCOURSE_SMTP_AUTH: "login"
DISCOURSE_PRECOMPILE_ASSETS: "false"
# SMTP_HOST: "{{ .apps.discourse.smtp.host }}"
# SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
# SMTP_USER_NAME: "{{ .apps.discourse.smtp.user }}"
# SMTP_TLS: "{{ .apps.discourse.smtp.tls }}"
# SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
# SMTP_AUTHENTICATION: "login"
# DISCOURSE_PRECOMPILE_ASSETS: "false"
# DISCOURSE_SKIP_INSTALL: "no"
# DISCOURSE_SKIP_BOOTSTRAP: "yes"

View File

@@ -37,7 +37,7 @@ spec:
initContainers:
containers:
- name: discourse
image: { { .apps.discourse.image } }
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0
imagePullPolicy: "IfNotPresent"
securityContext:
allowPrivilegeEscalation: false
@@ -85,7 +85,7 @@ spec:
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.redisPassword
key: apps.redis.password
- name: DISCOURSE_SECRET_KEY_BASE
valueFrom:
secretKeyRef:
@@ -139,7 +139,7 @@ spec:
mountPath: /bitnami/discourse
subPath: discourse
- name: sidekiq
image: { { .apps.discourse.sidekiqImage } }
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0
imagePullPolicy: "IfNotPresent"
securityContext:
allowPrivilegeEscalation: false
@@ -182,7 +182,7 @@ spec:
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.redisPassword
key: apps.redis.password
- name: DISCOURSE_SECRET_KEY_BASE
valueFrom:
secretKeyRef:

View File

@@ -6,8 +6,6 @@ requires:
- name: postgres
- name: redis
defaultConfig:
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0
sidekiqImage: docker.io/bitnami/discourse:3.4.7-debian-12-r0
timezone: UTC
port: 8080
storage: 10Gi
@@ -32,7 +30,7 @@ requiredSecrets:
- apps.discourse.adminPassword
- apps.discourse.dbPassword
- apps.discourse.dbUrl
- apps.discourse.redisPassword
- apps.redis.password
- apps.discourse.secretKeyBase
- apps.discourse.smtpPassword
- apps.postgres.password

View File

@@ -52,7 +52,7 @@ spec:
- name: POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
name: immich-secrets
key: apps.postgres.password
- name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}"

View File

@@ -4,6 +4,72 @@
set -e
set -o pipefail
# Parse command line flags
BACKUP_HOME=true
BACKUP_APPS=true
BACKUP_CLUSTER=true
show_help() {
echo "Usage: $0 [OPTIONS]"
echo "Backup components of your wild-cloud infrastructure"
echo ""
echo "Options:"
echo " --home-only Backup only WC_HOME (wild-cloud configuration)"
echo " --apps-only Backup only applications (databases and PVCs)"
echo " --cluster-only Backup only Kubernetes cluster resources"
echo " --no-home Skip WC_HOME backup"
echo " --no-apps Skip application backups"
echo " --no-cluster Skip cluster resource backup"
echo " -h, --help Show this help message"
echo ""
echo "Default: Backup all components (home, apps, cluster)"
}
# Process command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--home-only)
BACKUP_HOME=true
BACKUP_APPS=false
BACKUP_CLUSTER=false
shift
;;
--apps-only)
BACKUP_HOME=false
BACKUP_APPS=true
BACKUP_CLUSTER=false
shift
;;
--cluster-only)
BACKUP_HOME=false
BACKUP_APPS=false
BACKUP_CLUSTER=true
shift
;;
--no-home)
BACKUP_HOME=false
shift
;;
--no-apps)
BACKUP_APPS=false
shift
;;
--no-cluster)
BACKUP_CLUSTER=false
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
echo "WC_ROOT is not set."
@@ -46,18 +112,25 @@ else
echo "Repository initialized successfully."
fi
# Backup entire WC_HOME.
restic --verbose --tag wild-cloud --tag wc-home --tag "$(date +%Y-%m-%d)" backup $WC_HOME
# TODO: Ignore wild cloud cache?
# Backup entire WC_HOME
if [ "$BACKUP_HOME" = true ]; then
echo "Backing up WC_HOME..."
restic --verbose --tag wild-cloud --tag wc-home --tag "$(date +%Y-%m-%d)" backup $WC_HOME
echo "WC_HOME backup completed."
# TODO: Ignore wild cloud cache?
else
echo "Skipping WC_HOME backup."
fi
mkdir -p "$STAGING_DIR"
# Run backup for all apps at once
echo "Running backup for all apps..."
wild-app-backup --all
if [ "$BACKUP_APPS" = true ]; then
echo "Running backup for all apps..."
wild-app-backup --all
# Upload each app's backup to restic individually
for app_dir in "$STAGING_DIR"/apps/*; do
# Upload each app's backup to restic individually
for app_dir in "$STAGING_DIR"/apps/*; do
if [ ! -d "$app_dir" ]; then
continue
fi
@@ -65,14 +138,108 @@ for app_dir in "$STAGING_DIR"/apps/*; do
echo "Uploading backup for app: $app"
restic --verbose --tag wild-cloud --tag "$app" --tag "$(date +%Y-%m-%d)" backup "$app_dir"
echo "Backup for app '$app' completed."
done
done
else
echo "Skipping application backups."
fi
# Back up Kubernetes resources
# kubectl get all -A -o yaml > "$BACKUP_DIR/all-resources.yaml"
# kubectl get secrets -A -o yaml > "$BACKUP_DIR/secrets.yaml"
# kubectl get configmaps -A -o yaml > "$BACKUP_DIR/configmaps.yaml"
# --- etcd Backup Function ----------------------------------------------------
backup_etcd() {
local cluster_backup_dir="$1"
local etcd_backup_file="$cluster_backup_dir/etcd-snapshot.db"
# Back up persistent volumes
# TODO: Add logic to back up persistent volume data
echo "Creating etcd snapshot..."
# For Talos, we use talosctl to create etcd snapshots
if command -v talosctl >/dev/null 2>&1; then
# Try to get etcd snapshot via talosctl (works for Talos clusters)
local control_plane_nodes
control_plane_nodes=$(kubectl get nodes -l node-role.kubernetes.io/control-plane -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}' | tr ' ' '\n' | head -1)
if [[ -n "$control_plane_nodes" ]]; then
echo "Using talosctl to backup etcd from control plane node: $control_plane_nodes"
if talosctl --nodes "$control_plane_nodes" etcd snapshot "$etcd_backup_file"; then
echo " etcd backup created: $etcd_backup_file"
return 0
else
echo " talosctl etcd snapshot failed, trying alternative method..."
fi
else
echo " No control plane nodes found for talosctl method"
fi
fi
# Alternative: Try to backup via etcd pod if available
local etcd_pod
etcd_pod=$(kubectl get pods -n kube-system -l component=etcd -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$etcd_pod" ]]; then
echo "Using etcd pod: $etcd_pod"
# Create snapshot using etcdctl inside the etcd pod
if kubectl exec -n kube-system "$etcd_pod" -- etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /tmp/etcd-snapshot.db; then
# Copy snapshot out of pod
kubectl cp -n kube-system "$etcd_pod:/tmp/etcd-snapshot.db" "$etcd_backup_file"
# Clean up temporary file in pod
kubectl exec -n kube-system "$etcd_pod" -- rm -f /tmp/etcd-snapshot.db
echo " etcd backup created: $etcd_backup_file"
return 0
else
echo " etcd pod snapshot failed"
fi
else
echo " No etcd pod found in kube-system namespace"
fi
# Final fallback: Try direct etcdctl if available on local system
if command -v etcdctl >/dev/null 2>&1; then
echo "Attempting local etcdctl backup..."
# This would need proper certificates and endpoints configured
echo " Local etcdctl backup not implemented (requires certificate configuration)"
fi
echo " Warning: Could not create etcd backup - no working method found"
echo " Consider installing talosctl or ensuring etcd pods are accessible"
return 1
}
# Back up Kubernetes cluster resources
if [ "$BACKUP_CLUSTER" = true ]; then
echo "Backing up Kubernetes cluster resources..."
CLUSTER_BACKUP_DIR="$STAGING_DIR/cluster"
# Clean up any existing cluster backup files
if [[ -d "$CLUSTER_BACKUP_DIR" ]]; then
echo "Cleaning up existing cluster backup files..."
rm -rf "$CLUSTER_BACKUP_DIR"
fi
mkdir -p "$CLUSTER_BACKUP_DIR"
kubectl get all -A -o yaml > "$CLUSTER_BACKUP_DIR/all-resources.yaml"
kubectl get secrets -A -o yaml > "$CLUSTER_BACKUP_DIR/secrets.yaml"
kubectl get configmaps -A -o yaml > "$CLUSTER_BACKUP_DIR/configmaps.yaml"
kubectl get persistentvolumes -o yaml > "$CLUSTER_BACKUP_DIR/persistentvolumes.yaml"
kubectl get persistentvolumeclaims -A -o yaml > "$CLUSTER_BACKUP_DIR/persistentvolumeclaims.yaml"
kubectl get storageclasses -o yaml > "$CLUSTER_BACKUP_DIR/storageclasses.yaml"
echo "Backing up etcd..."
backup_etcd "$CLUSTER_BACKUP_DIR"
echo "Cluster resources backed up to $CLUSTER_BACKUP_DIR"
# Upload cluster backup to restic
echo "Uploading cluster backup to restic..."
restic --verbose --tag wild-cloud --tag cluster --tag "$(date +%Y-%m-%d)" backup "$CLUSTER_BACKUP_DIR"
echo "Cluster backup completed."
else
echo "Skipping cluster backup."
fi
echo "Backup completed: $BACKUP_DIR"

View File

@@ -58,10 +58,8 @@ fi
print_header "Talos Cluster Configuration Generation"
# Ensure required directories exist
NODE_SETUP_DIR="${WC_HOME}/setup/cluster-nodes"
# Check if generated directory already exists and has content
NODE_SETUP_DIR="${WC_HOME}/setup/cluster-nodes"
if [ -d "${NODE_SETUP_DIR}/generated" ] && [ "$(ls -A "${NODE_SETUP_DIR}/generated" 2>/dev/null)" ] && [ "$FORCE" = false ]; then
print_success "Cluster configuration already exists in ${NODE_SETUP_DIR}/generated/"
print_info "Skipping cluster configuration generation"
@@ -77,8 +75,6 @@ if [ -d "${NODE_SETUP_DIR}/generated" ]; then
rm -rf "${NODE_SETUP_DIR}/generated"
fi
mkdir -p "${NODE_SETUP_DIR}/generated"
talosctl gen secrets
print_info "New secrets will be generated in ${NODE_SETUP_DIR}/generated/"
# Ensure we have the configuration we need.
@@ -94,9 +90,8 @@ print_info "Cluster name: $CLUSTER_NAME"
print_info "Control plane endpoint: https://$VIP:6443"
cd "${NODE_SETUP_DIR}/generated"
talosctl gen secrets
talosctl gen config --with-secrets secrets.yaml "$CLUSTER_NAME" "https://$VIP:6443"
cd - >/dev/null
# Verify generated files
print_success "Cluster configuration generation completed!"

View File

@@ -51,76 +51,32 @@ else
init_wild_env
fi
# Check for required configuration
if [ -z "$(wild-config "cluster.nodes.talos.version")" ] || [ -z "$(wild-config "cluster.nodes.talos.schematicId")" ]; then
print_header "Talos Configuration Required"
print_error "Missing required Talos configuration"
print_info "Please run 'wild-setup' first to configure your cluster"
print_info "Or set the required configuration manually:"
print_info " wild-config-set cluster.nodes.talos.version v1.10.4"
print_info " wild-config-set cluster.nodes.talos.schematicId YOUR_SCHEMATIC_ID"
exit 1
fi
# =============================================================================
# INSTALLER IMAGE GENERATION AND ASSET DOWNLOADING
# =============================================================================
print_header "Talos Installer Image Generation and Asset Download"
print_header "Talos asset download"
# Get Talos version and schematic ID from config
TALOS_VERSION=$(wild-config cluster.nodes.talos.version)
SCHEMATIC_ID=$(wild-config cluster.nodes.talos.schematicId)
# Talos version
prompt_if_unset_config "cluster.nodes.talos.version" "Talos version" "v1.11.0"
TALOS_VERSION=$(wild-config "cluster.nodes.talos.version")
# Talos schematic ID
prompt_if_unset_config "cluster.nodes.talos.schematicId" "Talos schematic ID" "56774e0894c8a3a3a9834a2aea65f24163cacf9506abbcbdc3ba135eaca4953f"
SCHEMATIC_ID=$(wild-config "cluster.nodes.talos.schematicId")
print_info "Creating custom Talos installer image..."
print_info "Talos version: $TALOS_VERSION"
# Validate schematic ID
if [ -z "$SCHEMATIC_ID" ] || [ "$SCHEMATIC_ID" = "null" ]; then
print_error "No schematic ID found in config.yaml"
print_info "Please run 'wild-setup' first to configure your cluster"
exit 1
fi
print_info "Schematic ID: $SCHEMATIC_ID"
if [ -f "${WC_HOME}/config.yaml" ] && yq eval '.cluster.nodes.talos.schematic.customization.systemExtensions.officialExtensions' "${WC_HOME}/config.yaml" >/dev/null 2>&1; then
echo ""
print_info "Schematic includes:"
yq eval '.cluster.nodes.talos.schematic.customization.systemExtensions.officialExtensions[]' "${WC_HOME}/config.yaml" | sed 's/^/ - /' || true
echo ""
fi
# Generate installer image URL
INSTALLER_URL="factory.talos.dev/metal-installer/$SCHEMATIC_ID:$TALOS_VERSION"
print_success "Custom installer image URL generated!"
echo ""
print_info "Installer URL: $INSTALLER_URL"
# =============================================================================
# ASSET DOWNLOADING AND CACHING
# =============================================================================
print_header "Downloading and Caching PXE Boot Assets"
# Create cache directories organized by schematic ID
CACHE_DIR="${WC_HOME}/.wildcloud"
SCHEMATIC_CACHE_DIR="${CACHE_DIR}/node-boot-assets/${SCHEMATIC_ID}"
PXE_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/pxe"
IPXE_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/ipxe"
ISO_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/iso"
mkdir -p "$PXE_CACHE_DIR/amd64"
mkdir -p "$IPXE_CACHE_DIR"
mkdir -p "$ISO_CACHE_DIR"
# Download Talos kernel and initramfs for PXE boot
print_info "Downloading Talos PXE assets..."
KERNEL_URL="https://pxe.factory.talos.dev/image/${SCHEMATIC_ID}/${TALOS_VERSION}/kernel-amd64"
INITRAMFS_URL="https://pxe.factory.talos.dev/image/${SCHEMATIC_ID}/${TALOS_VERSION}/initramfs-amd64.xz"
KERNEL_PATH="${PXE_CACHE_DIR}/amd64/vmlinuz"
INITRAMFS_PATH="${PXE_CACHE_DIR}/amd64/initramfs.xz"
print_header "Downloading and caching boot assets"
# Function to download with progress
download_asset() {
@@ -129,17 +85,19 @@ download_asset() {
local description="$3"
if [ -f "$path" ]; then
print_info "$description already cached at $path"
print_success "$description already cached at $path"
return 0
fi
print_info "Downloading $description..."
print_info "URL: $url"
if command -v wget >/dev/null 2>&1; then
wget --progress=bar:force -O "$path" "$url"
elif command -v curl >/dev/null 2>&1; then
curl -L --progress-bar -o "$path" "$url"
if command -v curl >/dev/null 2>&1; then
curl -L -o "$path" "$url" \
--progress-bar \
--write-out "✓ Downloaded %{size_download} bytes at %{speed_download} B/s\n"
elif command -v wget >/dev/null 2>&1; then
wget --progress=bar:force:noscroll -O "$path" "$url"
else
print_error "Neither wget nor curl is available for downloading"
return 1
@@ -153,42 +111,51 @@ download_asset() {
fi
print_success "$description downloaded successfully"
echo
}
# Download Talos PXE assets
CACHE_DIR="${WC_HOME}/.wildcloud"
SCHEMATIC_CACHE_DIR="${CACHE_DIR}/node-boot-assets/${SCHEMATIC_ID}"
PXE_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/pxe"
IPXE_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/ipxe"
ISO_CACHE_DIR="${SCHEMATIC_CACHE_DIR}/iso"
mkdir -p "$PXE_CACHE_DIR/amd64"
mkdir -p "$IPXE_CACHE_DIR"
mkdir -p "$ISO_CACHE_DIR"
# Download Talos kernel and initramfs for PXE boot
KERNEL_URL="https://pxe.factory.talos.dev/image/${SCHEMATIC_ID}/${TALOS_VERSION}/kernel-amd64"
KERNEL_PATH="${PXE_CACHE_DIR}/amd64/vmlinuz"
download_asset "$KERNEL_URL" "$KERNEL_PATH" "Talos kernel"
INITRAMFS_URL="https://pxe.factory.talos.dev/image/${SCHEMATIC_ID}/${TALOS_VERSION}/initramfs-amd64.xz"
INITRAMFS_PATH="${PXE_CACHE_DIR}/amd64/initramfs.xz"
download_asset "$INITRAMFS_URL" "$INITRAMFS_PATH" "Talos initramfs"
# Download iPXE bootloader files
print_info "Downloading iPXE bootloader assets..."
download_asset "http://boot.ipxe.org/ipxe.efi" "${IPXE_CACHE_DIR}/ipxe.efi" "iPXE EFI bootloader"
download_asset "http://boot.ipxe.org/undionly.kpxe" "${IPXE_CACHE_DIR}/undionly.kpxe" "iPXE BIOS bootloader"
download_asset "http://boot.ipxe.org/arm64-efi/ipxe.efi" "${IPXE_CACHE_DIR}/ipxe-arm64.efi" "iPXE ARM64 EFI bootloader"
# Download Talos ISO
print_info "Downloading Talos ISO..."
ISO_URL="https://factory.talos.dev/image/${SCHEMATIC_ID}/${TALOS_VERSION}/metal-amd64.iso"
ISO_FILENAME="talos-${TALOS_VERSION}-metal-amd64.iso"
ISO_PATH="${ISO_CACHE_DIR}/${ISO_FILENAME}"
ISO_PATH="${ISO_CACHE_DIR}/talos-${TALOS_VERSION}-metal-amd64.iso"
download_asset "$ISO_URL" "$ISO_PATH" "Talos ISO"
echo ""
print_success "All assets downloaded and cached!"
echo ""
print_info "Cached assets for schematic $SCHEMATIC_ID:"
echo " Talos kernel: $KERNEL_PATH"
echo " Talos initramfs: $INITRAMFS_PATH"
echo " Talos ISO: $ISO_PATH"
echo " iPXE EFI: ${IPXE_CACHE_DIR}/ipxe.efi"
echo " iPXE BIOS: ${IPXE_CACHE_DIR}/undionly.kpxe"
echo " iPXE ARM64: ${IPXE_CACHE_DIR}/ipxe-arm64.efi"
print_header "Summary"
print_success "Cached assets for schematic $SCHEMATIC_ID:"
echo "- Talos kernel: $KERNEL_PATH"
echo "- Talos initramfs: $INITRAMFS_PATH"
echo "- Talos ISO: $ISO_PATH"
echo "- iPXE EFI: ${IPXE_CACHE_DIR}/ipxe.efi"
echo "- iPXE BIOS: ${IPXE_CACHE_DIR}/undionly.kpxe"
echo "- iPXE ARM64: ${IPXE_CACHE_DIR}/ipxe-arm64.efi"
echo ""
print_info "Cache location: $SCHEMATIC_CACHE_DIR"
echo ""
print_info "Use these assets for:"
echo " - PXE boot: Use kernel and initramfs from cache"
echo " - USB creation: Use ISO file for dd or imaging tools"
echo "- PXE boot: Use kernel and initramfs from cache"
echo "- USB creation: Use ISO file for dd or imaging tools"
echo " Example: sudo dd if=$ISO_PATH of=/dev/sdX bs=4M status=progress"
echo " - Custom installer: https://$INSTALLER_URL"
echo "- Custom installer: https://$INSTALLER_URL"
echo ""
print_success "Installer image generation and asset caching completed!"

View File

@@ -96,7 +96,7 @@ else
init_wild_env
fi
print_header "Talos Node Configuration Application"
print_header "Talos node configuration"
# Check if the specified node is registered
NODE_INTERFACE=$(yq eval ".cluster.nodes.active.\"${NODE_NAME}\".interface" "${WC_HOME}/config.yaml" 2>/dev/null)
@@ -156,10 +156,7 @@ PATCH_FILE="${NODE_SETUP_DIR}/patch/${NODE_NAME}.yaml"
# Check if patch file exists
if [ ! -f "$PATCH_FILE" ]; then
print_error "Patch file not found: $PATCH_FILE"
print_info "Generate the patch file first:"
print_info " wild-cluster-node-patch-generate $NODE_NAME"
exit 1
wild-cluster-node-patch-generate "$NODE_NAME"
fi
# Determine base config file

View File

@@ -0,0 +1,124 @@
#\!/bin/bash
set -e
set -o pipefail
# Usage function
usage() {
echo "Usage: wild-cluster-services-configure [options] [service...]"
echo ""
echo "Compile service templates with configuration"
echo ""
echo "Arguments:"
echo " service Specific service(s) to compile (optional)"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " wild-cluster-services-configure # Compile all services"
echo " wild-cluster-services-configure metallb traefik # Compile specific services"
echo ""
echo "Available services:"
echo " metallb, longhorn, traefik, coredns, cert-manager,"
echo " externaldns, kubernetes-dashboard, nfs, docker-registry"
}
# Parse arguments
DRY_RUN=false
LIST_SERVICES=false
SPECIFIC_SERVICES=()
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--dry-run)
DRY_RUN=true
shift
;;
-*)
echo "Unknown option $1"
usage
exit 1
;;
*)
SPECIFIC_SERVICES+=("$1")
shift
;;
esac
done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
CLUSTER_SETUP_DIR="${WC_HOME}/setup/cluster-services"
# Check if cluster setup directory exists
if [ ! -d "$CLUSTER_SETUP_DIR" ]; then
print_error "Cluster services setup directory not found: $CLUSTER_SETUP_DIR"
print_info "Run 'wild-cluster-services-generate' first to generate setup files"
exit 1
fi
# =============================================================================
# CLUSTER SERVICES TEMPLATE COMPILATION
# =============================================================================
print_header "Cluster services template compilation"
# Get list of services to compile
if [ ${#SPECIFIC_SERVICES[@]} -gt 0 ]; then
SERVICES_TO_INSTALL=("${SPECIFIC_SERVICES[@]}")
print_info "Compiling specific services: ${SERVICES_TO_INSTALL[*]}"
else
# Compile all available services in a specific order for dependencies
SERVICES_TO_INSTALL=(
"metallb"
"longhorn"
"traefik"
"coredns"
"cert-manager"
"externaldns"
"kubernetes-dashboard"
"nfs"
"docker-registry"
)
print_info "Installing all available services"
fi
print_info "Services to compile: ${SERVICES_TO_INSTALL[*]}"
# Compile services
cd "$CLUSTER_SETUP_DIR"
INSTALLED_COUNT=0
FAILED_COUNT=0
for service in "${SERVICES_TO_INSTALL[@]}"; do
print_info "Compiling $service"
service_dir="$CLUSTER_SETUP_DIR/$service"
source_service_dir="$service_dir/kustomize.template"
dest_service_dir="$service_dir/kustomize"
# Run configuration to make sure we have the template values we need.
config_script="$service_dir/configure.sh"
if [ -f "$config_script" ]; then
source "$config_script"
fi
wild-compile-template-dir --clean "$source_service_dir" "$dest_service_dir"
echo ""
done
cd - >/dev/null
print_success "Successfully compiled: $INSTALLED_COUNT services"

148
bin/wild-cluster-services-fetch Executable file
View File

@@ -0,0 +1,148 @@
#\!/bin/bash
set -e
set -o pipefail
# Usage function
usage() {
echo "Usage: wild-cluster-services-fetch [options]"
echo ""
echo "Fetch cluster services setup files from the repository."
echo ""
echo "Arguments:"
echo " service Specific service(s) to install (optional)"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --force Force fetching even if files exist"
echo ""
echo "Examples:"
echo " wild-cluster-services-fetch # Fetch all services"
echo " wild-cluster-services-fetch metallb traefik # Fetch specific services"
echo ""
echo "Available services:"
echo " metallb, longhorn, traefik, coredns, cert-manager,"
echo " externaldns, kubernetes-dashboard, nfs, docker-registry"
}
# Parse arguments
FORCE=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--force)
FORCE=true
shift
;;
-*)
echo "Unknown option $1"
usage
exit 1
;;
*)
echo "Unexpected argument: $1"
usage
exit 1
;;
esac
done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
print_header "Fetching cluster services templates"
SOURCE_DIR="${WC_ROOT}/setup/cluster-services"
DEST_DIR="${WC_HOME}/setup/cluster-services"
# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
print_error "Cluster setup source directory not found: $SOURCE_DIR"
print_info "Make sure the wild-cloud repository is properly set up"
exit 1
fi
# Check if destination already exists
if [ -d "$DEST_DIR" ] && [ "$FORCE" = false ]; then
print_warning "Cluster setup directory already exists: $DEST_DIR"
read -p "Overwrite existing files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
FORCE=true
fi
else
mkdir -p "$DEST_DIR"
fi
# Copy README
if [ ! -f "${WC_HOME}/setup/README.md" ]; then
cp "${WC_ROOT}/setup/README.md" "${WC_HOME}/setup/README.md"
fi
# Get list of services to install
if [ ${#SPECIFIC_SERVICES[@]} -gt 0 ]; then
SERVICES_TO_INSTALL=("${SPECIFIC_SERVICES[@]}")
print_info "Fetching specific services: ${SERVICES_TO_INSTALL[*]}"
else
# Install all available services in a specific order for dependencies
SERVICES_TO_INSTALL=(
"metallb"
"longhorn"
"traefik"
"coredns"
"cert-manager"
"externaldns"
"kubernetes-dashboard"
"nfs"
"docker-registry"
)
print_info "Fetching all available services."
fi
for service in "${SERVICES_TO_INSTALL[@]}"; do
SERVICE_SOURCE_DIR="$SOURCE_DIR/$service"
SERVICE_DEST_DIR="$DEST_DIR/$service"
TEMPLATE_SOURCE_DIR="$SERVICE_SOURCE_DIR/kustomize.template"
TEMPLATE_DEST_DIR="$SERVICE_DEST_DIR/kustomize.template"
if [ ! -d "$TEMPLATE_SOURCE_DIR" ]; then
print_error "Source directory not found: $TEMPLATE_SOURCE_DIR"
continue
fi
if $FORCE && [ -d "$TEMPLATE_DEST_DIR" ]; then
print_info "Removing existing $service templates in: $TEMPLATE_DEST_DIR"
rm -rf "$TEMPLATE_DEST_DIR"
elif [ -d "$TEMPLATE_DEST_DIR" ]; then
print_info "Files already exist for $service, skipping (use --force to overwrite)."
continue
fi
mkdir -p "$SERVICE_DEST_DIR"
mkdir -p "$TEMPLATE_DEST_DIR"
cp -f "$SERVICE_SOURCE_DIR/README.md" "$SERVICE_DEST_DIR/"
if [ -f "$SERVICE_SOURCE_DIR/configure.sh" ]; then
cp -f "$SERVICE_SOURCE_DIR/configure.sh" "$SERVICE_DEST_DIR/"
fi
if [ -f "$SERVICE_SOURCE_DIR/install.sh" ]; then
cp -f "$SERVICE_SOURCE_DIR/install.sh" "$SERVICE_DEST_DIR/"
fi
if [ -d "$TEMPLATE_SOURCE_DIR" ]; then
cp -r "$TEMPLATE_SOURCE_DIR/"* "$TEMPLATE_DEST_DIR/"
fi
print_success "Fetched $service templates."
done

View File

@@ -1,208 +0,0 @@
#\!/bin/bash
set -e
set -o pipefail
# Usage function
usage() {
echo "Usage: wild-cluster-services-generate [options]"
echo ""
echo "Generate cluster services setup files by compiling templates."
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --force Force regeneration even if files exist"
echo ""
echo "This script will:"
echo " - Copy cluster service templates from WC_ROOT to WC_HOME"
echo " - Compile all templates with current configuration"
echo " - Prepare services for installation"
echo ""
echo "Requirements:"
echo " - Must be run from a wild-cloud directory"
echo " - Basic cluster configuration must be completed"
echo " - Service configuration (DNS, storage, etc.) must be completed"
}
# Parse arguments
FORCE=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--force)
FORCE=true
shift
;;
-*)
echo "Unknown option $1"
usage
exit 1
;;
*)
echo "Unexpected argument: $1"
usage
exit 1
;;
esac
done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
# =============================================================================
# CLUSTER SERVICES SETUP GENERATION
# =============================================================================
print_header "Cluster Services Setup Generation"
SOURCE_DIR="${WC_ROOT}/setup/cluster-services"
DEST_DIR="${WC_HOME}/setup/cluster-services"
# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
print_error "Cluster setup source directory not found: $SOURCE_DIR"
print_info "Make sure the wild-cloud repository is properly set up"
exit 1
fi
# Check if destination already exists
if [ -d "$DEST_DIR" ] && [ "$FORCE" = false ]; then
print_warning "Cluster setup directory already exists: $DEST_DIR"
read -p "Overwrite existing files? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Skipping cluster services generation"
exit 0
fi
print_info "Regenerating cluster setup files..."
rm -rf "$DEST_DIR"
elif [ "$FORCE" = true ] && [ -d "$DEST_DIR" ]; then
print_info "Force regeneration enabled, removing existing files..."
rm -rf "$DEST_DIR"
fi
# Copy and compile cluster setup files
print_info "Copying and compiling cluster setup files from repository..."
mkdir -p "${WC_HOME}/setup"
# Copy README if it doesn't exist
if [ ! -f "${WC_HOME}/setup/README.md" ]; then
cp "${WC_ROOT}/setup/README.md" "${WC_HOME}/setup/README.md"
fi
# Create destination directory
mkdir -p "$DEST_DIR"
# First, copy root-level files from setup/cluster/ (install-all.sh, get_helm.sh, etc.)
print_info "Copying root-level cluster setup files..."
for item in "$SOURCE_DIR"/*; do
if [ -f "$item" ]; then
item_name=$(basename "$item")
print_info " Copying: ${item_name}"
cp "$item" "$DEST_DIR/$item_name"
fi
done
# Then, process each service directory in the source
print_info "Processing service directories..."
for service_dir in "$SOURCE_DIR"/*; do
if [ ! -d "$service_dir" ]; then
continue
fi
service_name=$(basename "$service_dir")
dest_service_dir="$DEST_DIR/$service_name"
print_info "Processing service: $service_name"
# Create destination service directory
mkdir -p "$dest_service_dir"
# Copy all files except kustomize.template directory
for item in "$service_dir"/*; do
item_name=$(basename "$item")
if [ "$item_name" = "kustomize.template" ]; then
# Compile kustomize.template to kustomize directory
if [ -d "$item" ]; then
print_info " Compiling kustomize templates for $service_name"
wild-compile-template-dir --clean "$item" "$dest_service_dir/kustomize"
fi
else
# Copy other files as-is (install.sh, README.md, etc.)
if [ -f "$item" ]; then
# Compile individual template files
if grep -q "{{" "$item" 2>/dev/null; then
print_info " Compiling: ${item_name}"
wild-compile-template < "$item" > "$dest_service_dir/$item_name"
else
cp "$item" "$dest_service_dir/$item_name"
fi
elif [ -d "$item" ]; then
cp -r "$item" "$dest_service_dir/"
fi
fi
done
done
print_success "Cluster setup files copied and compiled"
# Verify required configuration
print_info "Verifying service configuration..."
MISSING_CONFIG=()
# Check essential configuration values
if [ -z "$(wild-config cluster.name 2>/dev/null)" ]; then
MISSING_CONFIG+=("cluster.name")
fi
if [ -z "$(wild-config cloud.domain 2>/dev/null)" ]; then
MISSING_CONFIG+=("cloud.domain")
fi
if [ -z "$(wild-config cluster.ipAddressPool 2>/dev/null)" ]; then
MISSING_CONFIG+=("cluster.ipAddressPool")
fi
if [ -z "$(wild-config operator.email 2>/dev/null)" ]; then
MISSING_CONFIG+=("operator.email")
fi
if [ ${#MISSING_CONFIG[@]} -gt 0 ]; then
print_warning "Some required configuration values are missing:"
for config in "${MISSING_CONFIG[@]}"; do
print_warning " - $config"
done
print_info "Run 'wild-setup' to complete the configuration"
fi
print_success "Cluster services setup generation completed!"
echo ""
print_info "Generated setup directory: $DEST_DIR"
echo ""
print_info "Available services:"
for service_dir in "$DEST_DIR"/*; do
if [ -d "$service_dir" ] && [ -f "$service_dir/install.sh" ]; then
service_name=$(basename "$service_dir")
print_info " - $service_name"
fi
done
echo ""
print_info "Next steps:"
echo " 1. Review the generated configuration files in $DEST_DIR"
echo " 2. Make sure your cluster is running and kubectl is configured"
echo " 3. Install services with: wild-cluster-services-up"
echo " 4. Or install individual services by running their install.sh scripts"
print_success "Ready for cluster services installation!"

View File

@@ -14,22 +14,15 @@ usage() {
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --list List available services"
echo " --dry-run Show what would be installed without running"
echo ""
echo "Examples:"
echo " wild-cluster-services-up # Install all services"
echo " wild-cluster-services-up metallb traefik # Install specific services"
echo " wild-cluster-services-up --list # List available services"
echo ""
echo "Available services (when setup files exist):"
echo "Available services:"
echo " metallb, longhorn, traefik, coredns, cert-manager,"
echo " externaldns, kubernetes-dashboard, nfs, docker-registry"
echo ""
echo "Requirements:"
echo " - Must be run from a wild-cloud directory"
echo " - Cluster services must be generated first (wild-cluster-services-generate)"
echo " - Kubernetes cluster must be running and kubectl configured"
}
# Parse arguments
@@ -43,10 +36,6 @@ while [[ $# -gt 0 ]]; do
usage
exit 0
;;
--list)
LIST_SERVICES=true
shift
;;
--dry-run)
DRY_RUN=true
shift
@@ -81,43 +70,11 @@ if [ ! -d "$CLUSTER_SETUP_DIR" ]; then
exit 1
fi
# Function to get available services
get_available_services() {
local services=()
for service_dir in "$CLUSTER_SETUP_DIR"/*; do
if [ -d "$service_dir" ] && [ -f "$service_dir/install.sh" ]; then
services+=($(basename "$service_dir"))
fi
done
echo "${services[@]}"
}
# List services if requested
if [ "$LIST_SERVICES" = true ]; then
print_header "Available Cluster Services"
AVAILABLE_SERVICES=($(get_available_services))
if [ ${#AVAILABLE_SERVICES[@]} -eq 0 ]; then
print_warning "No services found in $CLUSTER_SETUP_DIR"
print_info "Run 'wild-cluster-services-generate' first"
else
print_info "Services available for installation:"
for service in "${AVAILABLE_SERVICES[@]}"; do
if [ -f "$CLUSTER_SETUP_DIR/$service/install.sh" ]; then
print_success " ✓ $service"
else
print_warning " ✗ $service (install.sh missing)"
fi
done
fi
exit 0
fi
# =============================================================================
# CLUSTER SERVICES INSTALLATION
# =============================================================================
print_header "Cluster Services Installation"
print_header "Cluster services installation"
# Check kubectl connectivity
if [ "$DRY_RUN" = false ]; then
@@ -151,28 +108,11 @@ else
print_info "Installing all available services"
fi
# Filter to only include services that actually exist
EXISTING_SERVICES=()
for service in "${SERVICES_TO_INSTALL[@]}"; do
if [ -d "$CLUSTER_SETUP_DIR/$service" ] && [ -f "$CLUSTER_SETUP_DIR/$service/install.sh" ]; then
EXISTING_SERVICES+=("$service")
elif [ ${#SPECIFIC_SERVICES[@]} -gt 0 ]; then
# Only warn if user specifically requested this service
print_warning "Service '$service' not found or missing install.sh"
fi
done
if [ ${#EXISTING_SERVICES[@]} -eq 0 ]; then
print_error "No installable services found"
print_info "Run 'wild-cluster-services-generate' first to generate setup files"
exit 1
fi
print_info "Services to install: ${EXISTING_SERVICES[*]}"
print_info "Services to install: ${SERVICES_TO_INSTALL[*]}"
if [ "$DRY_RUN" = true ]; then
print_info "DRY RUN - would install the following services:"
for service in "${EXISTING_SERVICES[@]}"; do
for service in "${SERVICES_TO_INSTALL[@]}"; do
print_info " - $service: $CLUSTER_SETUP_DIR/$service/install.sh"
done
exit 0
@@ -183,7 +123,9 @@ cd "$CLUSTER_SETUP_DIR"
INSTALLED_COUNT=0
FAILED_COUNT=0
for service in "${EXISTING_SERVICES[@]}"; do
SOURCE_DIR="${WC_ROOT}/setup/cluster-services"
for service in "${SERVICES_TO_INSTALL[@]}"; do
echo ""
print_header "Installing $service"
@@ -206,7 +148,7 @@ cd - >/dev/null
# Summary
echo ""
print_header "Installation Summary"
print_header "Installation summary"
print_success "Successfully installed: $INSTALLED_COUNT services"
if [ $FAILED_COUNT -gt 0 ]; then
print_warning "Failed to install: $FAILED_COUNT services"
@@ -219,13 +161,13 @@ if [ $INSTALLED_COUNT -gt 0 ]; then
echo " 2. Check service status with: kubectl get services --all-namespaces"
# Service-specific next steps
if [[ " ${EXISTING_SERVICES[*]} " =~ " kubernetes-dashboard " ]]; then
if [[ " ${SERVICES_TO_INSTALL[*]} " =~ " kubernetes-dashboard " ]]; then
INTERNAL_DOMAIN=$(wild-config cloud.internalDomain 2>/dev/null || echo "your-internal-domain")
echo " 3. Access dashboard at: https://dashboard.${INTERNAL_DOMAIN}"
echo " 4. Get dashboard token with: ${WC_ROOT}/bin/dashboard-token"
fi
if [[ " ${EXISTING_SERVICES[*]} " =~ " cert-manager " ]]; then
if [[ " ${SERVICES_TO_INSTALL[*]} " =~ " cert-manager " ]]; then
echo " 3. Check cert-manager: kubectl get clusterissuers"
fi
fi

View File

@@ -73,11 +73,9 @@ CONFIG_FILE="${WC_HOME}/config.yaml"
# Create config file if it doesn't exist
if [ ! -f "${CONFIG_FILE}" ]; then
echo "Creating new config file at ${CONFIG_FILE}"
print_info "Creating new config file at ${CONFIG_FILE}"
echo "{}" > "${CONFIG_FILE}"
fi
# Use yq to set the value in the YAML file
yq eval ".${KEY_PATH} = \"${VALUE}\"" -i "${CONFIG_FILE}"
echo "Set ${KEY_PATH} = ${VALUE}"

View File

@@ -48,6 +48,33 @@ while [[ $# -gt 0 ]]; do
esac
done
# Check if directory has any files (including hidden files, excluding . and .. and .git)
if [ "${UPDATE}" = false ]; then
if [ -n "$(find . -maxdepth 1 -name ".*" -o -name "*" | grep -v "^\.$" | grep -v "^\.\.$" | grep -v "^\./\.git$" | head -1)" ]; then
NC='\033[0m' # No Color
YELLOW='\033[1;33m' # Yellow
echo -e "${YELLOW}WARNING:${NC} Directory is not empty."
read -p "Do you want to overwrite existing files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
confirm="yes"
else
confirm="no"
fi
if [ "$confirm" != "yes" ]; then
echo "Aborting setup. Please run this script in an empty directory."
exit 1
fi
fi
fi
# Initialize .wildcloud directory if it doesn't exist.
if [ ! -d ".wildcloud" ]; then
mkdir -p ".wildcloud"
UPDATE=true
echo "Created '.wildcloud' directory."
fi
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
echo "WC_ROOT is not set."
@@ -56,12 +83,10 @@ else
source "${WC_ROOT}/scripts/common.sh"
fi
# Initialize .wildcloud directory if it doesn't exist.
if [ ! -d ".wildcloud" ]; then
mkdir -p ".wildcloud"
UPDATE=true
echo "Created '.wildcloud' directory."
# Initialize config.yaml if it doesn't exist.
if [ ! -f "config.yaml" ]; then
touch "config.yaml"
echo "Created 'config.yaml' file."
fi
# =============================================================================
@@ -84,46 +109,21 @@ if [ -z "$current_cluster_name" ] || [ "$current_cluster_name" = "null" ]; then
print_info "Set cluster name to: ${cluster_name}"
fi
# Check if current directory is empty for new cloud
if [ "${UPDATE}" = false ]; then
# Check if directory has any files (including hidden files, excluding . and .. and .git)
if [ -n "$(find . -maxdepth 1 -name ".*" -o -name "*" | grep -v "^\.$" | grep -v "^\.\.$" | grep -v "^\./\.git$" | grep -v "^\./\.wildcloud$"| head -1)" ]; then
echo "Warning: Current directory is not empty."
read -p "Do you want to overwrite existing files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
confirm="yes"
else
confirm="no"
fi
if [ "$confirm" != "yes" ]; then
echo "Aborting setup. Please run this script in an empty directory."
exit 1
fi
fi
fi
# =============================================================================
# COPY SCAFFOLD
# =============================================================================
# Copy cloud files to current directory only if they do not exist.
# Ignore files that already exist.
SRC_DIR="${WC_ROOT}/setup/home-scaffold"
rsync -av --ignore-existing --exclude=".git" "${SRC_DIR}/" ./ > /dev/null
print_success "Ready for cluster setup!"
# =============================================================================
# COMPLETION
# COPY DOCS
# =============================================================================
print_header "Wild Cloud Scaffold Setup Complete! Welcome to Wild Cloud!"
echo ""
echo "Next steps:"
echo " 1. Set up your Kubernetes cluster:"
echo " wild-setup-cluster"
echo ""
echo " 2. Install cluster services:"
echo " wild-setup-services"
echo ""
echo "Or run the complete setup:"
echo " wild-setup"
wild-update-docs --force
print_success "Wild Cloud initialized! Welcome to Wild Cloud!"

View File

@@ -11,14 +11,6 @@ SKIP_SERVICES=false
while [[ $# -gt 0 ]]; do
case $1 in
--skip-scaffold)
SKIP_SCAFFOLD=true
shift
;;
--skip-docs)
SKIP_DOCS=true
shift
;;
--skip-cluster)
SKIP_CLUSTER=true
shift
@@ -80,55 +72,12 @@ else
fi
print_header "Wild Cloud Setup"
print_info "Running complete Wild Cloud setup."
echo ""
# =============================================================================
# WC_HOME SCAFFOLDING
# =============================================================================
if [ "${SKIP_SCAFFOLD}" = false ]; then
print_header "Cloud Home Setup"
print_info "Scaffolding your cloud home..."
if wild-setup-scaffold; then
print_success "Cloud home setup completed"
else
print_error "Cloud home setup failed"
exit 1
fi
echo ""
else
print_info "Skipping Home Setup"
fi
# =============================================================================
# DOCS
# =============================================================================
if [ "${SKIP_DOCS}" = false ]; then
print_header "Cloud Docs"
print_info "Preparing your docs..."
if wild-setup-docs; then
print_success "Cloud docs setup completed"
else
print_error "Cloud docs setup failed"
exit 1
fi
echo ""
else
print_info "Skipping Docs Setup"
fi
# =============================================================================
# CLUSTER SETUP
# =============================================================================
if [ "${SKIP_CLUSTER}" = false ]; then
print_header "Cluster Setup"
print_info "Running wild-setup-cluster..."
if wild-setup-cluster; then
print_success "Cluster setup completed"
else
@@ -145,9 +94,6 @@ fi
# =============================================================================
if [ "${SKIP_SERVICES}" = false ]; then
print_header "Services Setup"
print_info "Running wild-setup-services..."
if wild-setup-services; then
print_success "Services setup completed"
else

View File

@@ -62,40 +62,6 @@ else
fi
print_header "Wild Cloud Cluster Setup"
print_info "Setting up cluster infrastructure"
echo ""
# Generate initial cluster configuration
if ! wild-cluster-config-generate; then
print_error "Failed to generate cluster configuration"
exit 1
fi
# Configure Talos cli with our new cluster context
CLUSTER_NAME=$(wild-config "cluster.name")
HAS_CONTEXT=$(talosctl config contexts | grep -c "$CLUSTER_NAME" || true)
if [ "$HAS_CONTEXT" -eq 0 ]; then
print_info "No Talos context found for cluster $CLUSTER_NAME, creating..."
talosctl config merge ${WC_HOME}/setup/cluster-nodes/generated/talosconfig
talosctl config use "$CLUSTER_NAME"
print_success "Talos context for $CLUSTER_NAME created and set as current"
fi
# Talos asset download
if [ "${SKIP_INSTALLER}" = false ]; then
print_header "Installer Image Generation"
print_info "Running wild-cluster-node-boot-assets-download..."
wild-cluster-node-boot-assets-download
print_success "Installer image generated"
echo ""
else
print_info "Skipping: Installer Image Generation"
fi
# =============================================================================
# Configuration
@@ -103,6 +69,9 @@ fi
prompt_if_unset_config "operator.email" "Operator email address"
prompt_if_unset_config "cluster.name" "Cluster name" "wild-cluster"
CLUSTER_NAME=$(wild-config "cluster.name")
# Configure hostname prefix for unique node names on LAN
prompt_if_unset_config "cluster.hostnamePrefix" "Hostname prefix (optional, e.g. 'test-' for unique names on LAN)" ""
HOSTNAME_PREFIX=$(wild-config "cluster.hostnamePrefix")
@@ -123,41 +92,41 @@ prompt_if_unset_config "cluster.ipAddressPool" "MetalLB IP address pool" "${SUBN
ip_pool=$(wild-config "cluster.ipAddressPool")
# Load balancer IP (automatically set to first address in the pool if not set)
current_lb_ip=$(wild-config "cluster.loadBalancerIp")
if [ -z "$current_lb_ip" ] || [ "$current_lb_ip" = "null" ]; then
lb_ip=$(echo "${ip_pool}" | cut -d'-' -f1)
wild-config-set "cluster.loadBalancerIp" "${lb_ip}"
print_info "Set load balancer IP to: ${lb_ip} (first IP in MetalLB pool)"
fi
default_lb_ip=$(echo "${ip_pool}" | cut -d'-' -f1)
prompt_if_unset_config "cluster.loadBalancerIp" "Load balancer IP" "${default_lb_ip}"
# Talos version
prompt_if_unset_config "cluster.nodes.talos.version" "Talos version" "v1.10.4"
prompt_if_unset_config "cluster.nodes.talos.version" "Talos version" "v1.11.0"
talos_version=$(wild-config "cluster.nodes.talos.version")
# Talos schematic ID
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -z "$current_schematic_id" ] || [ "$current_schematic_id" = "null" ]; then
echo ""
print_info "Get your Talos schematic ID from: https://factory.talos.dev/"
print_info "This customizes Talos with the drivers needed for your hardware."
# Use current schematic ID from config as default
default_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$default_schematic_id" ] && [ "$default_schematic_id" != "null" ]; then
print_info "Using schematic ID from config for Talos $talos_version"
else
default_schematic_id=""
fi
schematic_id=$(prompt_with_default "Talos schematic ID" "${default_schematic_id}" "${current_schematic_id}")
wild-config-set "cluster.nodes.talos.schematicId" "${schematic_id}"
fi
prompt_if_unset_config "cluster.nodes.talos.schematicId" "Talos schematic ID" "56774e0894c8a3a3a9834a2aea65f24163cacf9506abbcbdc3ba135eaca4953f"
schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
# External DNS
cluster_name=$(wild-config "cluster.name")
prompt_if_unset_config "cluster.externalDns.ownerId" "External DNS owner ID" "external-dns-${cluster_name}"
prompt_if_unset_config "cluster.externalDns.ownerId" "External DNS owner ID" "external-dns-${CLUSTER_NAME}"
# =============================================================================
# TALOS CLUSTER CONFIGURATION
# =============================================================================
prompt_if_unset_config "cluster.nodes.control.vip" "Control plane virtual IP" "${SUBNET_PREFIX}.90"
vip=$(wild-config "cluster.nodes.control.vip")
# Generate initial cluster configuration
if ! wild-cluster-config-generate; then
print_error "Failed to generate cluster configuration"
exit 1
fi
# Configure Talos cli with our new cluster context
HAS_CONTEXT=$(talosctl config contexts | grep -c "$CLUSTER_NAME" || true)
if [ "$HAS_CONTEXT" -eq 0 ]; then
print_info "No Talos context found for cluster $CLUSTER_NAME, creating..."
talosctl config merge ${WC_HOME}/setup/cluster-nodes/generated/talosconfig
talosctl config context "$CLUSTER_NAME"
print_success "Talos context for $CLUSTER_NAME created and set as current"
fi
# =============================================================================
# Node setup
@@ -167,12 +136,6 @@ if [ "${SKIP_HARDWARE}" = false ]; then
print_header "Control Plane Configuration"
print_info "Configure control plane nodes (you need at least 3 for HA):"
echo ""
prompt_if_unset_config "cluster.nodes.control.vip" "Control plane virtual IP" "${SUBNET_PREFIX}.90"
vip=$(wild-config "cluster.nodes.control.vip")
# Automatically configure the first three IPs after VIP for control plane nodes
vip_last_octet=$(echo "$vip" | cut -d. -f4)
vip_prefix=$(echo "$vip" | cut -d. -f1-3)
@@ -184,7 +147,6 @@ if [ "${SKIP_HARDWARE}" = false ]; then
for i in 1 2 3; do
NODE_NAME="${HOSTNAME_PREFIX}control-${i}"
TARGET_IP="${vip_prefix}.$(( vip_last_octet + i ))"
echo ""
print_info "Registering control plane node: $NODE_NAME (IP: $TARGET_IP)"
# Initialize the node in cluster.nodes.active if not already present
@@ -288,14 +250,8 @@ if [ "${SKIP_HARDWARE}" = false ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node
current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi
if [ -n "$current_schematic_id" ] && [ "$current_schematic_id" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".schematicId" "$current_schematic_id"
fi
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$talos_version"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".schematicId" "$schematic_id"
echo ""
read -p "Bring node $NODE_NAME ($TARGET_IP) up now? (y/N): " -r apply_config
@@ -315,7 +271,7 @@ if [ "${SKIP_HARDWARE}" = false ]; then
read -p "The cluster should be bootstrapped after the first control node is ready. Is it ready?: " -r is_ready
if [[ $is_ready =~ ^[Yy]$ ]]; then
print_info "Bootstrapping control plane node $TARGET_IP..."
talos config endpoint "$TARGET_IP"
talosctl config endpoint "$TARGET_IP"
# Attempt to bootstrap the cluster
if talosctl bootstrap --nodes "$TARGET_IP" 2>&1 | tee /tmp/bootstrap_output.log; then
@@ -425,14 +381,8 @@ if [ "${SKIP_HARDWARE}" = false ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node
current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi
if [ -n "$current_schematic_id" ] && [ "$current_schematic_id" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".schematicId" "$current_schematic_id"
fi
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$talos_version"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".schematicId" "$schematic_id"
print_success "Worker node $NODE_NAME registered successfully:"
print_info " - Name: $NODE_NAME"

View File

@@ -65,9 +65,7 @@ if [ -z "$(wild-config "cluster.name")" ]; then
exit 1
fi
print_header "Wild Cloud Services Setup"
print_info "Installing Kubernetes cluster services"
echo ""
print_header "Wild Cloud services setup"
if ! command -v kubectl >/dev/null 2>&1; then
print_error "kubectl is not installed or not in PATH"
@@ -82,8 +80,8 @@ if ! kubectl cluster-info >/dev/null 2>&1; then
fi
# Generate cluster services setup files
wild-cluster-services-generate --force
wild-cluster-services-fetch
wild-cluster-services-generate
# Apply cluster services to cluster

View File

@@ -4,28 +4,28 @@ set -e
set -o pipefail
# Parse arguments
UPDATE=false
FORCE=false
while [[ $# -gt 0 ]]; do
case $1 in
--update)
UPDATE=true
--force)
FORCE=true
shift
;;
-h|--help)
echo "Usage: $0 [--update]"
echo "Usage: $0 [--force]"
echo ""
echo "Copy Wild Cloud documentation to the current cloud directory."
echo ""
echo "Options:"
echo " --update Update existing docs (overwrite)"
echo " --force Force overwrite of existing docs"
echo " -h, --help Show this help message"
echo ""
exit 0
;;
-*)
echo "Unknown option $1"
echo "Usage: $0 [--update]"
echo "Usage: $0 [--force]"
exit 1
;;
*)
@@ -48,21 +48,21 @@ fi
DOCS_DEST="${WC_HOME}/docs"
# Check if docs already exist
if [ -d "${DOCS_DEST}" ] && [ "${UPDATE}" = false ]; then
echo "Documentation already exists at ${DOCS_DEST}"
if [ -d "${DOCS_DEST}" ] && [ "${FORCE}" = false ]; then
print_warning "Documentation already exists at ${DOCS_DEST}"
read -p "Do you want to update documentation files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
UPDATE=true
FORCE=true
else
echo "Skipping documentation update."
print_info "Skipping documentation update."
exit 0
fi
fi
# Copy docs directory from root to WC_HOME
if [ -d "${WC_ROOT}/docs" ]; then
if [ "${UPDATE}" = true ] && [ -d "${DOCS_DEST}" ]; then
if [ "${FORCE}" = true ] && [ -d "${DOCS_DEST}" ]; then
rm -rf "${DOCS_DEST}"
fi
cp -r "${WC_ROOT}/docs" "${DOCS_DEST}"

View File

@@ -4,7 +4,8 @@ Keep your wild cloud running smoothly.
- [Security Best Practices](./guides/security.md)
- [Monitoring](./guides/monitoring.md)
- [Backup and Restore](./guides/backup-and-restore.md)
- [Making backups](./guides/making-backups.md)
- [Restoring backups](./guides/restoring-backups.md)
## Upgrade

View File

@@ -1,3 +0,0 @@
# Backup and Restore
TBD

View File

@@ -0,0 +1,265 @@
# Making Backups
This guide covers how to create backups of your wild-cloud infrastructure using the integrated backup system.
## Overview
The wild-cloud backup system creates encrypted, deduplicated snapshots using restic. It backs up three main components:
- **Applications**: Database dumps and persistent volume data
- **Cluster**: Kubernetes resources and etcd state
- **Configuration**: Wild-cloud repository and settings
## Prerequisites
Before making backups, ensure you have:
1. **Environment configured**: Run `source env.sh` to load backup configuration
2. **Restic repository**: Backup repository configured in `config.yaml`
3. **Backup password**: Set in wild-cloud secrets
4. **Staging directory**: Configured path for temporary backup files
## Backup Components
### Applications (`wild-app-backup`)
Backs up individual applications including:
- **Database dumps**: PostgreSQL/MySQL databases in compressed custom format
- **PVC data**: Application files streamed directly for restic deduplication
- **Auto-discovery**: Finds databases and PVCs based on app manifest.yaml
### Cluster Resources (`wild-backup --cluster-only`)
Backs up cluster-wide resources:
- **Kubernetes resources**: All pods, services, deployments, secrets, configmaps
- **Storage definitions**: PersistentVolumes, PVCs, StorageClasses
- **etcd snapshot**: Complete cluster state for disaster recovery
### Configuration (`wild-backup --home-only`)
Backs up wild-cloud configuration:
- **Repository contents**: All app definitions, manifests, configurations
- **Settings**: Wild-cloud configuration files and customizations
## Making Backups
### Full System Backup (Recommended)
Create a complete backup of everything:
```bash
# Backup all components (apps + cluster + config)
wild-backup
```
This is equivalent to:
```bash
wild-backup --home --apps --cluster
```
### Selective Backups
#### Applications Only
```bash
# All applications
wild-backup --apps-only
# Single application
wild-app-backup discourse
# Multiple applications
wild-app-backup discourse gitea immich
```
#### Cluster Only
```bash
# Kubernetes resources + etcd
wild-backup --cluster-only
```
#### Configuration Only
```bash
# Wild-cloud repository
wild-backup --home-only
```
### Excluding Components
Skip specific components:
```bash
# Skip config, backup apps + cluster
wild-backup --no-home
# Skip applications, backup config + cluster
wild-backup --no-apps
# Skip cluster resources, backup config + apps
wild-backup --no-cluster
```
## Backup Process Details
### Application Backup Process
1. **Discovery**: Parses `manifest.yaml` to find database and PVC dependencies
2. **Database backup**: Creates compressed custom-format dumps
3. **PVC backup**: Streams files directly to staging for restic deduplication
4. **Staging**: Organizes files in clean directory structure
5. **Upload**: Creates individual restic snapshots per application
### Cluster Backup Process
1. **Resource export**: Exports all Kubernetes resources to YAML
2. **etcd snapshot**: Creates point-in-time etcd backup via talosctl
3. **Upload**: Creates single restic snapshot for cluster state
### Restic Snapshots
Each backup creates tagged restic snapshots:
```bash
# View all snapshots
restic snapshots
# Filter by component
restic snapshots --tag discourse # Specific app
restic snapshots --tag cluster # Cluster resources
restic snapshots --tag wc-home # Wild-cloud config
```
## Where Backup Files Are Staged
Before uploading to your restic repository, backup files are organized in a staging directory. This temporary area lets you see exactly what's being backed up and helps with deduplication.
Here's what the staging area looks like:
```
backup-staging/
├── apps/
│ ├── discourse/
│ │ ├── database_20250816T120000Z.dump
│ │ ├── globals_20250816T120000Z.sql
│ │ └── discourse/
│ │ └── data/ # All the actual files
│ ├── gitea/
│ │ ├── database_20250816T120000Z.dump
│ │ └── gitea-data/
│ │ └── data/ # Git repositories, etc.
│ └── immich/
│ ├── database_20250816T120000Z.dump
│ └── immich-data/
│ └── upload/ # Photos and videos
└── cluster/
├── all-resources.yaml # All running services
├── secrets.yaml # Passwords and certificates
├── configmaps.yaml # Configuration data
└── etcd-snapshot.db # Complete cluster state
```
This staging approach means you can examine backup contents before they're uploaded, and restic can efficiently deduplicate files that haven't changed.
## Advanced Usage
### Custom Backup Scripts
Applications can provide custom backup logic:
```bash
# Create apps/myapp/backup.sh for custom behavior
chmod +x apps/myapp/backup.sh
# wild-app-backup will use custom script if present
wild-app-backup myapp
```
### Monitoring Backup Status
```bash
# Check recent snapshots
restic snapshots | head -20
# Check specific app backups
restic snapshots --tag discourse
# Verify backup integrity
restic check
```
### Backup Automation
Set up automated backups with cron:
```bash
# Daily full backup at 2 AM
0 2 * * * cd /data/repos/payne-cloud && source env.sh && wild-backup
# Hourly app backups during business hours
0 9-17 * * * cd /data/repos/payne-cloud && source env.sh && wild-backup --apps-only
```
## Performance Considerations
### Large PVCs (like Immich photos)
The streaming backup approach provides:
- **First backup**: Full transfer time (all files processed)
- **Subsequent backups**: Only changed files processed (dramatically faster)
- **Storage efficiency**: Restic deduplication reduces storage usage
### Network Usage
- **Database dumps**: Compressed at source, efficient transfer
- **PVC data**: Uncompressed transfer, but restic handles deduplication
- **etcd snapshots**: Small files, minimal impact
## Troubleshooting
### Common Issues
**"No databases or PVCs found"**
- App has no `manifest.yaml` with database dependencies
- No PVCs with matching labels in app namespace
- Create custom `backup.sh` script for special cases
**"kubectl not found"**
- Ensure kubectl is installed and configured
- Check cluster connectivity with `kubectl get nodes`
**"Staging directory not set"**
- Configure `cloud.backup.staging` in `config.yaml`
- Ensure directory exists and is writable
**"Could not create etcd backup"**
- Ensure `talosctl` is installed for Talos clusters
- Check control plane node connectivity
- Verify etcd pods are accessible in kube-system namespace
### Backup Verification
Always verify backups periodically:
```bash
# Check restic repository integrity
restic check
# List recent snapshots
restic snapshots --compact
# Test restore to different directory
restic restore latest --target /tmp/restore-test
```
## Security Notes
- **Encryption**: All backups are encrypted with your backup password
- **Secrets**: Kubernetes secrets are included in cluster backups
- **Access control**: Secure your backup repository and passwords
- **Network**: Consider bandwidth usage for large initial backups
## Next Steps
- [Restoring Backups](restoring-backups.md) - Learn how to restore from backups
- Configure automated backup schedules
- Set up backup monitoring and alerting
- Test disaster recovery procedures

View File

@@ -0,0 +1,294 @@
# Restoring Backups
This guide will walk you through restoring your applications and cluster from wild-cloud backups. Hopefully you'll never need this, but when you do, it's critical that the process works smoothly.
## Understanding Restore Types
Your wild-cloud backup system can restore different types of data depending on what you need to recover:
**Application restores** bring back individual applications by restoring their database contents and file storage. This is what you'll use most often - maybe you accidentally deleted something in Discourse, or Gitea got corrupted, or you want to roll back Immich to before a bad update.
**Cluster restores** are for disaster recovery scenarios where you need to rebuild your entire Kubernetes cluster from scratch. This includes restoring all the cluster's configuration and even its internal state.
**Configuration restores** bring back your wild-cloud repository and settings, which contain all the "recipes" for how your infrastructure should be set up.
## Before You Start Restoring
Make sure you have everything needed to perform restores. You need to be in your wild-cloud directory with the environment loaded (`source env.sh`). Your backup repository and password should be configured and working - you can test this by running `restic snapshots` to see your available backups.
Most importantly, make sure you have kubectl access to your cluster, since restores involve creating temporary pods and manipulating storage.
## Restoring Applications
### Basic Application Restore
The most common restore scenario is bringing back a single application. To restore the latest backup of an app:
```bash
wild-app-restore discourse
```
This restores both the database and all file storage for the discourse app. The restore system automatically figures out what the app needs based on its manifest file and what was backed up.
If you want to restore from a specific backup instead of the latest:
```bash
wild-app-restore discourse abc123
```
Where `abc123` is the snapshot ID from `restic snapshots --tag discourse`.
### Partial Restores
Sometimes you only need to restore part of an application. Maybe the database is fine but the files got corrupted, or vice versa.
To restore only the database:
```bash
wild-app-restore discourse --db-only
```
To restore only the file storage:
```bash
wild-app-restore discourse --pvc-only
```
To restore without database roles and permissions (if they're causing conflicts):
```bash
wild-app-restore discourse --skip-globals
```
### Finding Available Backups
To see what backups are available for an app:
```bash
wild-app-restore discourse --list
```
This shows recent snapshots with their IDs, timestamps, and what was included.
## How Application Restores Work
Understanding what happens during a restore can help when things don't go as expected.
### Database Restoration
When restoring a database, the system first downloads the backup files from your restic repository. It then prepares the database by creating any needed roles, disconnecting existing users, and dropping/recreating the database to ensure a clean restore.
For PostgreSQL databases, it uses `pg_restore` with parallel processing to speed up large database imports. For MySQL, it uses standard mysql import commands. The system also handles database ownership and permissions automatically.
### File Storage Restoration
File storage (PVC) restoration is more complex because it involves safely replacing files that might be actively used by running applications.
First, the system creates a safety snapshot using Longhorn. This means if something goes wrong during the restore, you can get back to where you started. Then it scales your application down to zero replicas so no pods are using the storage.
Next, it creates a temporary utility pod with the PVC mounted and copies all the backup files into place, preserving file permissions and structure. Once the data is restored and verified, it removes the utility pod and scales your application back up.
If everything worked correctly, the safety snapshot is automatically deleted. If something went wrong, the safety snapshot is preserved so you can recover manually.
## Cluster Disaster Recovery
Cluster restoration is much less common but critical when you need to rebuild your entire infrastructure.
### Restoring Kubernetes Resources
To restore all cluster resources from a backup:
```bash
# Download cluster backup
restic restore --tag cluster latest --target ./restore/
# Apply all resources
kubectl apply -f restore/cluster/all-resources.yaml
```
You can also restore specific types of resources:
```bash
kubectl apply -f restore/cluster/secrets.yaml
kubectl apply -f restore/cluster/configmaps.yaml
```
### Restoring etcd State
**Warning: This is extremely dangerous and will affect your entire cluster.**
etcd restoration should only be done when rebuilding a cluster from scratch. For Talos clusters:
```bash
talosctl --nodes <control-plane-ip> etcd restore --from ./restore/cluster/etcd-snapshot.db
```
This command stops etcd, replaces its data with the backup, and restarts the cluster. Expect significant downtime while the cluster rebuilds itself.
## Common Disaster Recovery Scenarios
### Complete Application Loss
When an entire application is gone (namespace deleted, pods corrupted, etc.):
```bash
# Make sure the namespace exists
kubectl create namespace discourse --dry-run=client -o yaml | kubectl apply -f -
# Apply the application manifests if needed
kubectl apply -f apps/discourse/
# Restore the application data
wild-app-restore discourse
```
### Complete Cluster Rebuild
When rebuilding a cluster from scratch:
First, build your new cluster infrastructure and install wild-cloud components. Then configure backup access so you can reach your backup repository.
Restore cluster state:
```bash
restic restore --tag cluster latest --target ./restore/
# Apply etcd snapshot using appropriate method for your cluster type
```
Finally, restore all applications:
```bash
# See what applications are backed up
wild-app-restore --list
# Restore each application individually
wild-app-restore discourse
wild-app-restore gitea
wild-app-restore immich
```
### Rolling Back After Bad Changes
Sometimes you need to undo recent changes to an application:
```bash
# See available snapshots
wild-app-restore discourse --list
# Restore from before the problematic changes
wild-app-restore discourse abc123
```
## Cross-Cluster Migration
You can use backups to move applications between clusters:
On the source cluster, create a fresh backup:
```bash
wild-app-backup discourse
```
On the target cluster, deploy the application manifests:
```bash
kubectl apply -f apps/discourse/
```
Then restore the data:
```bash
wild-app-restore discourse
```
## Verifying Successful Restores
After any restore, verify that everything is working correctly.
For databases, check that you can connect and see expected data:
```bash
kubectl exec -n postgres deploy/postgres-deployment -- \
psql -U postgres -d discourse -c "SELECT count(*) FROM posts;"
```
For file storage, check that files exist and applications can start:
```bash
kubectl get pods -n discourse
kubectl logs -n discourse deployment/discourse
```
For web applications, test that you can access them:
```bash
curl -f https://discourse.example.com/latest.json
```
## When Things Go Wrong
### No Snapshots Found
If the restore system can't find backups for an application, check that snapshots exist:
```bash
restic snapshots --tag discourse
```
Make sure you're using the correct app name and that backups were actually created successfully.
### Database Restore Failures
Database restores can fail if the target database isn't accessible or if there are permission issues. Check that your postgres or mysql pods are running and that you can connect to them manually.
Review the restore error messages carefully - they usually indicate whether the problem is with the backup file, database connectivity, or permissions.
### PVC Restore Failures
If PVC restoration fails, check that you have sufficient disk space and that the PVC isn't being used by other pods. The error messages will usually indicate what went wrong.
Most importantly, remember that safety snapshots are preserved when PVC restores fail. You can see them with:
```bash
kubectl get snapshot.longhorn.io -n longhorn-system -l app=wild-app-restore
```
These snapshots let you recover to the pre-restore state if needed.
### Application Won't Start After Restore
If pods fail to start after restoration, check file permissions and ownership. Sometimes the restoration process doesn't perfectly preserve the exact permissions that the application expects.
You can also try scaling the application to zero and back to one, which sometimes resolves transient issues:
```bash
kubectl scale deployment/discourse -n discourse --replicas=0
kubectl scale deployment/discourse -n discourse --replicas=1
```
## Manual Recovery
When automated restore fails, you can always fall back to manual extraction and restoration:
```bash
# Extract backup files to local directory
restic restore --tag discourse latest --target ./manual-restore/
# Manually copy database dump to postgres pod
kubectl cp ./manual-restore/discourse/database_*.dump \
postgres/postgres-deployment-xxx:/tmp/
# Manually restore database
kubectl exec -n postgres deploy/postgres-deployment -- \
pg_restore -U postgres -d discourse /tmp/database_*.dump
```
For file restoration, you'd need to create a utility pod and manually copy files into the PVC.
## Best Practices
Test your restore procedures regularly in a non-production environment. It's much better to discover issues with your backup system during a planned test than during an actual emergency.
Always communicate with users before performing restores, especially if they involve downtime. Document any manual steps you had to take so you can improve the automated process.
After any significant restore, monitor your applications more closely than usual for a few days. Sometimes problems don't surface immediately.
## Security and Access Control
Restore operations are powerful and can be destructive. Make sure only trusted administrators can perform restores, and consider requiring approval or coordination before major restoration operations.
Be aware that cluster restores include all secrets, so they potentially expose passwords, API keys, and certificates. Ensure your backup repository is properly secured.
Remember that Longhorn safety snapshots are preserved when things go wrong. These snapshots may contain sensitive data, so clean them up appropriately once you've resolved any issues.
## What's Next
The best way to get comfortable with restore operations is to practice them in a safe environment. Set up a test cluster and practice restoring applications and data.
Consider creating runbooks for your most likely disaster scenarios, including the specific commands and verification steps for your infrastructure.
Read the [Making Backups](making-backups.md) guide to ensure you're creating the backups you'll need for successful recovery.

23
env.sh
View File

@@ -7,27 +7,4 @@ export WC_ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# Add bin to path first so wild-config is available
export PATH="$WC_ROOT/bin:$PATH"
# Install kubectl
if ! command -v kubectl &> /dev/null; then
echo "Error: kubectl is not installed. Installing."
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
fi
# Install talosctl
if ! command -v talosctl &> /dev/null; then
echo "Error: talosctl is not installed. Installing."
curl -sL https://talos.dev/install | sh
fi
# Check if gomplate is installed
if ! command -v gomplate &> /dev/null; then
echo "Error: gomplate is not installed. Please install gomplate first."
echo "Visit: https://docs.gomplate.ca/installing/"
exit 1
fi
echo "Wild Cloud root ready."

View File

@@ -84,7 +84,7 @@ prompt_with_default() {
if [ -n "${default}" ]; then
printf "%s [default: %s]: " "${prompt}" "${default}" >&2
else
printf "%s [default: empty]: " "${prompt}" >&2
printf "%s: " "${prompt}" >&2
fi
read -r result
if [ -z "${result}" ]; then
@@ -109,16 +109,18 @@ prompt_if_unset_config() {
local prompt="$2"
local default="$3"
# Check if key exists first to avoid error messages
if wild-config --check "${config_path}"; then
# Key exists, get its value
local current_value
current_value=$(wild-config "${config_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
print_info "Using existing ${config_path} = ${current_value}"
else
# Key doesn't exist, prompt for it
local new_value
new_value=$(prompt_with_default "${prompt}" "${default}" "")
wild-config-set "${config_path}" "${new_value}"
print_info "Set ${config_path} = ${new_value}"
else
print_info "Using existing ${config_path} = ${current_value}"
fi
}
@@ -128,16 +130,16 @@ prompt_if_unset_secret() {
local prompt="$2"
local default="$3"
local current_value
current_value=$(wild-secret "${secret_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
# Check if key exists first to avoid error messages
if wild-secret --check "${secret_path}"; then
# Key exists, we don't show the value for security
print_info "Using existing secret ${secret_path}"
else
# Key doesn't exist, prompt for it
local new_value
new_value=$(prompt_with_default "${prompt}" "${default}" "")
wild-secret-set "${secret_path}" "${new_value}"
print_info "Set secret ${secret_path}"
else
print_info "Using existing secret ${secret_path}"
fi
}
@@ -168,7 +170,7 @@ init_wild_env() {
if [ -z "${WC_ROOT}" ]; then
echo "ERROR: WC_ROOT is not set."
exit 1
else
fi
# Check if WC_ROOT is a valid directory
if [ ! -d "${WC_ROOT}" ]; then
@@ -187,6 +189,33 @@ init_wild_env() {
echo "ERROR: This command must be run from within a wildcloud home directory."
exit 1
fi
# Check kubectl
if ! command -v kubectl &> /dev/null; then
echo "Error: kubectl is not installed. Please run $WC_ROOT/scripts/install-wild-cloud-dependencies.sh."
fi
# Check talosctl
if ! command -v talosctl &> /dev/null; then
echo "Error: talosctl is not installed. Please run $WC_ROOT/scripts/install-wild-cloud-dependencies.sh."
fi
# Check gomplate
if ! command -v gomplate &> /dev/null; then
echo "Error: gomplate is not installed. Please run $WC_ROOT/scripts/install-wild-cloud-dependencies.sh."
exit 1
fi
# Check yq
if ! command -v yq &> /dev/null; then
echo "Error: yq is not installed. Please run $WC_ROOT/scripts/install-wild-cloud-dependencies.sh."
exit 1
fi
# Check restic
if ! command -v restic &> /dev/null; then
echo "Error: restic is not installed. Please run $WC_ROOT/scripts/install-wild-cloud-dependencies.sh."
exit 1
fi
}

View File

@@ -1,5 +1,14 @@
#!/bin/bash
# Install kubectl
if ! command -v kubectl &> /dev/null; then
echo "Error: kubectl is not installed. Installing."
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
fi
# Install gomplate
if command -v gomplate &> /dev/null; then
echo "gomplate is already installed."

View File

@@ -9,11 +9,13 @@ Follow the instructions to [set up cluster nodes](./cluster-nodes/README.md).
Set up cluster services:
```bash
./setup/cluster/setup-all.sh
wild-cluster-services-fetch
wild-cluster-services-configure
wild-cluster-services-up
```
Now make sure everything works:
```bash
./setup/cluster/validate-setup.sh
wild-health
```

View File

@@ -45,3 +45,18 @@
- siderolabs/nvidia-open-gpu-kernel-modules-lts
- siderolabs/nvidia-open-gpu-kernel-modules-production
- siderolabs/util-linux-tools"
"56774e0894c8a3a3a9834a2aea65f24163cacf9506abbcbdc3ba135eaca4953f":
version: "v1.11.0"
architecture: "amd64"
secureBoot: false
schematic:
customization:
systemExtensions:
officialExtensions:
- siderolabs/gvisor
- siderolabs/intel-ucode
- siderolabs/iscsi-tools
- siderolabs/nvidia-container-toolkit-production
- siderolabs/nvidia-fabricmanager-production
- siderolabs/nvidia-open-gpu-kernel-modules-production
- siderolabs/util-linux-tools

View File

@@ -0,0 +1,10 @@
#!/bin/bash
print_info "Collecting cert-manager configuration..."
prompt_if_unset_config "cloud.domain" "Enter main domain name" "example.com"
domain=$(wild-config "cloud.domain")
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name" "local.${domain}"
prompt_if_unset_config "operator.email" "Enter operator email address (for Let's Encrypt)" ""
prompt_if_unset_config "cluster.certManager.cloudflare.domain" "Enter Cloudflare domain (for DNS challenges)" "${domain}"
prompt_if_unset_secret "cloudflare.token" "Enter Cloudflare API token (for DNS challenges)" ""

View File

@@ -16,21 +16,6 @@ CERT_MANAGER_DIR="${CLUSTER_SETUP_DIR}/cert-manager"
print_header "Setting up cert-manager"
# Collect required configuration variables
print_info "Collecting cert-manager configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cloud.domain" "Enter main domain name" "example.com"
# Get the domain value to use as default for internal domain
domain=$(wild-config "cloud.domain")
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name" "local.${domain}"
prompt_if_unset_config "operator.email" "Enter operator email address (for Let's Encrypt)" ""
prompt_if_unset_config "cluster.certManager.cloudflare.domain" "Enter Cloudflare domain (for DNS challenges)" "${domain}"
prompt_if_unset_secret "cloudflare.token" "Enter Cloudflare API token (for DNS challenges)" ""
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled cert-manager templates..."
if [ ! -d "${CERT_MANAGER_DIR}/kustomize" ]; then

View File

@@ -0,0 +1,7 @@
#!/bin/bash
print_info "Collecting CoreDNS configuration..."
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name" "local.example.com"
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address" "192.168.1.240"
prompt_if_unset_config "cloud.dns.externalResolver" "Enter external DNS resolver" "8.8.8.8"

View File

@@ -16,16 +16,6 @@ COREDNS_DIR="${CLUSTER_SETUP_DIR}/coredns"
print_header "Setting up CoreDNS for k3s"
# Collect required configuration variables
print_info "Collecting CoreDNS configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name" "local.example.com"
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address" "192.168.1.240"
prompt_if_unset_config "cloud.dns.externalResolver" "Enter external DNS resolver" "8.8.8.8"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled CoreDNS templates..."
if [ ! -d "${COREDNS_DIR}/kustomize" ]; then

View File

@@ -0,0 +1,6 @@
#!/bin/bash
print_info "Collecting Docker Registry configuration..."
prompt_if_unset_config "cloud.dockerRegistryHost" "Enter Docker Registry hostname" "registry.local.example.com"
prompt_if_unset_config "cluster.dockerRegistry.storage" "Enter Docker Registry storage size" "100Gi"

View File

@@ -16,15 +16,6 @@ DOCKER_REGISTRY_DIR="${CLUSTER_SETUP_DIR}/docker-registry"
print_header "Setting up Docker Registry"
# Collect required configuration variables
print_info "Collecting Docker Registry configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cloud.dockerRegistryHost" "Enter Docker Registry hostname" "registry.local.example.com"
prompt_if_unset_config "cluster.dockerRegistry.storage" "Enter Docker Registry storage size" "100Gi"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled Docker Registry templates..."
if [ ! -d "${DOCKER_REGISTRY_DIR}/kustomize" ]; then

View File

@@ -0,0 +1,3 @@
print_info "Collecting ExternalDNS configuration..."
prompt_if_unset_config "cluster.externalDns.ownerId" "Enter ExternalDNS owner ID (unique identifier for this cluster)" "wild-cloud-$(hostname -s)"

View File

@@ -16,14 +16,6 @@ EXTERNALDNS_DIR="${CLUSTER_SETUP_DIR}/externaldns"
print_header "Setting up ExternalDNS"
# Collect required configuration variables
print_info "Collecting ExternalDNS configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cluster.externalDns.ownerId" "Enter ExternalDNS owner ID (unique identifier for this cluster)" "wild-cloud-$(hostname -s)"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled ExternalDNS templates..."
if [ ! -d "${EXTERNALDNS_DIR}/kustomize" ]; then

View File

@@ -1,22 +0,0 @@
#!/bin/bash
set -e
# Navigate to script directory
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
echo "Setting up your wild-cloud cluster services..."
echo
./metallb/install.sh
./longhorn/install.sh
./traefik/install.sh
./coredns/install.sh
./cert-manager/install.sh
./externaldns/install.sh
./kubernetes-dashboard/install.sh
./nfs/install.sh
./docker-registry/install.sh
echo "Service setup complete!"

View File

@@ -0,0 +1,5 @@
#!/bin/bash
print_info "Collecting Kubernetes Dashboard configuration..."
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name (for dashboard URL)" "local.example.com"

View File

@@ -16,14 +16,6 @@ KUBERNETES_DASHBOARD_DIR="${CLUSTER_SETUP_DIR}/kubernetes-dashboard"
print_header "Setting up Kubernetes Dashboard"
# Collect required configuration variables
print_info "Collecting Kubernetes Dashboard configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cloud.internalDomain" "Enter internal domain name (for dashboard URL)" "local.example.com"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled Dashboard templates..."
if [ ! -d "${KUBERNETES_DASHBOARD_DIR}/kustomize" ]; then

View File

@@ -0,0 +1,6 @@
#!/bin/bash
print_info "Collecting MetalLB configuration..."
prompt_if_unset_config "cluster.ipAddressPool" "Enter IP address pool for MetalLB (CIDR format, e.g., 192.168.1.240-192.168.1.250)" "192.168.1.240-192.168.1.250"
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address" "192.168.1.240"

View File

@@ -16,15 +16,6 @@ METALLB_DIR="${CLUSTER_SETUP_DIR}/metallb"
print_header "Setting up MetalLB"
# Collect required configuration variables
print_info "Collecting MetalLB configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cluster.ipAddressPool" "Enter IP address pool for MetalLB (CIDR format, e.g., 192.168.1.240-192.168.1.250)" "192.168.1.240-192.168.1.250"
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address" "192.168.1.240"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled MetalLB templates..."
if [ ! -d "${METALLB_DIR}/kustomize" ]; then

View File

@@ -0,0 +1,7 @@
#!/bin/bash
print_info "Collecting NFS configuration..."
prompt_if_unset_config "cloud.nfs.host" "Enter NFS server hostname or IP address" "192.168.1.100"
prompt_if_unset_config "cloud.nfs.mediaPath" "Enter NFS export path for media storage" "/mnt/storage/media"
prompt_if_unset_config "cloud.nfs.storageCapacity" "Enter NFS storage capacity (e.g., 1Ti, 500Gi)" "1Ti"

View File

@@ -16,16 +16,6 @@ NFS_DIR="${CLUSTER_SETUP_DIR}/nfs"
print_header "Registering NFS server with Kubernetes cluster"
# Collect required configuration variables
print_info "Collecting NFS configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cloud.nfs.host" "Enter NFS server hostname or IP address" "192.168.1.100"
prompt_if_unset_config "cloud.nfs.mediaPath" "Enter NFS export path for media storage" "/mnt/storage/media"
prompt_if_unset_config "cloud.nfs.storageCapacity" "Enter NFS storage capacity (e.g., 1Ti, 500Gi)" "1Ti"
print_success "Configuration collected successfully"
# Templates should already be compiled by wild-cluster-services-generate
echo "Using pre-compiled NFS templates..."
if [ ! -d "${NFS_DIR}/kustomize" ]; then

View File

@@ -1,15 +1,4 @@
#!/bin/bash
set -e
set -o pipefail
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
print_header "Setting up SMTP Configuration"
@@ -19,7 +8,6 @@ echo ""
# Collect SMTP configuration
print_info "Collecting SMTP configuration..."
prompt_if_unset_config "cloud.smtp.host" "Enter SMTP host (e.g., email-smtp.us-east-2.amazonaws.com for AWS SES)" ""
prompt_if_unset_config "cloud.smtp.port" "Enter SMTP port (usually 465 for SSL, 587 for STARTTLS)" "465"
prompt_if_unset_config "cloud.smtp.user" "Enter SMTP username/access key" ""
@@ -47,7 +35,3 @@ echo " User: $(wild-config cloud.smtp.user)"
echo " From: $(wild-config cloud.smtp.from)"
echo " Password: $(wild-secret cloud.smtp.password >/dev/null 2>&1 && echo "✓ Set" || echo "✗ Not set")"
echo ""
echo "Applications that use SMTP: ghost, gitea, and others"
echo ""
echo "To test SMTP configuration, deploy an app that uses email (like Ghost)"
echo "and try the password reset or user invitation features."

View File

@@ -0,0 +1,5 @@
#!/bin/bash
print_info "Collecting Traefik configuration..."
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address for Traefik" "192.168.1.240"

View File

@@ -16,14 +16,6 @@ TRAEFIK_DIR="${CLUSTER_SETUP_DIR}/traefik"
print_header "Setting up Traefik ingress controller"
# Collect required configuration variables
print_info "Collecting Traefik configuration..."
# Prompt for configuration using helper functions
prompt_if_unset_config "cluster.loadBalancerIp" "Enter load balancer IP address for Traefik" "192.168.1.240"
print_success "Configuration collected successfully"
# Install required CRDs first
echo "Installing Gateway API CRDs..."
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml