mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
refactor: format deployment plan for caddy (uc caddy deploy)
This commit is contained in:
+52
-10
@@ -9,6 +9,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -71,9 +73,11 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
if !errors.Is(err, api.ErrNotFound) {
|
if !errors.Is(err, api.ErrNotFound) {
|
||||||
return fmt.Errorf("inspect caddy service: %w", err)
|
return fmt.Errorf("inspect caddy service: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Service: %s (not running)\n", client.CaddyServiceName)
|
fmt.Println(tui.Faint.Render("service: ") + tui.NameStyle.Render(client.CaddyServiceName) +
|
||||||
|
tui.Faint.Render(" (not running)"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Service: %s (%s mode)\n", svc.Name, svc.Mode)
|
fmt.Println(tui.Faint.Render("service: ") + tui.NameStyle.Render(svc.Name) +
|
||||||
|
tui.Faint.Render(" ("+svc.Mode+" mode)"))
|
||||||
|
|
||||||
// Collect unique images of all containers in the running caddy service.
|
// Collect unique images of all containers in the running caddy service.
|
||||||
images := make(map[string]struct{}, len(svc.Containers))
|
images := make(map[string]struct{}, len(svc.Containers))
|
||||||
@@ -83,15 +87,22 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
currentImages := slices.Collect(maps.Keys(images))
|
currentImages := slices.Collect(maps.Keys(images))
|
||||||
|
|
||||||
if len(currentImages) > 1 {
|
if len(currentImages) > 1 {
|
||||||
commaSeparatedImages := strings.Join(currentImages, ", ")
|
formattedImages := make([]string, len(currentImages))
|
||||||
fmt.Printf("Current images (multiple versions detected): %s\n", commaSeparatedImages)
|
for i, img := range currentImages {
|
||||||
|
ref, _ := reference.ParseDockerRef(img)
|
||||||
|
formattedImages[i] = tui.FormatImage(ref, lipgloss.NewStyle())
|
||||||
|
}
|
||||||
|
fmt.Println(tui.Faint.Render("current images (multiple versions detected): ") +
|
||||||
|
strings.Join(formattedImages, tui.Faint.Render(", ")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Current image: %s\n", currentImages[0])
|
ref, _ := reference.ParseDockerRef(currentImages[0])
|
||||||
|
fmt.Println(tui.Faint.Render("current image: ") + tui.FormatImage(ref, lipgloss.NewStyle()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.image != "" {
|
if opts.image != "" {
|
||||||
fmt.Printf("Target image: %s\n", opts.image)
|
ref, _ := reference.ParseDockerRef(opts.image)
|
||||||
|
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(ref, tui.Green))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -106,7 +117,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.image == "" {
|
if opts.image == "" {
|
||||||
fmt.Printf("Target image: %s (latest stable)\n", d.Spec.Container.Image)
|
ref, _ := reference.ParseDockerRef(d.Spec.Container.Image)
|
||||||
|
fmt.Println(tui.Faint.Render("target image: ") + tui.FormatImage(ref,
|
||||||
|
tui.Green) + tui.Faint.Render(" (latest stable)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := d.Plan(ctx)
|
plan, err := d.Plan(ctx)
|
||||||
@@ -126,11 +139,40 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Deployment plan:")
|
fmt.Println(tui.Bold.Underline(true).Render("Deployment plan"))
|
||||||
fmt.Println(plan.Format())
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
confirmed, err := tui.Confirm("")
|
directConn := uncli.DirectConnection()
|
||||||
|
contextName := uncli.ContextOverrideOrCurrent()
|
||||||
|
deployTarget := ""
|
||||||
|
if directConn != "" {
|
||||||
|
deployTarget = directConn
|
||||||
|
fmt.Println(tui.Faint.Render("connection: ") + tui.NameStyle.Render(directConn))
|
||||||
|
fmt.Println()
|
||||||
|
} else if contextName != "" && len(uncli.Config.Contexts) > 1 {
|
||||||
|
// Only show context if there's more than one to avoid unnecessary clutter.
|
||||||
|
deployTarget = contextName
|
||||||
|
fmt.Println(tui.Faint.Render("context: ") + tui.NameStyle.Render(contextName))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(plan.Format())
|
||||||
|
|
||||||
|
summary := plan.FormatSummary()
|
||||||
|
fmt.Println(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
fmt.Println(summary)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
title := "Proceed with deployment?"
|
||||||
|
// Include the direct connection or context name in the confirmation prompt to avoid accidentally
|
||||||
|
// deploying to the wrong cluster.
|
||||||
|
if deployTarget != "" {
|
||||||
|
isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
|
||||||
|
confirmStyle := tui.ThemeConfirm().Theme(isDark).Focused.Title
|
||||||
|
title = "Proceed with deployment to " + tui.NameStyle.Render(deployTarget) + confirmStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmed, err := tui.Confirm(title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm deployment: %w", err)
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Preparing Caddy deployment...")
|
||||||
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)
|
||||||
@@ -210,15 +212,20 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
return fmt.Errorf("plan caddy deployment: %w", err)
|
return fmt.Errorf("plan caddy deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
if len(plan.Operations) == 0 {
|
if len(plan.Operations) == 0 {
|
||||||
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
fmt.Printf("%s service is up to date.\n", client.CaddyServiceName)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("caddy deployment plan:")
|
fmt.Println(tui.Bold.Underline(true).Render("Deployment plan"))
|
||||||
fmt.Println(plan.Format())
|
fmt.Println()
|
||||||
|
fmt.Print(plan.Format())
|
||||||
|
|
||||||
|
summary := plan.FormatSummary()
|
||||||
|
fmt.Println(tui.Faint.Render(strings.Repeat("─", lipgloss.Width(summary))))
|
||||||
|
fmt.Println(summary)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
if !opts.yes {
|
if !opts.yes {
|
||||||
confirmed, err := tui.Confirm("")
|
confirmed, err := tui.Confirm("Proceed with deployment?")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("confirm deployment: %w", err)
|
return fmt.Errorf("confirm deployment: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "charm.land/lipgloss/v2"
|
import (
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Faint = lipgloss.NewStyle().Faint(true)
|
Faint = lipgloss.NewStyle().Faint(true)
|
||||||
@@ -15,3 +18,13 @@ var (
|
|||||||
|
|
||||||
NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152"))
|
NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FormatImage renders an image reference with the given style, using a faint colon separator for tagged images.
|
||||||
|
func FormatImage(image reference.Named, style lipgloss.Style) string {
|
||||||
|
if tagged, ok := image.(reference.NamedTagged); ok {
|
||||||
|
return style.Render(reference.FamiliarName(image)) +
|
||||||
|
Faint.Render(":") +
|
||||||
|
style.Render(tagged.Tag())
|
||||||
|
}
|
||||||
|
return style.Render(reference.FamiliarString(image))
|
||||||
|
}
|
||||||
|
|||||||
+61
-16
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
@@ -163,6 +164,60 @@ func (sp *ServicePlan) Format() string {
|
|||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatSummary counts operations in the service plan and renders a styled summary line.
|
||||||
|
func (sp *ServicePlan) FormatSummary() string {
|
||||||
|
var createCount, startFirstCount, stopFirstCount, removeCount int
|
||||||
|
machines := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, op := range sp.Operations {
|
||||||
|
switch o := op.(type) {
|
||||||
|
case *operation.RunContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
createCount++
|
||||||
|
case *operation.ReplaceContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
if o.Order == api.UpdateOrderStopFirst {
|
||||||
|
stopFirstCount++
|
||||||
|
} else {
|
||||||
|
startFirstCount++
|
||||||
|
}
|
||||||
|
case *operation.RemoveContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
removeCount++
|
||||||
|
case *operation.StopContainerOperation:
|
||||||
|
machines[o.MachineID] = struct{}{}
|
||||||
|
removeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
if createCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldGreen.Render(strconv.Itoa(createCount))+" "+tui.Green.Render("create"))
|
||||||
|
}
|
||||||
|
if startFirstCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldGreen.Render(strconv.Itoa(startFirstCount))+" "+tui.Green.Render("replace (start-first)"))
|
||||||
|
}
|
||||||
|
if stopFirstCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)"))
|
||||||
|
}
|
||||||
|
if removeCount > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove"))
|
||||||
|
}
|
||||||
|
|
||||||
|
machinesWord := "machines"
|
||||||
|
if len(machines) == 1 {
|
||||||
|
machinesWord = "machine"
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("across %s %s", tui.Bold.Render(strconv.Itoa(len(machines))), machinesWord))
|
||||||
|
|
||||||
|
sep := " " + tui.Faint.Render("·") + " "
|
||||||
|
return strings.Join(parts, sep)
|
||||||
|
}
|
||||||
|
|
||||||
// formatImageDiff formats the image for display. If oldImage is empty, it formats newImage as a new (green) value.
|
// formatImageDiff formats the image for display. If oldImage is empty, it formats newImage as a new (green) value.
|
||||||
// Otherwise, it renders the diff between oldImage and newImage.
|
// Otherwise, it renders the diff between oldImage and newImage.
|
||||||
func formatImageDiff(oldImage, newImage string) string {
|
func formatImageDiff(oldImage, newImage string) string {
|
||||||
@@ -170,12 +225,12 @@ func formatImageDiff(oldImage, newImage string) string {
|
|||||||
|
|
||||||
// Create case: no old image.
|
// Create case: no old image.
|
||||||
if oldImage == "" {
|
if oldImage == "" {
|
||||||
return styledImage(newRef, tui.Green)
|
return tui.FormatImage(newRef, tui.Green)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update case: no change.
|
// Update case: no change.
|
||||||
if oldImage == newImage {
|
if oldImage == newImage {
|
||||||
return styledImage(newRef, lipgloss.NewStyle())
|
return tui.FormatImage(newRef, lipgloss.NewStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
oldRef, _ := reference.ParseDockerRef(oldImage)
|
oldRef, _ := reference.ParseDockerRef(oldImage)
|
||||||
@@ -184,9 +239,9 @@ func formatImageDiff(oldImage, newImage string) string {
|
|||||||
_, oldDigested := oldRef.(reference.Digested)
|
_, oldDigested := oldRef.(reference.Digested)
|
||||||
_, newDigested := newRef.(reference.Digested)
|
_, newDigested := newRef.(reference.Digested)
|
||||||
if oldDigested || newDigested {
|
if oldDigested || newDigested {
|
||||||
return styledImage(oldRef, tui.Red) + " " +
|
return tui.FormatImage(oldRef, tui.Red) + " " +
|
||||||
tui.Faint.Render("→") + " " +
|
tui.Faint.Render("→") + " " +
|
||||||
styledImage(newRef, tui.Green)
|
tui.FormatImage(newRef, tui.Green)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If repos match and both are tagged, show only tag diff.
|
// If repos match and both are tagged, show only tag diff.
|
||||||
@@ -201,19 +256,9 @@ func formatImageDiff(oldImage, newImage string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Different repos: full old → new.
|
// Different repos: full old → new.
|
||||||
return styledImage(oldRef, tui.Red) + " " +
|
return tui.FormatImage(oldRef, tui.Red) + " " +
|
||||||
tui.Faint.Render("→") + " " +
|
tui.Faint.Render("→") + " " +
|
||||||
styledImage(newRef, tui.Green)
|
tui.FormatImage(newRef, tui.Green)
|
||||||
}
|
|
||||||
|
|
||||||
// styledImage renders a parsed image reference with the given style, using a faint colon separator for tagged images.
|
|
||||||
func styledImage(image reference.Named, style lipgloss.Style) string {
|
|
||||||
if tagged, ok := image.(reference.NamedTagged); ok {
|
|
||||||
return style.Render(reference.FamiliarName(image)) +
|
|
||||||
tui.Faint.Render(":") +
|
|
||||||
style.Render(tagged.Tag())
|
|
||||||
}
|
|
||||||
return style.Render(reference.FamiliarString(image))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeployment creates a new deployment for the given service specification.
|
// NewDeployment creates a new deployment for the given service specification.
|
||||||
|
|||||||
Reference in New Issue
Block a user