mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 03:53:33 +00:00
feat: print deployment plan for caddy deployment
This commit is contained in:
+100
-5
@@ -2,10 +2,16 @@ package caddy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"uncloud/internal/cli"
|
"uncloud/internal/cli"
|
||||||
|
"uncloud/internal/cli/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type deployOptions struct {
|
type deployOptions struct {
|
||||||
@@ -38,24 +44,93 @@ func NewDeployCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||||
client, err := uncli.ConnectCluster(ctx, opts.cluster)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.cluster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to cluster: %w", err)
|
return fmt.Errorf("connect to cluster: %w", err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer clusterClient.Close()
|
||||||
|
|
||||||
d, err := client.NewCaddyDeployment(opts.image)
|
svc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, client.ErrNotFound) {
|
||||||
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
|
||||||
|
|
||||||
|
// Collect unique images of all containers in the running caddy service.
|
||||||
|
images := make(map[string]struct{}, len(svc.Containers))
|
||||||
|
for _, c := range svc.Containers {
|
||||||
|
images[c.Container.Config.Image] = struct{}{}
|
||||||
|
}
|
||||||
|
currentImages := slices.Collect(maps.Keys(images))
|
||||||
|
|
||||||
|
if len(currentImages) > 1 {
|
||||||
|
commaSeparatedImages := strings.Join(currentImages, ", ")
|
||||||
|
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Current image: %s\n", currentImages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.image != "" {
|
||||||
|
fmt.Printf("Target image: %s\n", opts.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Preparing a deployment plan...")
|
||||||
|
d, err := clusterClient.NewCaddyDeployment(opts.image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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 == "" {
|
||||||
|
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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.SequenceOperation.Operations) == 0 {
|
||||||
fmt.Println("caddy service is up to date.")
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.ID == "" {
|
||||||
|
fmt.Println("This will run a Caddy container on each machine.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("This will perform a rolling update of Caddy containers on each machine.")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("Deployment plan:")
|
||||||
|
fmt.Println(plan.Format(resolver))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
confirmed, err := confirm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Println("Cancelled. No changes were made.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,5 +139,25 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("deploy caddy: %w", err)
|
return fmt.Errorf("deploy caddy: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, uncli.ProgressOut(), "Deploying service "+d.Spec.Name)
|
}, uncli.ProgressOut(), fmt.Sprintf("Deploying service %s (%s mode)", d.Spec.Name, d.Spec.Mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ import (
|
|||||||
// Operations can be composed to form complex deployment strategies.
|
// Operations can be composed to form complex deployment strategies.
|
||||||
type Operation interface {
|
type Operation interface {
|
||||||
Execute(ctx context.Context, cli *Client) error
|
Execute(ctx context.Context, cli *Client) error
|
||||||
|
// Format returns a human-readable representation of the operation.
|
||||||
|
Format(resolver NameResolver) string
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NameResolver interface {
|
||||||
|
MachineName(machineID string) string
|
||||||
|
ContainerName(containerID string) string
|
||||||
|
}
|
||||||
|
|
||||||
// RunContainerOperation creates and starts a new container on a specific machine.
|
// RunContainerOperation creates and starts a new container on a specific machine.
|
||||||
type RunContainerOperation struct {
|
type RunContainerOperation struct {
|
||||||
ServiceID string
|
ServiceID string
|
||||||
@@ -36,8 +43,14 @@ func (o *RunContainerOperation) Execute(ctx context.Context, cli *Client) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *RunContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Run container [image=%s]", machineName, o.Spec.Container.Image)
|
||||||
|
}
|
||||||
|
|
||||||
func (o *RunContainerOperation) String() string {
|
func (o *RunContainerOperation) String() string {
|
||||||
return fmt.Sprintf("RunContainerOperation[%s, %s, %s]", o.ServiceID, o.Spec.Name, o.MachineID)
|
return fmt.Sprintf("RunContainerOperation[service_id=%s, image=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.Spec.Container.Image, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopContainerOperation stops a container on a specific machine.
|
// StopContainerOperation stops a container on a specific machine.
|
||||||
@@ -54,8 +67,14 @@ func (o *StopContainerOperation) Execute(ctx context.Context, cli *Client) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *StopContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Stop container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *StopContainerOperation) String() string {
|
func (o *StopContainerOperation) String() string {
|
||||||
return fmt.Sprintf("StopContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
return fmt.Sprintf("StopContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveContainerOperation stops and removes a container from a specific machine.
|
// RemoveContainerOperation stops and removes a container from a specific machine.
|
||||||
@@ -76,8 +95,14 @@ func (o *RemoveContainerOperation) Execute(ctx context.Context, cli *Client) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *RemoveContainerOperation) Format(resolver NameResolver) string {
|
||||||
|
machineName := resolver.MachineName(o.MachineID)
|
||||||
|
return fmt.Sprintf("%s: Remove container [name=%s]", machineName, resolver.ContainerName(o.ContainerID))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *RemoveContainerOperation) String() string {
|
func (o *RemoveContainerOperation) String() string {
|
||||||
return fmt.Sprintf("RemoveContainerOperation[%s, %s, %s]", o.ServiceID, o.ContainerID, o.MachineID)
|
return fmt.Sprintf("RemoveContainerOperation[service_id=%s, container_id=%s, machine_id=%s]",
|
||||||
|
o.ServiceID, o.ContainerID, o.MachineID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
// SequenceOperation is a composite operation that executes a sequence of operations in order.
|
||||||
@@ -94,6 +119,15 @@ func (o *SequenceOperation) Execute(ctx context.Context, cli *Client) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *SequenceOperation) Format(resolver NameResolver) string {
|
||||||
|
ops := make([]string, len(o.Operations))
|
||||||
|
for i, op := range o.Operations {
|
||||||
|
ops[i] = "- " + op.Format(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ops, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
func (o *SequenceOperation) String() string {
|
func (o *SequenceOperation) String() string {
|
||||||
ops := make([]string, len(o.Operations))
|
ops := make([]string, len(o.Operations))
|
||||||
for i, op := range o.Operations {
|
for i, op := range o.Operations {
|
||||||
@@ -102,3 +136,30 @@ func (o *SequenceOperation) String() string {
|
|||||||
|
|
||||||
return fmt.Sprintf("SequenceOperation[%s]", strings.Join(ops, ", "))
|
return fmt.Sprintf("SequenceOperation[%s]", strings.Join(ops, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapNameResolver resolves machine and container IDs to their names using a static map.
|
||||||
|
type MapNameResolver struct {
|
||||||
|
machines map[string]string
|
||||||
|
containers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNameResolver(machines, containers map[string]string) *MapNameResolver {
|
||||||
|
return &MapNameResolver{
|
||||||
|
machines: machines,
|
||||||
|
containers: containers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MapNameResolver) MachineName(machineID string) string {
|
||||||
|
if name, ok := r.machines[machineID]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return machineID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MapNameResolver) ContainerName(containerID string) string {
|
||||||
|
if name, ok := r.containers[containerID]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user