mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
174 lines
5.8 KiB
Go
174 lines
5.8 KiB
Go
package operation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/errdefs"
|
|
"github.com/docker/compose/v2/pkg/progress"
|
|
"github.com/docker/docker/api/types/container"
|
|
cliprogress "github.com/psviderski/uncloud/internal/cli/progress"
|
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
|
"github.com/psviderski/uncloud/pkg/api"
|
|
)
|
|
|
|
// DefaultPreDeployTimeout is the maximum duration to wait for a pre-deploy hook container to complete.
|
|
const DefaultPreDeployTimeout = 5 * time.Minute
|
|
|
|
// StopPreDeployOperation stops a running pre-deploy hook container from a previous deployment.
|
|
type StopPreDeployOperation struct {
|
|
MachineID string
|
|
// MachineName is used for formatting the operation as part of the deployment plan.
|
|
MachineName string
|
|
Container api.ServiceContainer
|
|
}
|
|
|
|
func (o *StopPreDeployOperation) Execute(ctx context.Context, cli Client) error {
|
|
if err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.StopOptions{}); err != nil {
|
|
if !errdefs.IsNotFound(err) {
|
|
return fmt.Errorf("stop pre-deploy hook container '%s': %w", o.Container.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *StopPreDeployOperation) Format() string {
|
|
displayName := o.Container.ServiceSpec.Name + tui.Faint.Render("/") + o.Container.ShortID()
|
|
status, _ := o.Container.HumanState()
|
|
|
|
return tui.BoldRed.Render("⏹") + " " +
|
|
tui.Faint.Render("stop pre-deploy hook") + " " +
|
|
displayName + " " +
|
|
tui.Faint.Render("("+status+")") + " " +
|
|
tui.Faint.Render("on") + " " +
|
|
o.MachineName
|
|
}
|
|
|
|
func (o *StopPreDeployOperation) String() string {
|
|
return fmt.Sprintf("StopPreDeployOperation[machine_id=%s container_id=%s]",
|
|
o.MachineID, o.Container.ID)
|
|
}
|
|
|
|
// RunPreDeployOperation runs a one-shot pre-deploy hook container before service deployment.
|
|
type RunPreDeployOperation struct {
|
|
ServiceID string
|
|
Spec api.ServiceSpec
|
|
MachineID string
|
|
// MachineName is used for formatting the operation as part of the deployment plan.
|
|
MachineName string
|
|
// OldContainerIDs are pre-deploy hook containers from previous deployments to remove.
|
|
OldContainerIDs []string
|
|
}
|
|
|
|
func (o *RunPreDeployOperation) Execute(ctx context.Context, cli Client) error {
|
|
// Remove old pre-deploy containers.
|
|
for _, id := range o.OldContainerIDs {
|
|
_ = cli.StopContainer(ctx, o.ServiceID, id, container.StopOptions{})
|
|
err := cli.RemoveContainer(ctx, o.ServiceID, id, container.RemoveOptions{RemoveVolumes: true})
|
|
if err != nil && !errdefs.IsNotFound(err) {
|
|
return fmt.Errorf("remove old pre-deploy hook container '%s': %w", id, err)
|
|
}
|
|
}
|
|
|
|
resp, err := cli.CreatePreDeployHookContainer(ctx, o.ServiceID, o.Spec, o.MachineID)
|
|
if err != nil {
|
|
return fmt.Errorf("create pre-deploy hook container: %w", err)
|
|
}
|
|
if err = cli.StartContainer(ctx, o.ServiceID, resp.ID); err != nil {
|
|
return fmt.Errorf("start pre-deploy hook container: %w", err)
|
|
}
|
|
|
|
timeout := DefaultPreDeployTimeout
|
|
if o.Spec.PreDeploy.Timeout != nil {
|
|
timeout = *o.Spec.PreDeploy.Timeout
|
|
}
|
|
|
|
return o.waitForExit(ctx, cli, resp.ID, timeout)
|
|
}
|
|
|
|
// waitForExit polls the container state until it exits or the timeout is reached.
|
|
func (o *RunPreDeployOperation) waitForExit(
|
|
ctx context.Context, cli Client, containerID string, timeout time.Duration,
|
|
) error {
|
|
pw := progress.ContextWriter(ctx)
|
|
eventID := cliprogress.PreDeployHookEventID(o.Spec.Name, o.MachineName)
|
|
pw.Event(progress.Waiting(eventID))
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
|
|
var ctr *api.ServiceContainer
|
|
for {
|
|
select {
|
|
case <-timeoutCtx.Done():
|
|
// Stop the container on timeout or context cancellation.
|
|
pw.Event(progress.StoppingEvent(eventID))
|
|
_ = cli.StopContainer(ctx, o.ServiceID, containerID, container.StopOptions{})
|
|
|
|
ctrID := containerID
|
|
if ctr != nil {
|
|
ctrID = fmt.Sprintf("%s/%s", o.Spec.Name, ctr.ShortID())
|
|
}
|
|
if ctx.Err() != nil {
|
|
// The parent context has been cancelled before the timeout.
|
|
pw.Event(progress.NewEvent(eventID, progress.Error, "Cancelled"))
|
|
return fmt.Errorf("pre-deploy hook container '%s': %w", ctrID, ctx.Err())
|
|
}
|
|
pw.Event(progress.NewEvent(eventID, progress.Error, fmt.Sprintf("Timeout (%s)", timeout)))
|
|
return fmt.Errorf("pre-deploy hook container '%s' timed out after %s. "+
|
|
"It's stopped and available for inspection. Fetch logs with 'uc logs %s'",
|
|
ctrID, timeout, o.Spec.Name)
|
|
|
|
case <-ticker.C:
|
|
mc, err := cli.InspectContainer(ctx, o.ServiceID, containerID)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect pre-deploy hook container: %w", err)
|
|
}
|
|
ctr = &mc.Container
|
|
|
|
if ctr.State.Running {
|
|
if state, err := ctr.Container.HumanState(); err == nil {
|
|
pw.Event(progress.NewEvent(eventID, progress.Working, fmt.Sprintf("Waiting (%s)", state)))
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Hook container has exited successfully.
|
|
if ctr.State.ExitCode == 0 {
|
|
pw.Event(progress.Event{
|
|
ID: eventID,
|
|
Status: progress.Done,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
pw.Event(progress.ErrorEvent(eventID))
|
|
ctrID := fmt.Sprintf("%s/%s", o.Spec.Name, ctr.ShortID())
|
|
return fmt.Errorf("pre-deploy hook container '%s' failed with exit code: %d. "+
|
|
"It's stopped and available for inspection. Fetch logs with 'uc logs %s'",
|
|
ctrID, ctr.State.ExitCode, o.Spec.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (o *RunPreDeployOperation) Format() string {
|
|
cmd := strings.Join(o.Spec.PreDeploy.Command, " ")
|
|
|
|
return tui.BoldGreen.Render("▶") + " " +
|
|
tui.Faint.Render("run pre-deploy hook") + " " +
|
|
// TODO: truncate a long cmd to fit the width of the terminal.
|
|
o.Spec.Name + " (" + cmd + ") " +
|
|
tui.Faint.Render("on") + " " +
|
|
o.MachineName
|
|
}
|
|
|
|
func (o *RunPreDeployOperation) String() string {
|
|
return fmt.Sprintf("RunPreDeployOperation[machine_id=%s service_id=%s cmd=%v]",
|
|
o.MachineID, o.ServiceID, o.Spec.PreDeploy.Command)
|
|
}
|