diff --git a/cmd/uncloud/caddy/deploy.go b/cmd/uncloud/caddy/deploy.go index 7bd37359..ac0e577e 100644 --- a/cmd/uncloud/caddy/deploy.go +++ b/cmd/uncloud/caddy/deploy.go @@ -125,15 +125,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { } } - // Initialise a machine and container name resolver to properly format the 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() fmt.Println("Deployment plan:") - fmt.Println(plan.Format(resolver)) + fmt.Println(plan.Format()) fmt.Println() confirmed, err := tui.Confirm("") diff --git a/cmd/uncloud/deploy.go b/cmd/uncloud/deploy.go index edfb44d7..e90443e9 100644 --- a/cmd/uncloud/deploy.go +++ b/cmd/uncloud/deploy.go @@ -11,11 +11,9 @@ import ( "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli/tui" - "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/psviderski/uncloud/pkg/client/deploy/operation" "github.com/spf13/cobra" ) @@ -193,10 +191,7 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { fmt.Println() } - if err = printPlan(ctx, clusterClient, plan); err != nil { - return fmt.Errorf("print deployment plan: %w", err) - } - fmt.Println() + fmt.Println(plan.Format()) // Ask for plan confirmation before proceeding with the deployment unless auto-confirmed with --yes. if !opts.yes { @@ -235,20 +230,3 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { return nil }, uncli.ProgressOut(), title) } - -func printPlan(ctx context.Context, cli *client.Client, plan compose.Plan) error { - resolvers := make(map[string]operation.NameResolver) - for _, svcPlan := range plan.Services { - svc, err := cli.InspectService(ctx, svcPlan.ServiceID) - if err != nil && !errors.Is(err, api.ErrNotFound) { - return fmt.Errorf("inspect service: %w", err) - } - resolver, err := cli.ServiceOperationNameResolver(ctx, svc) - if err != nil { - return fmt.Errorf("create resolver for service '%s': %w", svcPlan.ServiceName, err) - } - resolvers[svcPlan.ServiceID] = resolver - } - fmt.Print(plan.Format(resolvers)) - return nil -} diff --git a/cmd/uncloud/machine/add.go b/cmd/uncloud/machine/add.go index 20664555..ffe3ca7b 100644 --- a/cmd/uncloud/machine/add.go +++ b/cmd/uncloud/machine/add.go @@ -214,15 +214,8 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine, 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() + fmt.Println(plan.Format()) if !opts.yes { confirmed, err := tui.Confirm("") diff --git a/cmd/uncloud/service/scale.go b/cmd/uncloud/service/scale.go index f7eb6733..8d5bbc85 100644 --- a/cmd/uncloud/service/scale.go +++ b/cmd/uncloud/service/scale.go @@ -91,14 +91,8 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error { } if opts.replicas < currentReplicas { - // Initialise a machine and container name resolver to properly format the 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.Printf("Scaling plan for service %s (%d → %d replicas):\n", svc.Name, currentReplicas, opts.replicas) - fmt.Println(plan.Format(resolver)) + fmt.Println(plan.Format()) fmt.Println() // Ask for confirmation before scaling down as it may cause data loss. diff --git a/pkg/client/compose/plan.go b/pkg/client/compose/plan.go index 4b025ae2..b97af28c 100644 --- a/pkg/client/compose/plan.go +++ b/pkg/client/compose/plan.go @@ -25,12 +25,12 @@ func (p *Plan) IsEmpty() bool { } // Format renders the entire deployment plan as a styled tree with a summary footer. -func (p *Plan) Format(resolvers map[string]operation.NameResolver) string { +func (p *Plan) Format() string { var out strings.Builder // Format volume operations. for _, op := range p.Volumes { - out.WriteString(op.Format(nil)) + out.WriteString(op.Format()) out.WriteString("\n") } if len(p.Volumes) > 0 { @@ -39,8 +39,7 @@ func (p *Plan) Format(resolvers map[string]operation.NameResolver) string { // Format service plans. for _, svcPlan := range p.Services { - resolver := resolvers[svcPlan.ServiceID] - out.WriteString(svcPlan.Format(resolver)) + out.WriteString(svcPlan.Format()) out.WriteString("\n") } diff --git a/pkg/client/deploy/deploy.go b/pkg/client/deploy/deploy.go index 28f1f34d..b78c31fd 100644 --- a/pkg/client/deploy/deploy.go +++ b/pkg/client/deploy/deploy.go @@ -45,7 +45,7 @@ type ServicePlan struct { } // Format renders the service plan as a styled block with a spec diff and nested container operations. -func (sp *ServicePlan) Format(resolver operation.NameResolver) string { +func (sp *ServicePlan) Format() string { // Determine service-level operation type and extract the old spec from container operations. // Assume replace operations precede remove operations (rolling strategy) so the first replace operation // (if exists) determines the old spec for the diff. Otherwise, fallback to the first remove operation. @@ -156,7 +156,7 @@ func (sp *ServicePlan) Format(resolver operation.NameResolver) string { if i == opsCount-1 { connector = tui.Faint.Render(" ╰──") } - out.WriteString(connector + " " + op.Format(resolver)) + out.WriteString(connector + " " + op.Format()) out.WriteString("\n") } diff --git a/pkg/client/deploy/operation/container.go b/pkg/client/deploy/operation/container.go index afd0717c..6a6d3662 100644 --- a/pkg/client/deploy/operation/container.go +++ b/pkg/client/deploy/operation/container.go @@ -17,6 +17,8 @@ type RunContainerOperation struct { ServiceID string Spec api.ServiceSpec MachineID string + // MachineName is used for formatting the operation as part of the deployment plan. + MachineName string // SkipHealthMonitor skips the monitoring period and health checks after starting a container. SkipHealthMonitor bool } @@ -43,13 +45,12 @@ func (o *RunContainerOperation) Execute(ctx context.Context, cli Client) error { return nil } -func (o *RunContainerOperation) Format(resolver NameResolver) string { - machineName := resolver.MachineName(o.MachineID) +func (o *RunContainerOperation) Format() string { return tui.BoldGreen.Render("+") + " " + tui.Faint.Render("run container") + " " + o.Spec.Name + " " + tui.Faint.Render("on") + " " + - machineName + o.MachineName } func (o *RunContainerOperation) String() string { @@ -59,9 +60,11 @@ func (o *RunContainerOperation) String() string { // StopContainerOperation stops a container on a specific machine. type StopContainerOperation struct { - ServiceID string - ContainerID string - MachineID string + ServiceID string + ContainerID string + MachineID string + // MachineName is used for formatting the operation as part of the deployment plan. + MachineName string StopGracePeriod *time.Duration } @@ -72,8 +75,7 @@ func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error return nil } -func (o *StopContainerOperation) Format(resolver NameResolver) string { - machineName := resolver.MachineName(o.MachineID) +func (o *StopContainerOperation) Format() string { // TODO: pass service name to format the display name consistently with other operations. displayName := stringid.TruncateID(o.ContainerID) @@ -81,7 +83,7 @@ func (o *StopContainerOperation) Format(resolver NameResolver) string { tui.Faint.Render("stop container") + " " + displayName + " " + tui.Faint.Render("on") + " " + - machineName + o.MachineName } func (o *StopContainerOperation) String() string { @@ -91,7 +93,9 @@ func (o *StopContainerOperation) String() string { // RemoveContainerOperation stops and removes a container from a specific machine. type RemoveContainerOperation struct { - MachineID string + MachineID string + // MachineName is used for formatting the operation as part of the deployment plan. + MachineName string Container api.ServiceContainer StopGracePeriod *time.Duration } @@ -112,15 +116,14 @@ func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) erro return nil } -func (o *RemoveContainerOperation) Format(resolver NameResolver) string { - machineName := resolver.MachineName(o.MachineID) +func (o *RemoveContainerOperation) Format() string { displayName := o.Container.ServiceSpec.Name + tui.Faint.Render("/") + o.Container.ShortID() return tui.BoldRed.Render("-") + " " + tui.Faint.Render("remove container") + " " + displayName + " " + tui.Faint.Render("on") + " " + - machineName + o.MachineName } func (o *RemoveContainerOperation) String() string { @@ -132,9 +135,11 @@ func (o *RemoveContainerOperation) String() string { // For start-first: starts new container, then removes old container. // For stop-first: stops old container, starts new container, then removes old container. type ReplaceContainerOperation struct { - ServiceID string - Spec api.ServiceSpec - MachineID string + ServiceID string + Spec api.ServiceSpec + MachineID string + // MachineName is used for formatting the operation as part of the deployment plan. + MachineName string OldContainer api.ServiceContainer // Order specifies the update order: "start-first" or "stop-first". Order string @@ -223,8 +228,7 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err return nil } -func (o *ReplaceContainerOperation) Format(resolver NameResolver) string { - machineName := resolver.MachineName(o.MachineID) +func (o *ReplaceContainerOperation) Format() string { displayName := o.Spec.Name + tui.Faint.Render("/") + o.OldContainer.ShortID() if o.Order == api.UpdateOrderStopFirst { @@ -232,14 +236,14 @@ func (o *ReplaceContainerOperation) Format(resolver NameResolver) string { tui.Faint.Render("replace container") + " " + displayName + " " + tui.Faint.Render("on") + " " + - machineName + " " + + o.MachineName + " " + tui.Yellow.Render("(stop-first)") } return tui.BoldGreen.Render("+") + tui.Green.Render("/") + tui.BoldGreen.Render("-") + " " + tui.Faint.Render("replace container") + " " + displayName + " " + tui.Faint.Render("on") + " " + - machineName + o.MachineName } func (o *ReplaceContainerOperation) String() string { diff --git a/pkg/client/deploy/operation/operation.go b/pkg/client/deploy/operation/operation.go index 92a9c73a..800483e5 100644 --- a/pkg/client/deploy/operation/operation.go +++ b/pkg/client/deploy/operation/operation.go @@ -12,8 +12,7 @@ type Operation interface { // Execute performs the operation using the provided client. 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 + Format() string String() string } @@ -22,9 +21,3 @@ type Client interface { api.ContainerClient api.VolumeClient } - -// NameResolver resolves machine and container IDs to their names. -type NameResolver interface { - MachineName(machineID string) string - ContainerName(containerID string) string -} diff --git a/pkg/client/deploy/operation/sequence.go b/pkg/client/deploy/operation/sequence.go index 16ed8fea..e325819f 100644 --- a/pkg/client/deploy/operation/sequence.go +++ b/pkg/client/deploy/operation/sequence.go @@ -20,10 +20,10 @@ func (o *SequenceOperation) Execute(ctx context.Context, cli Client) error { return nil } -func (o *SequenceOperation) Format(resolver NameResolver) string { +func (o *SequenceOperation) Format() string { lines := make([]string, len(o.Operations)) for i, op := range o.Operations { - lines[i] = op.Format(resolver) + lines[i] = op.Format() } return strings.Join(lines, "\n") diff --git a/pkg/client/deploy/operation/volume.go b/pkg/client/deploy/operation/volume.go index ad8b6dfe..75b18767 100644 --- a/pkg/client/deploy/operation/volume.go +++ b/pkg/client/deploy/operation/volume.go @@ -13,7 +13,7 @@ import ( type CreateVolumeOperation struct { VolumeSpec api.VolumeSpec MachineID string - // MachineName is used for formatting the operation output only. + // MachineName is used for formatting the operation as part of the deployment plan. MachineName string } @@ -40,7 +40,7 @@ func (o *CreateVolumeOperation) Execute(ctx context.Context, cli Client) error { return nil } -func (o *CreateVolumeOperation) Format(_ NameResolver) string { +func (o *CreateVolumeOperation) Format() string { return fmt.Sprintf("%s create volume %s %s %s", tui.BoldGreen.Render("+"), tui.NameStyle.Render(o.VolumeSpec.DockerVolumeName()), diff --git a/pkg/client/deploy/strategy.go b/pkg/client/deploy/strategy.go index f7d52334..57b5f91a 100644 --- a/pkg/client/deploy/strategy.go +++ b/pkg/client/deploy/strategy.go @@ -67,6 +67,12 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) return plan, err } + // Build a machine ID to name map from the cluster state to resolve IDs for operations. + machineNames := make(map[string]string, len(s.state.Machines)) + for _, m := range s.state.Machines { + machineNames[m.Info.Id] = m.Info.Name + } + sched := scheduler.NewServiceScheduler(s.state, spec) // TODO: return a detailed report on required constraints and which ones are satisfied? availableMachines, err := sched.EligibleMachines() @@ -151,6 +157,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) ServiceID: plan.ServiceID, Spec: spec, MachineID: m.Id, + MachineName: m.Name, SkipHealthMonitor: s.SkipHealthMonitor, }) continue @@ -172,6 +179,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) ServiceID: plan.ServiceID, Spec: spec, MachineID: m.Id, + MachineName: m.Name, OldContainer: ctr, Order: order, SkipHealthMonitor: s.SkipHealthMonitor, @@ -184,6 +192,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) for _, c := range containers { plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ MachineID: mid, + MachineName: machineNames[mid], Container: c, StopGracePeriod: spec.StopGracePeriod, }) @@ -204,6 +213,12 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Se return plan, err } + // Build a machine ID to name map from the cluster state to resolve IDs for operations. + machineNames := make(map[string]string, len(s.state.Machines)) + for _, m := range s.state.Machines { + machineNames[m.Info.Id] = m.Info.Name + } + // Map machineID to service containers on that machine. For the global mode, there should be at most one // container per machine but we use a slice to handle multiple containers that may exist due to a bug // or interruption in the previous deployment. @@ -223,7 +238,13 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Se for _, m := range availableMachines { containers := containersOnMachine[m.Info.Id] ops, err := reconcileGlobalContainer( - containers, spec, plan.ServiceID, m.Info.Id, s.ForceRecreate, s.SkipHealthMonitor) + containers, + spec, + plan.ServiceID, + m.Info, + s.ForceRecreate, + s.SkipHealthMonitor, + ) if err != nil { return plan, err } @@ -237,6 +258,7 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Se for _, c := range containers { plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ MachineID: c.MachineID, + MachineName: machineNames[c.MachineID], Container: c.Container, StopGracePeriod: spec.StopGracePeriod, }) @@ -250,7 +272,10 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Se // It ensures exactly one container with the desired spec is running on the machine by creating a new container and // removing old ones. If there is a host port conflict, it stops the old container before starting a new one. func reconcileGlobalContainer( - containers []api.MachineServiceContainer, spec api.ServiceSpec, serviceID, machineID string, + containers []api.MachineServiceContainer, + spec api.ServiceSpec, + serviceID string, + machine *pb.MachineInfo, forceRecreate, skipHealthCheck bool, ) ([]operation.Operation, error) { var ops []operation.Operation @@ -260,7 +285,8 @@ func reconcileGlobalContainer( ops = append(ops, &operation.RunContainerOperation{ ServiceID: serviceID, Spec: spec, - MachineID: machineID, + MachineID: machine.Id, + MachineName: machine.Name, SkipHealthMonitor: skipHealthCheck, }) return ops, nil @@ -290,6 +316,7 @@ func reconcileGlobalContainer( } ops = append(ops, &operation.RemoveContainerOperation{ MachineID: old.MachineID, + MachineName: machine.Name, Container: old.Container, StopGracePeriod: spec.StopGracePeriod, }) @@ -325,7 +352,8 @@ func reconcileGlobalContainer( ops = append(ops, &operation.StopContainerOperation{ ServiceID: serviceID, ContainerID: c.Container.ID, - MachineID: machineID, + MachineID: machine.Id, + MachineName: machine.Name, StopGracePeriod: spec.StopGracePeriod, }) } @@ -336,7 +364,8 @@ func reconcileGlobalContainer( ops = append(ops, &operation.ReplaceContainerOperation{ ServiceID: serviceID, Spec: spec, - MachineID: machineID, + MachineID: machine.Id, + MachineName: machine.Name, OldContainer: containerToReplace.Container, Order: order, SkipHealthMonitor: skipHealthCheck, @@ -350,6 +379,7 @@ func reconcileGlobalContainer( } ops = append(ops, &operation.RemoveContainerOperation{ MachineID: c.MachineID, + MachineName: machine.Name, Container: c.Container, StopGracePeriod: spec.StopGracePeriod, }) @@ -359,12 +389,14 @@ func reconcileGlobalContainer( ops = append(ops, &operation.RunContainerOperation{ ServiceID: serviceID, Spec: spec, - MachineID: machineID, + MachineID: machine.Id, + MachineName: machine.Name, SkipHealthMonitor: skipHealthCheck, }) for _, c := range containers { ops = append(ops, &operation.RemoveContainerOperation{ MachineID: c.MachineID, + MachineName: machine.Name, Container: c.Container, StopGracePeriod: spec.StopGracePeriod, }) diff --git a/pkg/client/deploy/strategy_test.go b/pkg/client/deploy/strategy_test.go index 568ef86f..eca535f2 100644 --- a/pkg/client/deploy/strategy_test.go +++ b/pkg/client/deploy/strategy_test.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client/deploy/operation" "github.com/stretchr/testify/assert" @@ -315,8 +316,9 @@ func TestReconcileGlobalContainer(t *testing.T) { }, expectedOps: []operation.Operation{ &operation.RunContainerOperation{ - ServiceID: "service-1", - MachineID: "machine-1", + ServiceID: "service-1", + MachineID: "machine-1", + MachineName: "machine-1", }, }, }, @@ -335,6 +337,7 @@ func TestReconcileGlobalContainer(t *testing.T) { &operation.ReplaceContainerOperation{ ServiceID: "service-1", MachineID: "machine-1", + MachineName: "machine-1", OldContainer: container1, Order: api.UpdateOrderStopFirst, }, @@ -358,16 +361,19 @@ func TestReconcileGlobalContainer(t *testing.T) { ServiceID: "service-1", ContainerID: "container-2", MachineID: "machine-1", + MachineName: "machine-1", }, &operation.ReplaceContainerOperation{ ServiceID: "service-1", MachineID: "machine-1", + MachineName: "machine-1", OldContainer: container1, Order: api.UpdateOrderStopFirst, }, &operation.RemoveContainerOperation{ - MachineID: "machine-1", - Container: container2WithPort9090, + MachineID: "machine-1", + MachineName: "machine-1", + Container: container2WithPort9090, }, }, }, @@ -388,12 +394,14 @@ func TestReconcileGlobalContainer(t *testing.T) { &operation.ReplaceContainerOperation{ ServiceID: "service-1", MachineID: "machine-1", + MachineName: "machine-1", OldContainer: container1, Order: api.UpdateOrderStopFirst, }, &operation.RemoveContainerOperation{ - MachineID: "machine-1", - Container: container2WithPort3000, + MachineID: "machine-1", + MachineName: "machine-1", + Container: container2WithPort3000, }, }, }, @@ -402,7 +410,12 @@ func TestReconcileGlobalContainer(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ops, err := reconcileGlobalContainer( - tt.containers, tt.spec, "service-1", "machine-1", tt.forceRecreate, false, + tt.containers, + tt.spec, + "service-1", + &pb.MachineInfo{Id: "machine-1", Name: "machine-1"}, + tt.forceRecreate, + false, ) assert.NoError(t, err) assertOperationsEqual(t, tt.expectedOps, ops) diff --git a/pkg/client/resolver.go b/pkg/client/resolver.go deleted file mode 100644 index 26cb8385..00000000 --- a/pkg/client/resolver.go +++ /dev/null @@ -1,54 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/psviderski/uncloud/pkg/api" -) - -// 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 -} - -// ServiceOperationNameResolver returns a machine and container name resolver for a service that can be used to format -// deployment operations. -func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Service) (*MapNameResolver, error) { - machines, err := cli.ListMachines(ctx, nil) - if err != nil { - return nil, 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.Name - } - - return NewNameResolver(machineNames, containerNames), nil -}