ota.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. hostname, err := os.Hostname()
  126. if err != nil {
  127. return cosy.WrapErrorWithParams(ErrFailedToGetHostname, err.Error())
  128. }
  129. containerInfo, err := cli.ContainerInspect(ctx, hostname)
  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. PortBindings: containerInfo.HostConfig.PortBindings,
  150. RestartPolicy: containerInfo.HostConfig.RestartPolicy,
  151. },
  152. nil,
  153. nil,
  154. tempContainerName,
  155. )
  156. if err != nil {
  157. return cosy.WrapErrorWithParams(ErrFailedToCreateTempContainer, err.Error())
  158. }
  159. // Start the temp container to execute step 2
  160. err = cli.ContainerStart(ctx, tempContainerName, container.StartOptions{})
  161. if err != nil {
  162. return cosy.WrapErrorWithParams(ErrFailedToStartTempContainer, err.Error())
  163. }
  164. // Output status information
  165. logger.Info("Docker OTA upgrade step 1 completed. Temp container started to execute step 2.")
  166. return nil
  167. }
  168. // UpgradeStepTwo Trigger in the temp container
  169. func UpgradeStepTwo(ctx context.Context) (err error) {
  170. // 1. Copy the old config
  171. cli, err := initClient()
  172. if err != nil {
  173. return
  174. }
  175. defer cli.Close()
  176. // Get old container name from environment variable, fallback to settings if not available
  177. currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
  178. if currentContainerName == "" {
  179. return errors.New("could not find old container name")
  180. }
  181. // Get the current running temp container name
  182. // Since we can't directly get our own container name from inside, we'll search all temp containers
  183. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  184. if err != nil {
  185. return errors.Wrap(err, "failed to list containers")
  186. }
  187. // Find containers with the temp prefix
  188. var tempContainerName string
  189. for _, c := range containers {
  190. for _, name := range c.Names {
  191. processedName := strings.TrimPrefix(name, "/")
  192. if strings.HasPrefix(processedName, TempPrefix) {
  193. tempContainerName = processedName
  194. break
  195. }
  196. }
  197. if tempContainerName != "" {
  198. break
  199. }
  200. }
  201. if tempContainerName == "" {
  202. return errors.New("could not find temp container")
  203. }
  204. // Get temp container info to get the new image
  205. tempContainerInfo, err := cli.ContainerInspect(ctx, tempContainerName)
  206. if err != nil {
  207. return errors.Wrap(err, "failed to inspect temp container")
  208. }
  209. newImage := tempContainerInfo.Config.Image
  210. // Get current container info
  211. oldContainerInfo, err := cli.ContainerInspect(ctx, currentContainerName)
  212. if err != nil {
  213. return errors.Wrap(err, "failed to inspect current container")
  214. }
  215. // 2. Stop the old container and rename to _old
  216. err = cli.ContainerStop(ctx, currentContainerName, container.StopOptions{})
  217. if err != nil {
  218. return errors.Wrap(err, "failed to stop current container")
  219. }
  220. // Rename the old container with _old suffix
  221. err = cli.ContainerRename(ctx, currentContainerName, currentContainerName+OldSuffix)
  222. if err != nil {
  223. return errors.Wrap(err, "failed to rename old container")
  224. }
  225. // 3. Use the old config to create and start a new container with the updated image
  226. // Create new container with original config but using the new image
  227. newContainerEnv := oldContainerInfo.Config.Env
  228. // Pass the old container name to the new container
  229. newContainerEnv = append(newContainerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))
  230. _, err = cli.ContainerCreate(
  231. ctx,
  232. &container.Config{
  233. Image: newImage,
  234. Cmd: oldContainerInfo.Config.Cmd,
  235. Env: newContainerEnv,
  236. Entrypoint: oldContainerInfo.Config.Entrypoint,
  237. Labels: oldContainerInfo.Config.Labels,
  238. ExposedPorts: oldContainerInfo.Config.ExposedPorts,
  239. Volumes: oldContainerInfo.Config.Volumes,
  240. WorkingDir: oldContainerInfo.Config.WorkingDir,
  241. },
  242. &container.HostConfig{
  243. Binds: oldContainerInfo.HostConfig.Binds,
  244. PortBindings: oldContainerInfo.HostConfig.PortBindings,
  245. RestartPolicy: oldContainerInfo.HostConfig.RestartPolicy,
  246. NetworkMode: oldContainerInfo.HostConfig.NetworkMode,
  247. Mounts: oldContainerInfo.HostConfig.Mounts,
  248. Privileged: oldContainerInfo.HostConfig.Privileged,
  249. },
  250. nil,
  251. nil,
  252. currentContainerName,
  253. )
  254. if err != nil {
  255. // If creation fails, try to recover
  256. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  257. if recoverErr == nil {
  258. // Start old container
  259. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  260. if recoverErr == nil {
  261. return errors.Wrap(err, "failed to create new container, recovered to old container")
  262. }
  263. }
  264. return errors.Wrap(err, "failed to create new container and failed to recover")
  265. }
  266. // Start the new container
  267. err = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  268. if err != nil {
  269. // If startup fails, try to recover
  270. // First remove the failed new container
  271. removeErr := cli.ContainerRemove(ctx, currentContainerName, container.RemoveOptions{Force: true})
  272. if removeErr != nil {
  273. logger.Error("Failed to remove failed new container:", removeErr)
  274. }
  275. // Rename the old container back to original
  276. recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
  277. if recoverErr == nil {
  278. // Start old container
  279. recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
  280. if recoverErr == nil {
  281. return errors.Wrap(err, "failed to start new container, recovered to old container")
  282. }
  283. }
  284. return errors.Wrap(err, "failed to start new container and failed to recover")
  285. }
  286. logger.Info("Docker OTA upgrade step 2 completed successfully. New container is running.")
  287. return nil
  288. }
  289. // UpgradeStepThree Trigger in the new container
  290. func UpgradeStepThree() error {
  291. ctx := context.Background()
  292. // Remove the old container
  293. cli, err := initClient()
  294. if err != nil {
  295. return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
  296. }
  297. defer cli.Close()
  298. // Get old container name from environment variable, fallback to settings if not available
  299. currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
  300. if currentContainerName == "" {
  301. return nil
  302. }
  303. oldContainerName := currentContainerName + OldSuffix
  304. // Check if old container exists and remove it if it does
  305. containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
  306. if err != nil {
  307. return errors.Wrap(err, "failed to list containers")
  308. }
  309. for _, c := range containers {
  310. for _, name := range c.Names {
  311. processedName := strings.TrimPrefix(name, "/")
  312. // Remove old container
  313. if processedName == oldContainerName {
  314. err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
  315. if err != nil {
  316. logger.Error("Failed to remove old container:", err)
  317. // Continue execution, don't interrupt because of failure to remove old container
  318. } else {
  319. logger.Info("Successfully removed old container:", oldContainerName)
  320. }
  321. break
  322. }
  323. }
  324. }
  325. // Clean up all temp containers
  326. err = removeAllTempContainers(ctx, cli)
  327. if err != nil {
  328. logger.Error("Failed to clean up temp containers:", err)
  329. // Continue execution despite cleanup errors
  330. }
  331. return nil
  332. }