ota.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. Entrypoint: upgradeCmd,
  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. // Stop the old container
  224. err = cli.ContainerStop(ctx, currentContainerName+OldSuffix, container.StopOptions{})
  225. if err != nil {
  226. return errors.Wrap(err, "failed to stop old container")
  227. }
  228. // 3. Use the old config to create and start a new container with the updated image
  229. // Create new container with original config but using the new image
  230. newContainerEnv := oldContainerInfo.Config.Env
  231. // Pass the old container name to the new container
  232. newContainerEnv = append(newContainerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))
  233. _, err = cli.ContainerCreate(
  234. ctx,
  235. &container.Config{
  236. Image: newImage,
  237. Cmd: oldContainerInfo.Config.Cmd,
  238. Env: newContainerEnv,
  239. Entrypoint: oldContainerInfo.Config.Entrypoint,
  240. Labels: oldContainerInfo.Config.Labels,
  241. ExposedPorts: oldContainerInfo.Config.ExposedPorts,
  242. Volumes: oldContainerInfo.Config.Volumes,
  243. WorkingDir: oldContainerInfo.Config.WorkingDir,
  244. },
  245. &container.HostConfig{
  246. Binds: oldContainerInfo.HostConfig.Binds,
  247. PortBindings: oldContainerInfo.HostConfig.PortBindings,
  248. RestartPolicy: oldContainerInfo.HostConfig.RestartPolicy,
  249. NetworkMode: oldContainerInfo.HostConfig.NetworkMode,
  250. Mounts: oldContainerInfo.HostConfig.Mounts,
  251. Privileged: oldContainerInfo.HostConfig.Privileged,
  252. },
  253. nil,
  254. nil,
  255. currentContainerName,
  256. )
  257. if err != nil {
  258. // If creation fails, try to recover
  259. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  260. if recoverErr == nil {
  261. // Start old container
  262. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  263. if recoverErr == nil {
  264. return errors.Wrap(err, "failed to create new container, recovered to old container")
  265. }
  266. }
  267. return errors.Wrap(err, "failed to create new container and failed to recover")
  268. }
  269. // Start the new container
  270. err = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  271. if err != nil {
  272. logger.Error("Failed to start new container:", err)
  273. // If startup fails, try to recover
  274. // First remove the failed new container
  275. removeErr := cli.ContainerRemove(ctx, currentContainerName, container.RemoveOptions{Force: true})
  276. if removeErr != nil {
  277. logger.Error("Failed to remove failed new container:", removeErr)
  278. }
  279. // Rename the old container back to original
  280. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  281. if recoverErr == nil {
  282. // Start old container
  283. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  284. if recoverErr == nil {
  285. return errors.Wrap(err, "failed to start new container, recovered to old container")
  286. }
  287. }
  288. return errors.Wrap(err, "failed to start new container and failed to recover")
  289. }
  290. logger.Info("Docker OTA upgrade step 2 completed successfully. New container is running.")
  291. return nil
  292. }
  293. // UpgradeStepThree Trigger in the new container
  294. func UpgradeStepThree() error {
  295. ctx := context.Background()
  296. // Remove the old container
  297. cli, err := initClient()
  298. if err != nil {
  299. return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
  300. }
  301. defer cli.Close()
  302. // Get old container name from environment variable, fallback to settings if not available
  303. currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
  304. if currentContainerName == "" {
  305. return nil
  306. }
  307. oldContainerName := currentContainerName + OldSuffix
  308. // Check if old container exists and remove it if it does
  309. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  310. if err != nil {
  311. return errors.Wrap(err, "failed to list containers")
  312. }
  313. for _, c := range containers {
  314. for _, name := range c.Names {
  315. processedName := strings.TrimPrefix(name, "/")
  316. // Remove old container
  317. if processedName == oldContainerName {
  318. err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
  319. if err != nil {
  320. logger.Error("Failed to remove old container:", err)
  321. // Continue execution, don't interrupt because of failure to remove old container
  322. } else {
  323. logger.Info("Successfully removed old container:", oldContainerName)
  324. }
  325. break
  326. }
  327. }
  328. }
  329. // Clean up all temp containers
  330. err = removeAllTempContainers(ctx, cli)
  331. if err != nil {
  332. logger.Error("Failed to clean up temp containers:", err)
  333. // Continue execution despite cleanup errors
  334. }
  335. return nil
  336. }