fix: compose deployment plan preview

This commit is contained in:
Pavel Sviderski
2025-04-23 12:04:04 +10:00
parent edad2429c5
commit 33550be65e
4 changed files with 55 additions and 35 deletions
+32 -30
View File
@@ -6,10 +6,10 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/compose-spec/compose-go/v2/types"
"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"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/compose" "github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -26,9 +26,8 @@ type deployOptions struct {
func NewDeployCommand() *cobra.Command { func NewDeployCommand() *cobra.Command {
opts := deployOptions{} opts := deployOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "deploy [FLAGS] [SERVICE...]", Use: "deploy [FLAGS] [SERVICE...]",
// TODO: remove WIP when the command is fully implemented. Short: "Deploy services from a Compose file.",
Short: "WIP: Deploy services from a Compose file.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) uncli := cmd.Context().Value("cli").(*cli.CLI)
@@ -58,8 +57,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
if len(opts.services) > 0 { if len(opts.services) > 0 {
// TODO: handle dependencies properly. // Includes service dependencies by default. This is the default docker compose behavior.
project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies) project, err = project.WithSelectedServices(opts.services)
if err != nil { if err != nil {
return fmt.Errorf("select services: %w", err) return fmt.Errorf("select services: %w", err)
} }
@@ -87,30 +86,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
fmt.Println("Deployment plan:") fmt.Println("Deployment plan:")
if err = printPlan(ctx, clusterClient, plan); err != nil {
for _, op := range plan.Operations { return fmt.Errorf("print deployment plan: %w", err)
svcPlan, ok := op.(*deploy.Plan)
if !ok {
return fmt.Errorf("expected service Plan, got: %T", op)
}
svc, err := clusterClient.InspectService(ctx, svcPlan.ServiceID)
if err != nil {
if !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
fmt.Printf("- Run service [name=%s]\n", svcPlan.ServiceName)
} else {
fmt.Printf("- Update service [name=%s]\n", svc.Name)
}
// Initialise a machine and container name resolver to properly format the service 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(indent(svcPlan.Format(resolver), " "))
} }
fmt.Println() fmt.Println()
@@ -131,6 +108,31 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
}, uncli.ProgressOut(), "Deploying services") }, uncli.ProgressOut(), "Deploying services")
} }
func printPlan(ctx context.Context, cli *client.Client, plan deploy.SequenceOperation) error {
for _, op := range plan.Operations {
svcPlan, ok := op.(*deploy.Plan)
if !ok {
fmt.Println("- " + op.Format(nil))
continue
}
svc, err := cli.InspectService(ctx, svcPlan.ServiceID)
if err != nil && !errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("inspect service: %w", err)
}
// Initialise a machine and container name resolver to properly format the service plan output.
resolver, err := cli.ServiceOperationNameResolver(ctx, svc)
if err != nil {
return fmt.Errorf("create machine and container name resolver for service operations: %w", err)
}
fmt.Printf("- Deploy service [name=%s]\n", svcPlan.ServiceName)
fmt.Println(indent(svcPlan.Format(resolver), " "))
}
return nil
}
func indent(text, prefix string) string { func indent(text, prefix string) string {
lines := strings.Split(text, "\n") lines := strings.Split(text, "\n")
for i, line := range lines { for i, line := range lines {
+8 -2
View File
@@ -142,9 +142,15 @@ func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.Crea
var ops []*deploy.CreateVolumeOperation var ops []*deploy.CreateVolumeOperation
for machineID, volumes := range scheduledVolumes { for machineID, volumes := range scheduledVolumes {
for _, v := range volumes { for _, v := range volumes {
machineName := machineID
if m, ok := d.state.Machine(machineID); ok {
machineName = m.Info.Name
}
ops = append(ops, &deploy.CreateVolumeOperation{ ops = append(ops, &deploy.CreateVolumeOperation{
MachineID: machineID, MachineID: machineID,
VolumeSpec: v, MachineName: machineName,
VolumeSpec: v,
}) })
} }
} }
+5 -3
View File
@@ -18,6 +18,7 @@ type Operation interface {
// can be provided. But in reality, the operation is tightly coupled with the client that was used to create it. // can be provided. But in reality, the operation is tightly coupled with the client that was used to create it.
Execute(ctx context.Context, cli Client) error Execute(ctx context.Context, cli Client) error
// Format returns a human-readable representation of the operation. // Format returns a human-readable representation of the operation.
// TODO: get rid of the resolver and assign the required names for formatting in the operation itself.
Format(resolver NameResolver) string Format(resolver NameResolver) string
String() string String() string
} }
@@ -115,6 +116,8 @@ func (o *RemoveContainerOperation) String() string {
type CreateVolumeOperation struct { type CreateVolumeOperation struct {
VolumeSpec api.VolumeSpec VolumeSpec api.VolumeSpec
MachineID string MachineID string
// MachineName is used for formatting the operation output only.
MachineName string
} }
func (o *CreateVolumeOperation) Execute(ctx context.Context, cli Client) error { func (o *CreateVolumeOperation) Execute(ctx context.Context, cli Client) error {
@@ -140,9 +143,8 @@ func (o *CreateVolumeOperation) Execute(ctx context.Context, cli Client) error {
return nil return nil
} }
func (o *CreateVolumeOperation) Format(resolver NameResolver) string { func (o *CreateVolumeOperation) Format(_ NameResolver) string {
machineName := resolver.MachineName(o.MachineID) return fmt.Sprintf("%s: Create volume [name=%s]", o.MachineName, o.VolumeSpec.DockerVolumeName())
return fmt.Sprintf("%s: Create volume [name=%s]", machineName, o.VolumeSpec.DockerVolumeName())
} }
func (o *CreateVolumeOperation) String() string { func (o *CreateVolumeOperation) String() string {
+10
View File
@@ -57,3 +57,13 @@ func InspectClusterState(ctx context.Context, cli Client) (*ClusterState, error)
Machines: machines, Machines: machines,
}, nil }, nil
} }
// Machine returns the machine with the given name or ID from the cluster state.
func (s *ClusterState) Machine(nameOrID string) (*Machine, bool) {
for _, m := range s.Machines {
if m.Info.Id == nameOrID || m.Info.Name == nameOrID {
return m, true
}
}
return nil, false
}