Compare commits

...
10 Commits
7 changed files with 219 additions and 25 deletions
+9 -3
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"maps" "maps"
@@ -173,7 +174,11 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
fmt.Println() fmt.Println()
if _, err = clusterClient.GetDomain(ctx); err != nil { return UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
}
func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, progressOut *streams.Out) error {
if _, err := clusterClient.GetDomain(ctx); err != nil {
if errors.Is(err, client.ErrNotFound) { if errors.Is(err, client.ErrNotFound) {
fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').") fmt.Println("Skipping DNS records update as no cluster domain is reserved (see 'uc dns').")
return nil return nil
@@ -185,10 +190,11 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation. // TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
var records []*pb.DNSRecord var records []*pb.DNSRecord
err = progress.RunWithTitle(ctx, func(ctx context.Context) error { err := progress.RunWithTitle(ctx, func(ctx context.Context) error {
var err error
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName) records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
return err return err
}, uncli.ProgressOut(), "Verifying internet access to caddy service") }, progressOut, "Verifying internet access to caddy service")
if err != nil { if err != nil {
if errors.Is(err, client.ErrNoReachableMachines) { if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println() fmt.Println()
+8 -1
View File
@@ -12,6 +12,7 @@ import (
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"net/netip" "net/netip"
"time" "time"
"uncloud/cmd/uncloud/caddy"
"uncloud/internal/cli" "uncloud/internal/cli"
"uncloud/internal/cli/client" "uncloud/internal/cli/client"
"uncloud/internal/cli/config" "uncloud/internal/cli/config"
@@ -138,12 +139,18 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
return progress.RunWithTitle(ctx, func(ctx context.Context) error { err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil { if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err) return fmt.Errorf("deploy caddy: %w", err)
} }
return nil return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name)) }, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
} }
func waitClusterInitialised(ctx context.Context, client *client.Client) error { func waitClusterInitialised(ctx context.Context, client *client.Client) error {
+44 -15
View File
@@ -6,18 +6,23 @@ import (
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net/netip" "net/netip"
"uncloud/cmd/uncloud/caddy"
"uncloud/cmd/uncloud/dns"
"uncloud/internal/cli" "uncloud/internal/cli"
"uncloud/internal/cli/config" "uncloud/internal/cli/config"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/cluster" "uncloud/internal/machine/cluster"
) )
type initOptions struct { type initOptions struct {
name string dnsEndpoint string
network string name string
noCaddy bool network string
publicIP string noCaddy bool
sshKey string noDNS bool
cluster string publicIP string
sshKey string
cluster string
} }
func NewInitCommand() *cobra.Command { func NewInitCommand() *cobra.Command {
@@ -47,6 +52,8 @@ func NewInitCommand() *cobra.Command {
return initCluster(cmd.Context(), uncli, remoteMachine, opts) return initCluster(cmd.Context(), uncli, remoteMachine, opts)
}, },
} }
cmd.Flags().StringVar(&opts.dnsEndpoint, "dns-endpoint", dns.DefaultUncloudDNSAPIEndpoint,
"API endpoint for the Uncloud DNS service.")
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.name, "name", "n", "", &opts.name, "name", "n", "",
"Assign a name to the machine.", "Assign a name to the machine.",
@@ -59,6 +66,10 @@ func NewInitCommand() *cobra.Command {
&opts.noCaddy, "no-caddy", false, &opts.noCaddy, "no-caddy", false,
"Don't deploy Caddy reverse proxy service to the machine.", "Don't deploy Caddy reverse proxy service to the machine.",
) )
cmd.Flags().BoolVar(
&opts.noDNS, "no-dns", false,
"Don't reserve a cluster domain in Uncloud DNS.",
)
cmd.Flags().StringVar( cmd.Flags().StringVar(
&opts.publicIP, "public-ip", "auto", &opts.publicIP, "public-ip", "auto",
"Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+ "Public IP address of the machine for ingress configuration. Use 'auto' for automatic detection, "+
@@ -102,7 +113,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
defer client.Close() defer client.Close()
if opts.noCaddy { if opts.noCaddy && opts.noDNS {
return nil return nil
} }
@@ -111,15 +122,33 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
// after cluster initialisation, we keep the user informed during this wait. // after cluster initialisation, we keep the user informed during this wait.
fmt.Println("Waiting for the machine to be ready...") fmt.Println("Waiting for the machine to be ready...")
d, err := client.NewCaddyDeployment("", nil) if !opts.noDNS {
if err != nil { domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
return fmt.Errorf("create caddy deployment: %w", err) if err != nil {
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
}
fmt.Printf("Reserved cluster domain: %s\n", domain.Name)
} }
return progress.RunWithTitle(ctx, func(ctx context.Context) error { if !opts.noCaddy {
if _, err = d.Run(ctx); err != nil { d, err := client.NewCaddyDeployment("", nil)
return fmt.Errorf("deploy caddy: %w", err) if err != nil {
return fmt.Errorf("create caddy deployment: %w", err)
} }
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name)) err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
if _, err = d.Run(ctx); err != nil {
return fmt.Errorf("deploy caddy: %w", err)
}
return nil
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s", d.Spec.Name))
if err != nil {
return err
}
fmt.Println()
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
}
return nil
} }
-1
View File
@@ -31,7 +31,6 @@ func (cli *Client) NewCaddyDeployment(image string, filter MachineFilter) (*Depl
image = reference.FamiliarString(latest) image = reference.FamiliarString(latest)
} }
// TODO: set restart policy to always. https://github.com/psviderski/uncloud/issues/26
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Container: api.ContainerSpec{ Container: api.ContainerSpec{
Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"}, Command: []string{"caddy", "run", "-c", "/config/caddy.json", "--watch"},
+3 -2
View File
@@ -19,6 +19,7 @@ import (
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"slices"
"strconv" "strconv"
"uncloud/internal/corrosion" "uncloud/internal/corrosion"
"uncloud/internal/docker" "uncloud/internal/docker"
@@ -540,7 +541,7 @@ func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (
} }
publicIP, pubIPErr := network.GetPublicIP() publicIP, pubIPErr := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services. // Ignore the error if failed to get the public IP using API services.
if pubIPErr == nil { if pubIPErr == nil && !slices.Contains(ips, publicIP) {
ips = append(ips, publicIP) ips = append(ips, publicIP)
} }
endpoints := make([]*pb.IPPort, len(ips)) endpoints := make([]*pb.IPPort, len(ips))
@@ -682,7 +683,7 @@ func (m *Machine) Token(_ context.Context, _ *emptypb.Empty) (*pb.TokenResponse,
} }
publicIP, err := network.GetPublicIP() publicIP, err := network.GetPublicIP()
// Ignore the error if failed to get the public IP using API services. // Ignore the error if failed to get the public IP using API services.
if err == nil { if err == nil && !slices.Contains(ips, publicIP) {
ips = append(ips, publicIP) ips = append(ips, publicIP)
} }
endpoints := make([]netip.AddrPort, len(ips)) endpoints := make([]netip.AddrPort, len(ips))
+17 -3
View File
@@ -47,7 +47,7 @@ Uncloud supports only systemd-based Linux systems for now."
} }
install_docker() { install_docker() {
if command_exists docker; then if command_exists dockerd; then
log "✓ Docker is already installed." log "✓ Docker is already installed."
docker version docker version
return return
@@ -111,23 +111,37 @@ install_uncloud_binaries() {
trap "rm -rf '$tmp_dir'" EXIT trap "rm -rf '$tmp_dir'" EXIT
local uncloudd_url local uncloudd_url
local uninstall_url
if [ "${UNCLOUD_VERSION}" == "latest" ]; then if [ "${UNCLOUD_VERSION}" == "latest" ]; then
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/latest/download/uncloudd_linux_${file_arch}.tar.gz" uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/latest/download/uncloudd_linux_${file_arch}.tar.gz"
uninstall_url="https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh"
else else
uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/${UNCLOUD_VERSION}/uncloudd_linux_${file_arch}.tar.gz" uncloudd_url="${UNCLOUD_GITHUB_URL}/releases/download/${UNCLOUD_VERSION}/uncloudd_linux_${file_arch}.tar.gz"
uninstall_url="https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/${UNCLOUD_VERSION}/scripts/uninstall.sh"
fi fi
local uncloudd_download_path="${tmp_dir}/uncloudd.tar.gz" local uncloudd_download_path="${tmp_dir}/uncloudd.tar.gz"
local uninstall_download_path="${tmp_dir}/uninstall.sh"
log "⏳ Downloading uncloudd binary: ${uncloudd_url}" log "⏳ Downloading uncloudd binary: ${uncloudd_url}"
if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then if ! curl -fsSL -o "${uncloudd_download_path}" "${uncloudd_url}"; then
error "Failed to download uncloudd binary." error "Failed to download uncloudd binary."
fi fi
tar -xf "${uncloudd_download_path}" tar -xf "${uncloudd_download_path}" --directory "${tmp_dir}"
if ! install ./uncloudd "${uncloudd_install_path}"; then if ! install "${tmp_dir}/uncloudd" "${uncloudd_install_path}"; then
error "Failed to install uncloud binary to ${uncloudd_install_path}" error "Failed to install uncloud binary to ${uncloudd_install_path}"
fi fi
log "✓ uncloudd binary installed: ${uncloudd_install_path}" 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. # TODO: install uncloud CLI binary and create a uc alias.
} }
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
AUTO_CONFIRM=${AUTO_CONFIRM:-false}
# Define variables based on the same defaults used in the installation script.
INSTALL_BIN_DIR=${INSTALL_BIN_DIR:-/usr/local/bin}
INSTALL_SYSTEMD_DIR=${INSTALL_SYSTEMD_DIR:-/etc/systemd/system}
UNCLOUD_USER="uncloud"
UNCLOUD_DATA_DIR=${UNCLOUD_DATA_DIR:-/var/lib/uncloud}
UNCLOUD_RUN_DIR=${UNCLOUD_RUN_DIR:-/run/uncloud}
log() {
echo -e "\033[1;32m$1\033[0m"
}
error() {
echo -e "\033[1;31mERROR: $1\033[0m" >&2
exit 1
}
confirm() {
if [ "${AUTO_CONFIRM}" = "true" ]; then
return 0
fi
read -r -p "$1 [y/N] " response
case "$response" in
[yY][eE][sS]|[yY])
return 0
;;
*)
return 1
;;
esac
}
# Check if user is root.
if [ "$EUID" -ne 0 ]; then
error "Please run the uninstall script with sudo or as root."
fi
# Confirm before proceeding.
log "⚠️This script will uninstall Uncloud and remove ALL Uncloud managed containers on this machine."
log "The following actions will be performed:"
echo "- Remove Uncloud systemd services"
echo "- Remove Uncloud binaries and data"
echo "- Remove Uncloud user and group"
echo "- Remove all Docker containers managed by Uncloud"
echo "- Remove Uncloud Docker network"
echo "- Remove Uncloud WireGuard interface"
if ! confirm "Do you want to proceed with uninstallation?"; then
log "Uninstallation cancelled."
exit 0
fi
log "⏳ Stopping systemd services..."
systemctl stop uncloud.service || log "uncloud.service not running or doesn't exist."
systemctl stop uncloud-corrosion.service || log "uncloud-corrosion.service not running or doesn't exist."
systemctl disable uncloud.service || log "uncloud.service already disabled or doesn't exist."
systemctl disable uncloud-corrosion.service || log "uncloud-corrosion.service already disabled or doesn't exist."
log "✓ Systemd services stopped."
log "⏳ Removing systemd service files..."
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud.service"
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud-corrosion.service"
systemctl daemon-reload
log "✓ Systemd service files removed."
log "⏳ Removing binaries..."
rm -fv "${INSTALL_BIN_DIR}/uncloudd"
rm -fv "${INSTALL_BIN_DIR}/uncloud-corrosion"
log "✓ Binaries removed."
log "⏳ Removing data and run directories..."
rm -rfv "${UNCLOUD_DATA_DIR}"
rm -rfv "${UNCLOUD_RUN_DIR}"
log "✓ Data and run directories removed."
log "⏳ Removing Linux user and group..."
if id "${UNCLOUD_USER}" &> /dev/null; then
userdel "${UNCLOUD_USER}" || error "Failed to remove user ${UNCLOUD_USER}, it may still be used by other processes."
log "✓ Linux user '${UNCLOUD_USER}' removed."
else
log "Linux user '${UNCLOUD_USER}' does not exist."
fi
# Check if the group still exists (other users may be using it).
if getent group "${UNCLOUD_USER}" &> /dev/null; then
groupdel "${UNCLOUD_USER}" || error "Failed to remove group ${UNCLOUD_USER}, it may still be used by other processes or users."
log "✓ Linux group '${UNCLOUD_USER}' removed."
else
log "Linux group '${UNCLOUD_USER}' does not exist or was already removed."
fi
log "⏳ Looking for Docker containers and network created by Uncloud..."
if command -v docker &> /dev/null; then
uncloud_containers=$(docker ps -a --filter "label=uncloud.managed" -q)
if [ -n "${uncloud_containers}" ]; then
log "Found $(echo "${uncloud_containers}" | wc -w) Uncloud managed containers."
log "⏳ Stopping Uncloud managed containers..."
echo "${uncloud_containers}" | xargs docker stop || error "Failed to stop Uncloud managed containers."
log "⏳ Removing Uncloud managed containers..."
echo "${uncloud_containers}" | xargs docker rm || error "Failed to remove Uncloud managed containers."
log "✓ Uncloud managed containers stopped and removed."
else
log "No Uncloud managed containers found."
fi
uncloud_network=$(docker network ls --filter "name=uncloud" -q)
if [ -n "${uncloud_network}" ]; then
log "⏳ Removing Docker network uncloud..."
docker network rm uncloud || error "Failed to remove Docker network uncloud."
log "✓ Docker network uncloud removed."
else
log "Docker network uncloud not found."
fi
else
log "Docker CLI not found, skipping Docker container and network cleanup."
fi
log "⏳ Removing WireGuard interface uncloud..."
if ip link show uncloud &> /dev/null; then
ip link delete uncloud
log "✓ WireGuard interface uncloud removed."
else
log "WireGuard interface uncloud not found."
fi
log "⏳ Removing uninstall script..."
rm -fv "${INSTALL_BIN_DIR}/uncloud-uninstall"
log "✓ Uninstall script removed."
echo
log "✅ Uncloud has been uninstalled successfully!"
log "Note: Docker installation was preserved. If you want to completely remove Docker as well, \
follow https://docs.docker.com/engine/install/ubuntu/#uninstall-docker-engine"