docker.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. package docker
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "goseg/config"
  7. "goseg/structs"
  8. "log/slog"
  9. "os"
  10. "strings"
  11. "github.com/docker/docker/api/types"
  12. "github.com/docker/docker/client"
  13. "github.com/docker/docker/api/types/container"
  14. )
  15. var (
  16. logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
  17. EventBus = make(chan structs.Event, 100)
  18. )
  19. func GetShipStatus(patps []string) (map[string]string, error) {
  20. statuses := make(map[string]string)
  21. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  22. if err != nil {
  23. errmsg := fmt.Sprintf("Error getting Docker info: %v", err)
  24. logger.Error(errmsg)
  25. return statuses, err
  26. } else {
  27. containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
  28. if err != nil {
  29. errmsg := fmt.Sprintf("Error getting containers: %v", err)
  30. logger.Error(errmsg)
  31. return statuses, err
  32. } else {
  33. for _, pier := range patps {
  34. found := false
  35. for _, container := range containers {
  36. for _, name := range container.Names {
  37. fasPier := "/" + pier
  38. if name == fasPier {
  39. statuses[pier] = container.Status
  40. found = true
  41. break
  42. }
  43. }
  44. if found {
  45. break
  46. }
  47. }
  48. if !found {
  49. statuses[pier] = "not found"
  50. }
  51. }
  52. }
  53. return statuses, nil
  54. }
  55. }
  56. // return the name of a container's network
  57. func GetContainerNetwork(name string) (string, error) {
  58. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  59. if err != nil {
  60. return "", err
  61. }
  62. defer cli.Close()
  63. containerJSON, err := cli.ContainerInspect(context.Background(), name)
  64. if err != nil {
  65. return "", err
  66. }
  67. for networkName := range containerJSON.NetworkSettings.Networks {
  68. return networkName, nil
  69. }
  70. return "", fmt.Errorf("container is not attached to any network")
  71. }
  72. // return the disk and memory usage for a container
  73. func GetContainerStats(containerName string) (structs.ContainerStats, error) {
  74. var res structs.ContainerStats
  75. cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
  76. if err != nil {
  77. return res, err
  78. }
  79. defer cli.Close()
  80. statsResp, err := cli.ContainerStats(context.Background(), containerName, false)
  81. if err != nil {
  82. return res, err
  83. }
  84. defer statsResp.Body.Close()
  85. var stat types.StatsJSON
  86. if err := json.NewDecoder(statsResp.Body).Decode(&stat); err != nil {
  87. return res, err
  88. }
  89. memUsage := stat.MemoryStats.Usage
  90. inspectResp, err := cli.ContainerInspect(context.Background(), containerName)
  91. if err != nil {
  92. return res, err
  93. }
  94. diskUsage := int64(0)
  95. if inspectResp.SizeRw != nil {
  96. diskUsage = *inspectResp.SizeRw
  97. }
  98. return structs.ContainerStats{
  99. MemoryUsage: memUsage,
  100. DiskUsage: diskUsage,
  101. }, nil
  102. }
  103. // start a container by name + tag
  104. // not for booting new ships
  105. func StartContainer(containerName string, containerType string) error {
  106. ctx := context.Background()
  107. cli, err := client.NewClientWithOpts(client.FromEnv)
  108. if err != nil {
  109. return err
  110. }
  111. // get the desired tag and hash from config
  112. containerInfo, err := getLatestContainerInfo(containerType)
  113. if err != nil {
  114. return err
  115. }
  116. // check if container exists
  117. containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
  118. if err != nil {
  119. return err
  120. }
  121. var existingContainer *types.Container = nil
  122. for _, container := range containers {
  123. for _, name := range container.Names {
  124. if name == "/"+containerName {
  125. existingContainer = &container
  126. break
  127. }
  128. }
  129. if existingContainer != nil {
  130. break
  131. }
  132. }
  133. desiredTag := containerInfo["tag"]
  134. desiredHash := containerInfo["hash"]
  135. desiredRepo := containerInfo["repo"]
  136. if desiredTag == "" || desiredHash == "" {
  137. err = fmt.Errorf("Version info has not been retrieved!")
  138. return err
  139. }
  140. // check if the desired image is available locally
  141. images, err := cli.ImageList(ctx, types.ImageListOptions{})
  142. if err != nil {
  143. return err
  144. }
  145. imageExistsLocally := false
  146. for _, img := range images {
  147. for _, tag := range img.RepoTags {
  148. if tag == containerType+":"+desiredTag && img.ID == desiredHash {
  149. imageExistsLocally = true
  150. break
  151. }
  152. }
  153. if imageExistsLocally {
  154. break
  155. }
  156. }
  157. if !imageExistsLocally {
  158. // pull the image if it doesn't exist locally
  159. _, err = cli.ImagePull(ctx, desiredRepo+":"+desiredTag, types.ImagePullOptions{})
  160. if err != nil {
  161. return err
  162. }
  163. }
  164. switch {
  165. case existingContainer == nil:
  166. // if the container does not exist, create and start it
  167. _, err := cli.ContainerCreate(ctx, &container.Config{
  168. Image: containerType + ":" + desiredTag,
  169. }, nil, nil, nil, containerName)
  170. if err != nil {
  171. return err
  172. }
  173. err = cli.ContainerStart(ctx, containerName, types.ContainerStartOptions{})
  174. if err != nil {
  175. return err
  176. }
  177. msg := fmt.Sprintf("%s started with image %s:%s", containerName, containerType, desiredTag)
  178. logger.Info(msg)
  179. case existingContainer.State == "exited":
  180. // if the container exists but is stopped, start it
  181. err := cli.ContainerStart(ctx, containerName, types.ContainerStartOptions{})
  182. if err != nil {
  183. return err
  184. }
  185. msg := fmt.Sprintf("Started stopped container %s", containerName)
  186. logger.Info(msg)
  187. default:
  188. // if container is running, check the image tag
  189. currentImage := existingContainer.Image
  190. currentTag := strings.Split(currentImage, ":")[1]
  191. if currentTag != desiredTag {
  192. // if the tags don't match, recreate the container with the new tag
  193. err := cli.ContainerRemove(ctx, containerName, types.ContainerRemoveOptions{Force: true})
  194. if err != nil {
  195. return err
  196. }
  197. _, err = cli.ContainerCreate(ctx, &container.Config{
  198. Image: containerType + ":" + desiredTag,
  199. }, nil, nil, nil, containerName)
  200. if err != nil {
  201. return err
  202. }
  203. err = cli.ContainerStart(ctx, containerName, types.ContainerStartOptions{})
  204. if err != nil {
  205. return err
  206. }
  207. msg := fmt.Sprintf("Restarted %s with image %s:%s", containerName, containerType, desiredTag)
  208. logger.Info(msg)
  209. } else {
  210. msg := fmt.Sprintf("%s is already running with the correct tag: %s", containerName, desiredTag)
  211. logger.Info(msg)
  212. }
  213. }
  214. return nil
  215. }
  216. // convert the version info back into json then a map lol
  217. // so we can easily get the correct repo/release channel/tag/hash
  218. func getLatestContainerInfo(containerType string) (map[string]string, error) {
  219. var res map[string]string
  220. conf := config.Conf()
  221. releaseChannel := conf.UpdateBranch
  222. arch := config.Architecture
  223. hashLabel := arch + "_sha256"
  224. versionInfo := config.VersionInfo
  225. jsonData, err := json.Marshal(versionInfo)
  226. if err != nil {
  227. return res, err
  228. }
  229. // Convert JSON to map
  230. var m map[string]interface{}
  231. err = json.Unmarshal(jsonData, &m)
  232. if err != nil {
  233. return res, err
  234. }
  235. groundseg, ok := m["groundseg"].(map[string]interface{})
  236. if !ok {
  237. return nil, fmt.Errorf("groundseg is not a map")
  238. }
  239. channel, ok := groundseg[releaseChannel].(map[string]interface{})
  240. if !ok {
  241. return nil, fmt.Errorf("%s is not a map", releaseChannel)
  242. }
  243. containerData, ok := channel[containerType].(map[string]interface{})
  244. if !ok {
  245. return nil, fmt.Errorf("%s data is not a map", containerType)
  246. }
  247. tag, ok := containerData["tag"].(string)
  248. if !ok {
  249. return nil, fmt.Errorf("'tag' is not a string")
  250. }
  251. hashValue, ok := containerData[hashLabel].(string)
  252. if !ok {
  253. return nil, fmt.Errorf("'%s' is not a string", hashLabel)
  254. }
  255. repo, ok := containerData["repo"].(string)
  256. if !ok {
  257. return nil, fmt.Errorf("'repo' is not a string")
  258. }
  259. res = make(map[string]string)
  260. res["tag"] = tag
  261. res["hash"] = hashValue
  262. res["repo"] = repo
  263. return res, nil
  264. }
  265. // stop a container with the name
  266. func StopContainerByName(containerName string) error {
  267. ctx := context.Background()
  268. cli, err := client.NewClientWithOpts(client.FromEnv)
  269. if err != nil {
  270. return err
  271. }
  272. // fetch all containers incl stopped
  273. containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
  274. if err != nil {
  275. return err
  276. }
  277. for _, cont := range containers {
  278. for _, name := range cont.Names {
  279. if name == "/"+containerName {
  280. // Stop the container
  281. options := container.StopOptions{}
  282. if err := cli.ContainerStop(ctx, cont.ID, options); err != nil {
  283. return fmt.Errorf("failed to stop container %s: %v", containerName, err)
  284. }
  285. logger.Info(fmt.Sprintf("Successfully stopped container %s\n", containerName))
  286. return nil
  287. }
  288. }
  289. }
  290. return fmt.Errorf("container with name %s not found", containerName)
  291. }
  292. func DockerListener() {
  293. ctx := context.Background()
  294. cli, err := client.NewClientWithOpts(client.FromEnv)
  295. if err != nil {
  296. logger.Error(fmt.Sprintf("Error initializing Docker client: %v", err))
  297. return
  298. }
  299. messages, errs := cli.Events(ctx, types.EventsOptions{})
  300. for {
  301. select {
  302. case event := <-messages:
  303. // Convert the Docker event to our custom event and send it to the EventBus
  304. EventBus <- structs.Event{Type: event.Action, Data: event}
  305. case err := <-errs:
  306. logger.Error(fmt.Sprintf("Docker event error: %v", err))
  307. }
  308. }
  309. }