ota.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package docker
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "os"
  8. "strings"
  9. "time"
  10. "github.com/0xJacky/Nginx-UI/internal/version"
  11. "github.com/docker/docker/api/types/container"
  12. "github.com/docker/docker/api/types/image"
  13. "github.com/docker/docker/client"
  14. "github.com/pkg/errors"
  15. "github.com/uozi-tech/cosy"
  16. "github.com/uozi-tech/cosy/logger"
  17. )
  18. const (
  19. ImageName = "uozi/nginx-ui"
  20. TempPrefix = "nginx-ui-temp-"
  21. OldSuffix = "_old"
  22. )
  23. // getTimestampedTempName returns a temporary container name with timestamp
  24. func getTimestampedTempName() string {
  25. return fmt.Sprintf("%s%d", TempPrefix, time.Now().Unix())
  26. }
  27. // removeAllTempContainers removes all containers with the TempPrefix
  28. func removeAllTempContainers(ctx context.Context, cli *client.Client) (err error) {
  29. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  30. if err != nil {
  31. return
  32. }
  33. for _, c := range containers {
  34. for _, name := range c.Names {
  35. processedName := strings.TrimPrefix(name, "/")
  36. if strings.HasPrefix(processedName, TempPrefix) {
  37. err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
  38. if err != nil {
  39. logger.Error("Failed to remove temp container:", err)
  40. } else {
  41. logger.Info("Successfully removed temp container:", processedName)
  42. }
  43. break
  44. }
  45. }
  46. }
  47. return nil
  48. }
  49. // UpgradeStepOne Trigger in the OTA upgrade
  50. func UpgradeStepOne(channel string, progressChan chan<- float64) (err error) {
  51. ctx := context.Background()
  52. // 1. Get the tag of the latest release
  53. release, err := version.GetRelease(channel)
  54. if err != nil {
  55. return err
  56. }
  57. tag := release.TagName
  58. // 2. Pull the image
  59. cli, err := initClient()
  60. if err != nil {
  61. return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
  62. }
  63. defer cli.Close()
  64. // Pull the image with the specified tag
  65. out, err := cli.ImagePull(ctx, fmt.Sprintf("%s:%s", ImageName, tag), image.PullOptions{})
  66. if err != nil {
  67. return cosy.WrapErrorWithParams(ErrFailedToPullImage, err.Error())
  68. }
  69. defer out.Close()
  70. // Parse JSON stream and send progress updates through channel
  71. decoder := json.NewDecoder(out)
  72. type ProgressDetail struct {
  73. Current int64 `json:"current"`
  74. Total int64 `json:"total"`
  75. }
  76. type PullStatus struct {
  77. Status string `json:"status"`
  78. ProgressDetail ProgressDetail `json:"progressDetail"`
  79. ID string `json:"id"`
  80. }
  81. layers := make(map[string]float64)
  82. var status PullStatus
  83. var lastProgress float64
  84. for {
  85. if err := decoder.Decode(&status); err != nil {
  86. if err == io.EOF {
  87. break
  88. }
  89. logger.Error("Error decoding Docker pull status:", err)
  90. continue
  91. }
  92. // Only process layers with progress information
  93. if status.ProgressDetail.Total > 0 {
  94. progress := float64(status.ProgressDetail.Current) / float64(status.ProgressDetail.Total) * 100
  95. layers[status.ID] = progress
  96. // Calculate overall progress (average of all layers)
  97. var totalProgress float64
  98. for _, p := range layers {
  99. totalProgress += p
  100. }
  101. overallProgress := totalProgress / float64(len(layers))
  102. // Only send progress updates when there's a meaningful change
  103. if overallProgress > lastProgress+1 || overallProgress >= 100 {
  104. if progressChan != nil {
  105. progressChan <- overallProgress
  106. }
  107. lastProgress = overallProgress
  108. }
  109. }
  110. }
  111. // Ensure we send 100% at the end
  112. if progressChan != nil && lastProgress < 100 {
  113. progressChan <- 100
  114. }
  115. // 3. Create a temp container
  116. // Clean up any existing temp containers
  117. err = removeAllTempContainers(ctx, cli)
  118. if err != nil {
  119. logger.Error("Failed to clean up existing temp containers:", err)
  120. // Continue execution despite cleanup errors
  121. }
  122. // Generate timestamped temp container name
  123. tempContainerName := getTimestampedTempName()
  124. // Get current container name
  125. containerID, err := GetContainerID()
  126. if err != nil {
  127. return cosy.WrapErrorWithParams(ErrFailedToGetContainerID, err.Error())
  128. }
  129. containerInfo, err := cli.ContainerInspect(ctx, containerID)
  130. if err != nil {
  131. return cosy.WrapErrorWithParams(ErrFailedToInspectCurrentContainer, err.Error())
  132. }
  133. currentContainerName := strings.TrimPrefix(containerInfo.Name, "/")
  134. // Set up the command for the temp container to execute step 2
  135. upgradeCmd := []string{"./nginx-ui", "upgrade-docker-step2"}
  136. // Add old container name as environment variable
  137. containerEnv := containerInfo.Config.Env
  138. containerEnv = append(containerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))
  139. // Create temp container using new image
  140. _, err = cli.ContainerCreate(
  141. ctx,
  142. &container.Config{
  143. Image: fmt.Sprintf("%s:%s", ImageName, tag),
  144. Cmd: upgradeCmd, // Use upgrade command instead of original command
  145. Env: containerEnv,
  146. },
  147. &container.HostConfig{
  148. Binds: containerInfo.HostConfig.Binds,
  149. },
  150. nil,
  151. nil,
  152. tempContainerName,
  153. )
  154. if err != nil {
  155. return cosy.WrapErrorWithParams(ErrFailedToCreateTempContainer, err.Error())
  156. }
  157. // Start the temp container to execute step 2
  158. err = cli.ContainerStart(ctx, tempContainerName, container.StartOptions{})
  159. if err != nil {
  160. return cosy.WrapErrorWithParams(ErrFailedToStartTempContainer, err.Error())
  161. }
  162. // Output status information
  163. logger.Info("Docker OTA upgrade step 1 completed. Temp container started to execute step 2.")
  164. return nil
  165. }
  166. // UpgradeStepTwo Trigger in the temp container
  167. func UpgradeStepTwo(ctx context.Context) (err error) {
  168. // 1. Copy the old config
  169. cli, err := initClient()
  170. if err != nil {
  171. return
  172. }
  173. defer cli.Close()
  174. // Get old container name from environment variable, fallback to settings if not available
  175. currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
  176. if currentContainerName == "" {
  177. return errors.New("could not find old container name")
  178. }
  179. // Get the current running temp container name
  180. // Since we can't directly get our own container name from inside, we'll search all temp containers
  181. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  182. if err != nil {
  183. return errors.Wrap(err, "failed to list containers")
  184. }
  185. // Find containers with the temp prefix
  186. var tempContainerName string
  187. for _, c := range containers {
  188. for _, name := range c.Names {
  189. processedName := strings.TrimPrefix(name, "/")
  190. if strings.HasPrefix(processedName, TempPrefix) {
  191. tempContainerName = processedName
  192. break
  193. }
  194. }
  195. if tempContainerName != "" {
  196. break
  197. }
  198. }
  199. if tempContainerName == "" {
  200. return errors.New("could not find temp container")
  201. }
  202. // Get temp container info to get the new image
  203. tempContainerInfo, err := cli.ContainerInspect(ctx, tempContainerName)
  204. if err != nil {
  205. return errors.Wrap(err, "failed to inspect temp container")
  206. }
  207. newImage := tempContainerInfo.Config.Image
  208. // Get current container info
  209. oldContainerInfo, err := cli.ContainerInspect(ctx, currentContainerName)
  210. if err != nil {
  211. return errors.Wrap(err, "failed to inspect current container")
  212. }
  213. // 2. Stop the old container and rename to _old
  214. err = cli.ContainerStop(ctx, currentContainerName, container.StopOptions{})
  215. if err != nil {
  216. return errors.Wrap(err, "failed to stop current container")
  217. }
  218. // Rename the old container with _old suffix
  219. err = cli.ContainerRename(ctx, currentContainerName, currentContainerName+OldSuffix)
  220. if err != nil {
  221. return errors.Wrap(err, "failed to rename old container")
  222. }
  223. // 3. Use the old config to create and start a new container with the updated image
  224. // Create new container with original config but using the new image
  225. newContainerEnv := oldContainerInfo.Config.Env
  226. // Pass the old container name to the new container
  227. newContainerEnv = append(newContainerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))
  228. _, err = cli.ContainerCreate(
  229. ctx,
  230. &container.Config{
  231. Image: newImage,
  232. Cmd: oldContainerInfo.Config.Cmd,
  233. Env: newContainerEnv,
  234. Entrypoint: oldContainerInfo.Config.Entrypoint,
  235. Labels: oldContainerInfo.Config.Labels,
  236. ExposedPorts: oldContainerInfo.Config.ExposedPorts,
  237. Volumes: oldContainerInfo.Config.Volumes,
  238. WorkingDir: oldContainerInfo.Config.WorkingDir,
  239. },
  240. &container.HostConfig{
  241. Binds: oldContainerInfo.HostConfig.Binds,
  242. PortBindings: oldContainerInfo.HostConfig.PortBindings,
  243. RestartPolicy: oldContainerInfo.HostConfig.RestartPolicy,
  244. NetworkMode: oldContainerInfo.HostConfig.NetworkMode,
  245. Mounts: oldContainerInfo.HostConfig.Mounts,
  246. Privileged: oldContainerInfo.HostConfig.Privileged,
  247. },
  248. nil,
  249. nil,
  250. currentContainerName,
  251. )
  252. if err != nil {
  253. // If creation fails, try to recover
  254. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  255. if recoverErr == nil {
  256. // Start old container
  257. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  258. if recoverErr == nil {
  259. return errors.Wrap(err, "failed to create new container, recovered to old container")
  260. }
  261. }
  262. return errors.Wrap(err, "failed to create new container and failed to recover")
  263. }
  264. // Start the new container
  265. err = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  266. if err != nil {
  267. // If startup fails, try to recover
  268. // First remove the failed new container
  269. removeErr := cli.ContainerRemove(ctx, currentContainerName, container.RemoveOptions{Force: true})
  270. if removeErr != nil {
  271. logger.Error("Failed to remove failed new container:", removeErr)
  272. }
  273. // Rename the old container back to original
  274. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  275. if recoverErr == nil {
  276. // Start old container
  277. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  278. if recoverErr == nil {
  279. return errors.Wrap(err, "failed to start new container, recovered to old container")
  280. }
  281. }
  282. return errors.Wrap(err, "failed to start new container and failed to recover")
  283. }
  284. logger.Info("Docker OTA upgrade step 2 completed successfully. New container is running.")
  285. return nil
  286. }
  287. // UpgradeStepThree Trigger in the new container
  288. func UpgradeStepThree() error {
  289. ctx := context.Background()
  290. // Remove the old container
  291. cli, err := initClient()
  292. if err != nil {
  293. return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
  294. }
  295. defer cli.Close()
  296. // Get old container name from environment variable, fallback to settings if not available
  297. currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
  298. if currentContainerName == "" {
  299. return nil
  300. }
  301. oldContainerName := currentContainerName + OldSuffix
  302. // Check if old container exists and remove it if it does
  303. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  304. if err != nil {
  305. return errors.Wrap(err, "failed to list containers")
  306. }
  307. for _, c := range containers {
  308. for _, name := range c.Names {
  309. processedName := strings.TrimPrefix(name, "/")
  310. // Remove old container
  311. if processedName == oldContainerName {
  312. err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
  313. if err != nil {
  314. logger.Error("Failed to remove old container:", err)
  315. // Continue execution, don't interrupt because of failure to remove old container
  316. } else {
  317. logger.Info("Successfully removed old container:", oldContainerName)
  318. }
  319. break
  320. }
  321. }
  322. }
  323. // Clean up all temp containers
  324. err = removeAllTempContainers(ctx, cli)
  325. if err != nil {
  326. logger.Error("Failed to clean up temp containers:", err)
  327. // Continue execution despite cleanup errors
  328. }
  329. return nil
  330. }