mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 20:13:33 +00:00
chore: update 'uc machine init/add' to wait for cluster readiness, confirm caddy deployment on added machine
This commit is contained in:
+54
-37
@@ -8,7 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/charmbracelet/huh/spinner"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -16,8 +17,6 @@ import (
|
|||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addOptions struct {
|
type addOptions struct {
|
||||||
@@ -131,25 +130,37 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the cluster to be initialised to be able to deploy the Caddy service.
|
// Wait for the cluster to be initialised on the machine to be able to deploy the Caddy service.
|
||||||
fmt.Println("Waiting for the machine to be ready...")
|
err = spinner.New().
|
||||||
fmt.Println()
|
Title(" Waiting for the machine to join the cluster...").
|
||||||
if err = waitClusterInitialised(ctx, machineClient); err != nil {
|
Type(spinner.MiniDot).
|
||||||
return fmt.Errorf("wait for cluster to be initialised on machine: %w", err)
|
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
||||||
|
TitleStyle(lipgloss.NewStyle()).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return machineClient.WaitClusterReady(ctx, 5*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for machine to join the cluster: %w", err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("Machine joined the cluster.")
|
||||||
|
|
||||||
|
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
||||||
|
// that may cause a small downtime.
|
||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
// use the deployed image version. Otherwise, use the latest version.
|
// use the deployed image version.
|
||||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||||
// issues accessing the Machine API of existing machines in the cluster.
|
// issues accessing the Machine API of existing machines in the cluster.
|
||||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, api.ErrNotFound) {
|
if errors.Is(err, api.ErrNotFound) {
|
||||||
|
// Caddy service is not deployed.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
caddyImage = caddySvc.Containers[0].Container.Config.Image
|
||||||
// Find the latest created container and use its image.
|
// Find the latest created container and use its image.
|
||||||
var latestCreated time.Time
|
var latestCreated time.Time
|
||||||
@@ -163,47 +174,53 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
caddyImage = c.Container.Config.Image
|
caddyImage = c.Container.Config.Image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: scale the existing Caddy service to the new machine instead of running a new deployment
|
|
||||||
// that may cause a small downtime.
|
|
||||||
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create caddy deployment: %w", err)
|
return fmt.Errorf("create caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plan, err := d.Plan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if len(plan.Operations) == 0 {
|
||||||
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
// Initialise a machine and container name resolver to properly format the plan output.
|
||||||
|
resolver, err := clusterClient.ServiceOperationNameResolver(ctx, caddySvc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("caddy deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if !opts.yes {
|
||||||
|
confirmed, err := cli.Confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = 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 (%s mode)", d.Spec.Name, d.Spec.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
return caddy.UpdateDomainRecords(ctx, machineClient, uncli.ProgressOut())
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitClusterInitialised(ctx context.Context, client *client.Client) error {
|
|
||||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
|
||||||
backoff.WithMaxInterval(1*time.Second),
|
|
||||||
backoff.WithMaxElapsedTime(5*time.Minute),
|
|
||||||
), ctx)
|
|
||||||
|
|
||||||
check := func() error {
|
|
||||||
_, err := client.ListMachines(ctx, nil)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
statusErr := status.Convert(err)
|
|
||||||
if statusErr.Code() == codes.FailedPrecondition {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return backoff.Permanent(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return backoff.Retry(check, boff)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh/spinner"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
"github.com/psviderski/uncloud/cmd/uncloud/caddy"
|
||||||
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
"github.com/psviderski/uncloud/cmd/uncloud/dns"
|
||||||
@@ -173,14 +176,27 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
|
// Since the cluster API needs a few moments to become ready after cluster initialisation,
|
||||||
|
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
|
||||||
|
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
|
||||||
|
err = spinner.New().
|
||||||
|
Title(" Waiting for the cluster to be ready...").
|
||||||
|
Type(spinner.MiniDot).
|
||||||
|
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
|
||||||
|
TitleStyle(lipgloss.NewStyle()).
|
||||||
|
ActionWithErr(func(ctx context.Context) error {
|
||||||
|
return client.WaitClusterReady(ctx, 1*time.Minute)
|
||||||
|
}).
|
||||||
|
Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for cluster to be ready: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cluster is ready.")
|
||||||
|
|
||||||
if opts.noCaddy && opts.noDNS {
|
if opts.noCaddy && opts.noDNS {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy the Caddy service to the initialised machine.
|
|
||||||
// The creation of a deployment plan talks to cluster API. Since the API needs a few moments to become available
|
|
||||||
// after cluster initialisation, we keep the user informed during this wait.
|
|
||||||
fmt.Println("Waiting for the machine to be ready...")
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if !opts.noDNS {
|
if !opts.noDNS {
|
||||||
|
|||||||
Reference in New Issue
Block a user