From 33550be65e22534ad8831871a1fef768c58313c0 Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Wed, 23 Apr 2025 12:04:04 +1000 Subject: [PATCH] fix: compose deployment plan preview --- cmd/uncloud/deploy.go | 62 ++++++++++++++-------------- pkg/client/compose/deploy.go | 10 ++++- pkg/client/deploy/operation.go | 8 ++-- pkg/client/deploy/scheduler/state.go | 10 +++++ 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/cmd/uncloud/deploy.go b/cmd/uncloud/deploy.go index 187185e4..fe722bc8 100644 --- a/cmd/uncloud/deploy.go +++ b/cmd/uncloud/deploy.go @@ -6,10 +6,10 @@ import ( "fmt" "strings" - "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" "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/deploy" "github.com/spf13/cobra" @@ -26,9 +26,8 @@ type deployOptions struct { func NewDeployCommand() *cobra.Command { opts := deployOptions{} cmd := &cobra.Command{ - Use: "deploy [FLAGS] [SERVICE...]", - // TODO: remove WIP when the command is fully implemented. - Short: "WIP: Deploy services from a Compose file.", + Use: "deploy [FLAGS] [SERVICE...]", + Short: "Deploy services from a Compose file.", RunE: func(cmd *cobra.Command, args []string) error { 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 { - // TODO: handle dependencies properly. - project, err = project.WithSelectedServices(opts.services, types.IgnoreDependencies) + // Includes service dependencies by default. This is the default docker compose behavior. + project, err = project.WithSelectedServices(opts.services) if err != nil { 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:") - - for _, op := range plan.Operations { - 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), " ")) + if err = printPlan(ctx, clusterClient, plan); err != nil { + return fmt.Errorf("print deployment plan: %w", err) } fmt.Println() @@ -131,6 +108,31 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { }, 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 { lines := strings.Split(text, "\n") for i, line := range lines { diff --git a/pkg/client/compose/deploy.go b/pkg/client/compose/deploy.go index 7b7dff4c..c57707e0 100644 --- a/pkg/client/compose/deploy.go +++ b/pkg/client/compose/deploy.go @@ -142,9 +142,15 @@ func (d *Deployment) planVolumes(serviceSpecs []api.ServiceSpec) ([]*deploy.Crea var ops []*deploy.CreateVolumeOperation for machineID, volumes := range scheduledVolumes { for _, v := range volumes { + machineName := machineID + if m, ok := d.state.Machine(machineID); ok { + machineName = m.Info.Name + } + ops = append(ops, &deploy.CreateVolumeOperation{ - MachineID: machineID, - VolumeSpec: v, + MachineID: machineID, + MachineName: machineName, + VolumeSpec: v, }) } } diff --git a/pkg/client/deploy/operation.go b/pkg/client/deploy/operation.go index cf13e8f3..7e458296 100644 --- a/pkg/client/deploy/operation.go +++ b/pkg/client/deploy/operation.go @@ -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. Execute(ctx context.Context, cli Client) error // 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 String() string } @@ -115,6 +116,8 @@ func (o *RemoveContainerOperation) String() string { type CreateVolumeOperation struct { VolumeSpec api.VolumeSpec MachineID string + // MachineName is used for formatting the operation output only. + MachineName string } 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 } -func (o *CreateVolumeOperation) Format(resolver NameResolver) string { - machineName := resolver.MachineName(o.MachineID) - return fmt.Sprintf("%s: Create volume [name=%s]", machineName, o.VolumeSpec.DockerVolumeName()) +func (o *CreateVolumeOperation) Format(_ NameResolver) string { + return fmt.Sprintf("%s: Create volume [name=%s]", o.MachineName, o.VolumeSpec.DockerVolumeName()) } func (o *CreateVolumeOperation) String() string { diff --git a/pkg/client/deploy/scheduler/state.go b/pkg/client/deploy/scheduler/state.go index e30ce1ac..17eec0de 100644 --- a/pkg/client/deploy/scheduler/state.go +++ b/pkg/client/deploy/scheduler/state.go @@ -57,3 +57,13 @@ func InspectClusterState(ctx context.Context, cli Client) (*ClusterState, error) Machines: machines, }, 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 +}