mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586a4fa129 | ||
|
|
2a82f75cad | ||
|
|
805afd0d28 | ||
|
|
11098ea7d1 | ||
|
|
90e357a1dc | ||
|
|
91d58bc701 | ||
|
|
69d3a511d6 | ||
|
|
c3123820b2 | ||
|
|
2e49d94fad | ||
|
|
bb51e01c5c | ||
|
|
cdb2884e4e |
+17
-20
@@ -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"
|
||||||
@@ -99,21 +100,6 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a machine and container name resolver to properly format the plan output.
|
|
||||||
machines, err := clusterClient.ListMachines(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("list machines: %w", err)
|
|
||||||
}
|
|
||||||
machineNames := make(map[string]string, len(machines))
|
|
||||||
for _, m := range machines {
|
|
||||||
machineNames[m.Machine.Id] = m.Machine.Name
|
|
||||||
}
|
|
||||||
containerNames := make(map[string]string, len(svc.Containers))
|
|
||||||
for _, c := range svc.Containers {
|
|
||||||
containerNames[c.Container.ID] = c.Container.NameWithoutSlash()
|
|
||||||
}
|
|
||||||
resolver := client.NewNameResolver(machineNames, containerNames)
|
|
||||||
|
|
||||||
if opts.image == "" {
|
if opts.image == "" {
|
||||||
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||||
}
|
}
|
||||||
@@ -126,7 +112,7 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("plan caddy deployment: %w", err)
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plan.SequenceOperation.Operations) == 0 {
|
if len(plan.Operations) == 0 {
|
||||||
if opts.machine != "" {
|
if opts.machine != "" {
|
||||||
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
fmt.Printf("%s service is up to date on selected machines.\n", client.CaddyServiceName)
|
||||||
} else {
|
} else {
|
||||||
@@ -146,8 +132,14 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
fmt.Println("Deployment plan:")
|
fmt.Println("Deployment plan:")
|
||||||
fmt.Println(plan.Format(resolver))
|
fmt.Println(plan.Format(resolver))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -173,7 +165,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 +181,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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func main() {
|
|||||||
service.NewListCommand(),
|
service.NewListCommand(),
|
||||||
service.NewRmCommand(),
|
service.NewRmCommand(),
|
||||||
service.NewRunCommand(),
|
service.NewRunCommand(),
|
||||||
|
service.NewScaleCommand(),
|
||||||
)
|
)
|
||||||
cobra.CheckErr(cmd.Execute())
|
cobra.CheckErr(cmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
NewListCommand(),
|
NewListCommand(),
|
||||||
NewRmCommand(),
|
NewRmCommand(),
|
||||||
NewRunCommand(),
|
NewRunCommand(),
|
||||||
|
NewScaleCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"strconv"
|
||||||
|
"uncloud/internal/api"
|
||||||
|
"uncloud/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scaleOptions struct {
|
||||||
|
service string
|
||||||
|
replicas uint
|
||||||
|
cluster string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScaleCommand() *cobra.Command {
|
||||||
|
opts := scaleOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "scale SERVICE REPLICAS",
|
||||||
|
Short: "Scale a replicated service by changing the number of replicas.",
|
||||||
|
Long: "Scale a replicated service by changing the number of replicas. Scaling down requires confirmation.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
||||||
|
|
||||||
|
opts.service = args[0]
|
||||||
|
replicas, err := strconv.ParseUint(args[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid number of replicas: %w", err)
|
||||||
|
}
|
||||||
|
opts.replicas = uint(replicas)
|
||||||
|
|
||||||
|
return scale(cmd.Context(), uncli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(
|
||||||
|
&opts.cluster, "cluster", "c", "",
|
||||||
|
"Name of the cluster. (default is the current cluster)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error {
|
||||||
|
if opts.replicas == 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"scaling to zero replicas is not supported. This would effectively remove the service without preserving "+
|
||||||
|
"its configuration, making it impossible to scale back up. Uncloud derives the service configuration "+
|
||||||
|
"from existing containers. Use 'uc rm %s' instead if you want to remove the service",
|
||||||
|
opts.service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
|
}
|
||||||
|
defer clusterClient.Close()
|
||||||
|
|
||||||
|
svc, err := clusterClient.InspectService(ctx, opts.service)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect service '%s': %w", opts.service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Mode != api.ServiceModeReplicated {
|
||||||
|
return fmt.Errorf("scaling is only supported for services in %s mode, service '%s' is in %s mode",
|
||||||
|
api.ServiceModeReplicated, svc.Name, svc.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentReplicas := uint(len(svc.Containers))
|
||||||
|
|
||||||
|
if currentReplicas == opts.replicas {
|
||||||
|
fmt.Printf("Service '%s' already has %d replicas. No changes required.\n", svc.Name, currentReplicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale.
|
||||||
|
// This can happen if a service deployment failed midway and some containers were not updated.
|
||||||
|
// Derive the service spec from the first container.
|
||||||
|
spec, err := svc.Containers[0].Container.ServiceSpec()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get service spec from container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.Replicas = opts.replicas
|
||||||
|
|
||||||
|
deploy, err := clusterClient.NewDeployment(spec, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := deploy.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("Service '%s' is already scaled to %d replicas.\n", svc.Name, opts.replicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.replicas < currentReplicas {
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Ask for confirmation before scaling down as it may cause data loss.
|
||||||
|
confirmed, err := confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm scaling: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("Scaling service %s (%d → %d replicas)", svc.Name, currentReplicas, opts.replicas)
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err = deploy.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deploy service: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, uncli.ProgressOut(), title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirm() (bool, error) {
|
||||||
|
var confirmed bool
|
||||||
|
form := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewConfirm().
|
||||||
|
Title(
|
||||||
|
"Do you want to continue?",
|
||||||
|
).
|
||||||
|
Affirmative("Yes!").
|
||||||
|
Negative("No").
|
||||||
|
Value(&confirmed),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err := form.Run(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed, nil
|
||||||
|
}
|
||||||
@@ -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"},
|
||||||
|
|||||||
@@ -164,3 +164,22 @@ func (r *MapNameResolver) ContainerName(containerID string) string {
|
|||||||
}
|
}
|
||||||
return containerID
|
return containerID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceOperationNameResolver returns a machine and container name resolver for a service that can be used to format
|
||||||
|
// deployment operations.
|
||||||
|
func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Service) (*MapNameResolver, error) {
|
||||||
|
machines, err := cli.ListMachines(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list machines: %w", err)
|
||||||
|
}
|
||||||
|
machineNames := make(map[string]string, len(machines))
|
||||||
|
for _, m := range machines {
|
||||||
|
machineNames[m.Machine.Id] = m.Machine.Name
|
||||||
|
}
|
||||||
|
containerNames := make(map[string]string, len(svc.Containers))
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
containerNames[c.Container.ID] = c.Container.NameWithoutSlash()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewNameResolver(machineNames, containerNames), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user