mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
372 lines
14 KiB
Bash
Executable File
372 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# Avoid locale warnings (e.g. on apt install) from SSH-forwarded LANG/LC_* on minimal hosts. C.UTF-8 is built into
|
|
# glibc and present on all supported distros, unlike locale-specific values like en_US.UTF-8 that may not be generated.
|
|
export LC_ALL="${LC_ALL:-C.UTF-8}"
|
|
|
|
# If set to 'true', only install the packages and dependencies, without running, reloading, or
|
|
# restarting services or systemd.
|
|
INSTALL_ONLY=${INSTALL_ONLY:-false}
|
|
|
|
INSTALL_BIN_DIR=${INSTALL_BIN_DIR:-/usr/local/bin}
|
|
INSTALL_SYSTEMD_DIR=${INSTALL_SYSTEMD_DIR:-/etc/systemd/system}
|
|
|
|
UNCLOUD_GITHUB_URL="https://github.com/psviderski/uncloud"
|
|
UNCLOUD_VERSION=${UNCLOUD_VERSION:-latest}
|
|
# Remove the 'v' prefix from the version if it exists.
|
|
UNCLOUD_VERSION=${UNCLOUD_VERSION#v}
|
|
UNCLOUD_USER="uncloud"
|
|
# Add the specified Linux user to group $UNCLOUD_USER to allow the user to run uncloud commands without sudo.
|
|
UNCLOUD_GROUP_ADD_USER=${UNCLOUD_GROUP_ADD_USER:-}
|
|
UNCLOUD_DATA_DIR=${UNCLOUD_DATA_DIR:-/var/lib/uncloud}
|
|
|
|
DOCKER_ALREADY_INSTALLED=false
|
|
CONTAINERD_IMAGE_STORE_ENABLED=false
|
|
DOCKER_DAEMON_CONFIG_FILE=${DOCKER_DAEMON_CONFIG_FILE:-/etc/docker/daemon.json}
|
|
# Docker daemon configuration optimised for Uncloud.
|
|
DOCKER_DAEMON_CONFIG='{
|
|
"features": {
|
|
"containerd-snapshotter": true
|
|
},
|
|
"live-restore": true
|
|
}'
|
|
|
|
log() {
|
|
echo -e "\033[1;32m$1\033[0m"
|
|
}
|
|
|
|
warning() {
|
|
echo -e "\033[1;33m$1\033[0m"
|
|
}
|
|
|
|
error() {
|
|
echo -e "\033[1;31mERROR: $1\033[0m" >&2
|
|
exit 1
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
verify_system() {
|
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
error "Uncloud machine must be a Linux system. Your system ($(uname -s)) is not supported."
|
|
fi
|
|
|
|
local arch
|
|
arch=$(uname -m)
|
|
if [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]]; then
|
|
error "Uncloud machine must have amd64 (x86_64) or arm64 (aarch64) architecture. \
|
|
Your system architecture ($arch) is not supported."
|
|
fi
|
|
|
|
if [[ ! -d /run/systemd/system && "${INSTALL_ONLY}" != "true" ]]; then
|
|
error "Cannot find systemd to use as a service manager for the Uncloud machine daemon. \
|
|
Uncloud supports only systemd-based Linux systems for now."
|
|
fi
|
|
}
|
|
|
|
# install_prerequisites ensures curl is available, installing it with the system package manager if needed.
|
|
install_prerequisites() {
|
|
if command_exists curl; then
|
|
return
|
|
fi
|
|
|
|
log "⏳ curl is required but not installed, installing it using the system package manager..."
|
|
|
|
if command_exists apt-get; then
|
|
apt-get update -qq >/dev/null
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates
|
|
elif command_exists dnf; then
|
|
dnf install -y curl ca-certificates
|
|
elif command_exists yum; then
|
|
yum install -y curl ca-certificates
|
|
elif command_exists pacman; then
|
|
pacman -Sy --noconfirm curl ca-certificates
|
|
elif command_exists zypper; then
|
|
zypper --non-interactive refresh >/dev/null
|
|
zypper --non-interactive install curl ca-certificates
|
|
else
|
|
error "curl is required but not installed, and no supported package manager \
|
|
(apt, dnf, yum, pacman, zypper) was found to install it automatically. \
|
|
Please install curl manually and re-run the command."
|
|
fi
|
|
|
|
log "✓ curl installed."
|
|
}
|
|
|
|
install_docker() {
|
|
if command_exists dockerd; then
|
|
log "✓ Docker is already installed."
|
|
DOCKER_ALREADY_INSTALLED=true
|
|
|
|
if [[ "${INSTALL_ONLY}" == "true" ]]; then
|
|
return
|
|
fi
|
|
|
|
docker version
|
|
|
|
# Check if the installed Docker configured to use the containerd image store.
|
|
local driver_status
|
|
driver_status=$(docker info -f '{{ .DriverStatus }}' 2>/dev/null)
|
|
if [[ "$driver_status" == *"io.containerd.snapshotter"* ]]; then
|
|
CONTAINERD_IMAGE_STORE_ENABLED="true"
|
|
fi
|
|
|
|
return
|
|
fi
|
|
|
|
log "⏳ Installing Docker..."
|
|
curl -fsSL https://get.docker.com | sh
|
|
|
|
# Configure Docker daemon for new installation.
|
|
# Create Docker daemon config directory if it doesn't exist.
|
|
local docker_config_dir
|
|
docker_config_dir=$(dirname "${DOCKER_DAEMON_CONFIG_FILE}")
|
|
if [ ! -d "${docker_config_dir}" ]; then
|
|
mkdir -p "${docker_config_dir}"
|
|
fi
|
|
|
|
log "⏳ Configuring Docker daemon (${DOCKER_DAEMON_CONFIG_FILE}) to optimise it for Uncloud..."
|
|
echo "${DOCKER_DAEMON_CONFIG}" > "${DOCKER_DAEMON_CONFIG_FILE}"
|
|
|
|
if [[ "${INSTALL_ONLY}" != "true" ]]; then
|
|
systemctl restart docker
|
|
fi
|
|
|
|
log "✓ Docker installed and configured successfully."
|
|
}
|
|
|
|
create_uncloud_user_and_group() {
|
|
if id "${UNCLOUD_USER}" &> /dev/null; then
|
|
log "✓ Linux user '${UNCLOUD_USER}' already exists."
|
|
else
|
|
# In addition to creating the user, create a group with the same name as the user.
|
|
if ! useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin --user-group "${UNCLOUD_USER}"; then
|
|
error "Failed to create Linux user '${UNCLOUD_USER}'."
|
|
fi
|
|
log "✓ Linux user and group '${UNCLOUD_USER}' created."
|
|
fi
|
|
|
|
if [ -n "${UNCLOUD_GROUP_ADD_USER}" ]; then
|
|
if ! gpasswd --add "${UNCLOUD_GROUP_ADD_USER}" "${UNCLOUD_USER}" > /dev/null; then
|
|
error "Failed to add user '${UNCLOUD_GROUP_ADD_USER}' to group '${UNCLOUD_USER}'."
|
|
fi
|
|
log "✓ Linux user '${UNCLOUD_GROUP_ADD_USER}' added to group '${UNCLOUD_USER}'."
|
|
fi
|
|
}
|
|
|
|
install_uncloud_binaries() {
|
|
local arch
|
|
local file_arch
|
|
|
|
arch=$(uname -m)
|
|
case $arch in
|
|
x86_64)
|
|
file_arch="amd64"
|
|
;;
|
|
aarch64)
|
|
file_arch="arm64"
|
|
;;
|
|
*)
|
|
error "Unsupported architecture: ${arch}"
|
|
;;
|
|
esac
|
|
|
|
local uncloudd_install_path="${INSTALL_BIN_DIR}/uncloudd"
|
|
local installed_version=""
|
|
if [ -f "${uncloudd_install_path}" ]; then
|
|
installed_version=$("${uncloudd_install_path}" version -o '{{.Version}}' 2>/dev/null || true)
|
|
fi
|
|
|
|
# Decide whether to (re)install based on UNCLOUD_VERSION and the already installed binary, and pick the
|
|
# download URL for the same channel/tag.
|
|
local uncloudd_url uninstall_ref
|
|
local uncloudd_archive_name="uncloudd_linux_${file_arch}.tar.gz"
|
|
|
|
case "${UNCLOUD_VERSION}" in
|
|
nightly)
|
|
log "⏳ Installing uncloudd from the nightly channel (installed: ${installed_version:-none})..."
|
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/nightly/${uncloudd_archive_name}"
|
|
# The 'nightly' tag is deleted and recreated on every nightly build, so pin uninstall.sh to main
|
|
# to avoid racing.
|
|
uninstall_ref="refs/heads/main"
|
|
;;
|
|
latest)
|
|
# Resolve the redirect of releases/latest to discover the concrete latest version.
|
|
local latest_url latest_version
|
|
latest_url=$(curl -sLI -o /dev/null -w '%{url_effective}' \
|
|
"${UNCLOUD_GITHUB_URL}/releases/latest" 2>/dev/null || true)
|
|
latest_version="${latest_url##*/}"
|
|
latest_version="${latest_version#v}"
|
|
|
|
uninstall_ref="refs/tags/v${latest_version}"
|
|
|
|
if [ -z "${latest_version}" ] || [ "${latest_version}" = "latest" ]; then
|
|
warning "Could not resolve the version of the latest release on GitHub."
|
|
# Fall back to the redirecting URL so curl follows whatever GitHub considers latest.
|
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/latest/download/${uncloudd_archive_name}"
|
|
uninstall_ref="refs/heads/main"
|
|
elif [ -z "${installed_version}" ]; then
|
|
log "⏳ Installing uncloudd ${latest_version} (latest stable)..."
|
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/v${latest_version}/${uncloudd_archive_name}"
|
|
elif [ "${installed_version}" = "${latest_version}" ]; then
|
|
log "✓ uncloudd ${installed_version} is already the latest stable version."
|
|
return 0
|
|
else
|
|
# Substitute '-' with '~' so sort -V treats pre-release versions per SemVer (Debian-style rules):
|
|
# 0.20.0~nightly-abc < 0.20.0 < 0.21.0~nightly-def.
|
|
# latest_version is always a clean stable tag from releases/latest, so the substitution is one-sided.
|
|
local newest
|
|
newest=$(printf '%s\n%s\n' "${installed_version//-/~}" "${latest_version}" | sort -V | tail -n1)
|
|
if [ "${newest}" = "${latest_version}" ]; then
|
|
log "⏳ Upgrading uncloudd ${installed_version} → ${latest_version}..."
|
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/v${latest_version}/${uncloudd_archive_name}"
|
|
else
|
|
log "✓ uncloudd ${installed_version} is newer than the latest stable ${latest_version}, keeping it."
|
|
return 0
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
# Explicit version. Install if it differs from the installed one (covers upgrade and downgrade).
|
|
if [ "${installed_version}" = "${UNCLOUD_VERSION}" ]; then
|
|
log "✓ uncloudd ${installed_version} matches the requested version, keeping it."
|
|
return 0
|
|
fi
|
|
log "⏳ Installing uncloudd ${UNCLOUD_VERSION} (replacing ${installed_version:-none})..."
|
|
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/v${UNCLOUD_VERSION}/${uncloudd_archive_name}"
|
|
uninstall_ref="refs/tags/v${UNCLOUD_VERSION}"
|
|
;;
|
|
esac
|
|
local uninstall_url="https://raw.githubusercontent.com/psviderski/uncloud/${uninstall_ref}/scripts/uninstall.sh"
|
|
|
|
# Create a temporary directory for downloads.
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
# Ensure the temporary directory is deleted on script exit.
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '$tmp_dir'" EXIT
|
|
|
|
local uncloudd_download_path="${tmp_dir}/uncloudd.tar.gz"
|
|
local uninstall_download_path="${tmp_dir}/uninstall.sh"
|
|
|
|
log "⏳ Downloading uncloudd binary: ${uncloudd_url}"
|
|
if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then
|
|
error "Failed to download uncloudd binary."
|
|
fi
|
|
tar -xf "${uncloudd_download_path}" --directory "${tmp_dir}"
|
|
if ! install "${tmp_dir}/uncloudd" "${uncloudd_install_path}"; then
|
|
error "Failed to install uncloud binary to ${uncloudd_install_path}"
|
|
fi
|
|
log "✓ uncloudd binary installed: ${uncloudd_install_path}"
|
|
|
|
log "⏳ Downloading uninstall script: ${uninstall_url}"
|
|
if ! curl -fsSL -o "${uninstall_download_path}" "${uninstall_url}"; then
|
|
error "Failed to download uninstall script."
|
|
fi
|
|
local uninstall_install_path="${INSTALL_BIN_DIR}/uncloud-uninstall"
|
|
if ! install "${uninstall_download_path}" "${uninstall_install_path}"; then
|
|
error "Failed to install uninstall.sh script to ${uninstall_install_path}"
|
|
fi
|
|
log "✓ uncloud-uninstall script installed: ${uninstall_install_path}"
|
|
|
|
# TODO: install uncloud CLI binary and create a uc alias.
|
|
}
|
|
|
|
install_uncloud_systemd() {
|
|
local uncloud_service_path="${INSTALL_SYSTEMD_DIR}/uncloud.service"
|
|
cat > "${uncloud_service_path}" << EOF
|
|
[Unit]
|
|
Description=Uncloud machine daemon
|
|
After=network-online.target docker.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=notify
|
|
ExecStart=${INSTALL_BIN_DIR}/uncloudd
|
|
TimeoutStartSec=20
|
|
Restart=always
|
|
RestartSec=2
|
|
|
|
# Hardening options.
|
|
NoNewPrivileges=true
|
|
ProtectSystem=full
|
|
ProtectControlGroups=true
|
|
ProtectHome=read-only
|
|
ProtectKernelTunables=true
|
|
PrivateTmp=true
|
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
|
RestrictNamespaces=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
log "✓ Systemd unit file created: ${uncloud_service_path}"
|
|
|
|
|
|
if [[ "${INSTALL_ONLY}" != "true" ]]; then
|
|
# Reload systemd to recognize the new or updated unit file.
|
|
systemctl daemon-reload
|
|
fi
|
|
systemctl enable uncloud.service
|
|
}
|
|
|
|
start_uncloud() {
|
|
if [[ "${INSTALL_ONLY}" == "true" ]]; then
|
|
return
|
|
fi
|
|
|
|
log "⏳ Starting Uncloud machine daemon (uncloud.service)..."
|
|
systemctl restart uncloud.service
|
|
log "✓ Uncloud machine daemon started."
|
|
}
|
|
|
|
log "⏳ Running Uncloud install script..."
|
|
|
|
if [ "$EUID" -ne 0 ]; then
|
|
error "Please run the install script with sudo or as root."
|
|
fi
|
|
|
|
verify_system
|
|
install_prerequisites
|
|
install_docker
|
|
create_uncloud_user_and_group
|
|
install_uncloud_binaries
|
|
install_uncloud_systemd
|
|
start_uncloud
|
|
|
|
# Show warning if Docker was already installed without containerd image store enabled.
|
|
if [ "$DOCKER_ALREADY_INSTALLED" = "true" ] && [ "$CONTAINERD_IMAGE_STORE_ENABLED" = "false" ]; then
|
|
echo ""
|
|
warning "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
warning "⚠️ IMPORTANT: Containerd image store configuration"
|
|
warning "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
warning "Docker was already installed on the machine but it doesn't use the containerd"
|
|
warning "image store. Uncloud works best with the containerd image store enabled in Docker."
|
|
warning "It allows Docker to directly use the images stored in containerd (pushed with"
|
|
warning "'uc deploy' or 'uc image push') without duplicating them in Docker. This saves"
|
|
warning "disk space and makes image management more efficient."
|
|
echo ""
|
|
warning "See https://docs.docker.com/engine/storage/containerd/ for more details."
|
|
echo ""
|
|
warning "To enable it, run the following commands on the machine:"
|
|
echo ""
|
|
echo "sudo bash -c 'cat > ${DOCKER_DAEMON_CONFIG_FILE} << EOF"
|
|
echo "${DOCKER_DAEMON_CONFIG}"
|
|
echo "EOF'"
|
|
echo "sudo systemctl restart docker"
|
|
echo ""
|
|
warning "WARNING: Switching to containerd image store causes you to temporarily lose images"
|
|
warning "and containers created using the classic storage driver. Those resources still"
|
|
warning "exist on your filesystem, and you can retrieve them by turning off the containerd"
|
|
warning "image store feature."
|
|
warning "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
fi
|
|
|
|
log "✓ Uncloud installed on the machine successfully! 🎉"
|