ota.go 10 KB

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