| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 | package dockerimport (	"context"	"encoding/json"	"fmt"	"io"	"os"	"strings"	"time"	"github.com/0xJacky/Nginx-UI/internal/version"	"github.com/docker/docker/api/types/container"	"github.com/docker/docker/api/types/image"	"github.com/docker/docker/client"	"github.com/pkg/errors"	"github.com/uozi-tech/cosy"	"github.com/uozi-tech/cosy/logger")const (	ImageName  = "uozi/nginx-ui"	TempPrefix = "nginx-ui-temp-"	OldSuffix  = "_old")// getTimestampedTempName returns a temporary container name with timestampfunc getTimestampedTempName() string {	return fmt.Sprintf("%s%d", TempPrefix, time.Now().Unix())}// removeAllTempContainers removes all containers with the TempPrefixfunc removeAllTempContainers(ctx context.Context, cli *client.Client) (err error) {	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})	if err != nil {		return	}	for _, c := range containers {		for _, name := range c.Names {			processedName := strings.TrimPrefix(name, "/")			if strings.HasPrefix(processedName, TempPrefix) {				err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})				if err != nil {					logger.Error("Failed to remove temp container:", err)				} else {					logger.Info("Successfully removed temp container:", processedName)				}				break			}		}	}	return nil}// UpgradeStepOne Trigger in the OTA upgradefunc UpgradeStepOne(channel string, progressChan chan<- float64) (err error) {	ctx := context.Background()	// 1. Get the tag of the latest release	release, err := version.GetRelease(channel)	if err != nil {		return err	}	tag := release.TagName	// 2. Pull the image	cli, err := initClient()	if err != nil {		return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())	}	defer cli.Close()	// Pull the image with the specified tag	out, err := cli.ImagePull(ctx, fmt.Sprintf("%s:%s", ImageName, tag), image.PullOptions{})	if err != nil {		return cosy.WrapErrorWithParams(ErrFailedToPullImage, err.Error())	}	defer out.Close()	// Parse JSON stream and send progress updates through channel	decoder := json.NewDecoder(out)	type ProgressDetail struct {		Current int64 `json:"current"`		Total   int64 `json:"total"`	}	type PullStatus struct {		Status         string         `json:"status"`		ProgressDetail ProgressDetail `json:"progressDetail"`		ID             string         `json:"id"`	}	layers := make(map[string]float64)	var status PullStatus	var lastProgress float64	for {		if err := decoder.Decode(&status); err != nil {			if err == io.EOF {				break			}			logger.Error("Error decoding Docker pull status:", err)			continue		}		// Only process layers with progress information		if status.ProgressDetail.Total > 0 {			progress := float64(status.ProgressDetail.Current) / float64(status.ProgressDetail.Total) * 100			layers[status.ID] = progress			// Calculate overall progress (average of all layers)			var totalProgress float64			for _, p := range layers {				totalProgress += p			}			overallProgress := totalProgress / float64(len(layers))			// Only send progress updates when there's a meaningful change			if overallProgress > lastProgress+1 || overallProgress >= 100 {				if progressChan != nil {					progressChan <- overallProgress				}				lastProgress = overallProgress			}		}	}	// Ensure we send 100% at the end	if progressChan != nil && lastProgress < 100 {		progressChan <- 100	}	// 3. Create a temp container	// Clean up any existing temp containers	err = removeAllTempContainers(ctx, cli)	if err != nil {		logger.Error("Failed to clean up existing temp containers:", err)		// Continue execution despite cleanup errors	}	// Generate timestamped temp container name	tempContainerName := getTimestampedTempName()	// Get current container name	containerID, err := GetContainerID()	if err != nil {		return cosy.WrapErrorWithParams(ErrFailedToGetContainerID, err.Error())	}	containerInfo, err := cli.ContainerInspect(ctx, containerID)	if err != nil {		return cosy.WrapErrorWithParams(ErrFailedToInspectCurrentContainer, err.Error())	}	currentContainerName := strings.TrimPrefix(containerInfo.Name, "/")	// Set up the command for the temp container to execute step 2	upgradeCmd := []string{"nginx-ui", "upgrade-docker-step2"}	// Add old container name as environment variable	containerEnv := containerInfo.Config.Env	containerEnv = append(containerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))	// Create temp container using new image	_, err = cli.ContainerCreate(		ctx,		&container.Config{			Image:      fmt.Sprintf("%s:%s", ImageName, tag),			Entrypoint: upgradeCmd,			Env:        containerEnv,		},		&container.HostConfig{			Binds: containerInfo.HostConfig.Binds,		},		nil,		nil,		tempContainerName,	)	if err != nil {		return cosy.WrapErrorWithParams(ErrFailedToCreateTempContainer, err.Error())	}	// Start the temp container to execute step 2	err = cli.ContainerStart(ctx, tempContainerName, container.StartOptions{})	if err != nil {		return cosy.WrapErrorWithParams(ErrFailedToStartTempContainer, err.Error())	}	// Output status information	logger.Info("Docker OTA upgrade step 1 completed. Temp container started to execute step 2.")	return nil}// UpgradeStepTwo Trigger in the temp containerfunc UpgradeStepTwo(ctx context.Context) (err error) {	// 1. Copy the old config	cli, err := initClient()	if err != nil {		return	}	defer cli.Close()	// Get old container name from environment variable, fallback to settings if not available	currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")	if currentContainerName == "" {		return errors.New("could not find old container name")	}	// Get the current running temp container name	// Since we can't directly get our own container name from inside, we'll search all temp containers	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})	if err != nil {		return errors.Wrap(err, "failed to list containers")	}	// Find containers with the temp prefix	var tempContainerName string	for _, c := range containers {		for _, name := range c.Names {			processedName := strings.TrimPrefix(name, "/")			if strings.HasPrefix(processedName, TempPrefix) {				tempContainerName = processedName				break			}		}		if tempContainerName != "" {			break		}	}	if tempContainerName == "" {		return errors.New("could not find temp container")	}	// Get temp container info to get the new image	tempContainerInfo, err := cli.ContainerInspect(ctx, tempContainerName)	if err != nil {		return errors.Wrap(err, "failed to inspect temp container")	}	newImage := tempContainerInfo.Config.Image	// Get current container info	oldContainerInfo, err := cli.ContainerInspect(ctx, currentContainerName)	if err != nil {		return errors.Wrap(err, "failed to inspect current container")	}	// 2. Stop the old container and rename to _old	err = cli.ContainerStop(ctx, currentContainerName, container.StopOptions{})	if err != nil {		return errors.Wrap(err, "failed to stop current container")	}	// Rename the old container with _old suffix	err = cli.ContainerRename(ctx, currentContainerName, currentContainerName+OldSuffix)	if err != nil {		return errors.Wrap(err, "failed to rename old container")	}	// Stop the old container	err = cli.ContainerStop(ctx, currentContainerName+OldSuffix, container.StopOptions{})	if err != nil {		return errors.Wrap(err, "failed to stop old container")	}	// 3. Use the old config to create and start a new container with the updated image	// Create new container with original config but using the new image	newContainerEnv := oldContainerInfo.Config.Env	// Pass the old container name to the new container	newContainerEnv = append(newContainerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))	_, err = cli.ContainerCreate(		ctx,		&container.Config{			Image:        newImage,			Cmd:          oldContainerInfo.Config.Cmd,			Env:          newContainerEnv,			Entrypoint:   oldContainerInfo.Config.Entrypoint,			Labels:       oldContainerInfo.Config.Labels,			ExposedPorts: oldContainerInfo.Config.ExposedPorts,			Volumes:      oldContainerInfo.Config.Volumes,			WorkingDir:   oldContainerInfo.Config.WorkingDir,		},		&container.HostConfig{			Binds:         oldContainerInfo.HostConfig.Binds,			PortBindings:  oldContainerInfo.HostConfig.PortBindings,			RestartPolicy: oldContainerInfo.HostConfig.RestartPolicy,			NetworkMode:   oldContainerInfo.HostConfig.NetworkMode,			Mounts:        oldContainerInfo.HostConfig.Mounts,			Privileged:    oldContainerInfo.HostConfig.Privileged,		},		nil,		nil,		currentContainerName,	)	if err != nil {		// If creation fails, try to recover		recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)		if recoverErr == nil {			// Start old container			recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})			if recoverErr == nil {				return errors.Wrap(err, "failed to create new container, recovered to old container")			}		}		return errors.Wrap(err, "failed to create new container and failed to recover")	}	// Start the new container	err = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})	if err != nil {		logger.Error("Failed to start new container:", err)		// If startup fails, try to recover		// First remove the failed new container		removeErr := cli.ContainerRemove(ctx, currentContainerName, container.RemoveOptions{Force: true})		if removeErr != nil {			logger.Error("Failed to remove failed new container:", removeErr)		}		// Rename the old container back to original		recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)		if recoverErr == nil {			// Start old container			recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})			if recoverErr == nil {				return errors.Wrap(err, "failed to start new container, recovered to old container")			}		}		return errors.Wrap(err, "failed to start new container and failed to recover")	}	logger.Info("Docker OTA upgrade step 2 completed successfully. New container is running.")	return nil}// UpgradeStepThree Trigger in the new containerfunc UpgradeStepThree() error {	ctx := context.Background()	// Remove the old container	cli, err := initClient()	if err != nil {		return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())	}	defer cli.Close()	// Get old container name from environment variable, fallback to settings if not available	currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")	if currentContainerName == "" {		return nil	}	oldContainerName := currentContainerName + OldSuffix	// Check if old container exists and remove it if it does	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})	if err != nil {		return errors.Wrap(err, "failed to list containers")	}	for _, c := range containers {		for _, name := range c.Names {			processedName := strings.TrimPrefix(name, "/")			// Remove old container			if processedName == oldContainerName {				err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})				if err != nil {					logger.Error("Failed to remove old container:", err)					// Continue execution, don't interrupt because of failure to remove old container				} else {					logger.Info("Successfully removed old container:", oldContainerName)				}				break			}		}	}	// Clean up all temp containers	err = removeAllTempContainers(ctx, cli)	if err != nil {		logger.Error("Failed to clean up temp containers:", err)		// Continue execution despite cleanup errors	}	return nil}
 |