agent.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. package docker
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "strings"
  8. "time"
  9. "github.com/docker/docker/api/types"
  10. "github.com/docker/docker/api/types/container"
  11. "github.com/docker/docker/api/types/filters"
  12. "github.com/docker/docker/api/types/network"
  13. "github.com/docker/docker/api/types/volume"
  14. "github.com/docker/docker/client"
  15. )
  16. // Agent is a Docker client for performing operations that interact
  17. // with the Docker engine over REST
  18. type Agent struct {
  19. client *client.Client
  20. ctx context.Context
  21. label string
  22. }
  23. // CreateLocalVolumeIfNotExist creates a volume using driver type "local" with the
  24. // given name if it does not exist. If the volume does exist but does not contain
  25. // the required label (a.label), an error is thrown.
  26. func (a *Agent) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error) {
  27. volListBody, err := a.client.VolumeList(a.ctx, filters.Args{})
  28. if err != nil {
  29. return nil, a.handleDockerClientErr(err, "Could not list volumes")
  30. }
  31. for _, _vol := range volListBody.Volumes {
  32. if contains, ok := _vol.Labels[a.label]; ok && contains == "true" && _vol.Name == name {
  33. return _vol, nil
  34. } else if !ok && _vol.Name == name {
  35. return nil, fmt.Errorf("volume conflict for %s: please remove existing volume and try again", name)
  36. }
  37. }
  38. return a.CreateLocalVolume(name)
  39. }
  40. // CreateLocalVolume creates a volume using driver type "local" with no
  41. // configured options. The equivalent of:
  42. //
  43. // docker volume create --driver local [name]
  44. func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
  45. labels := make(map[string]string)
  46. labels[a.label] = "true"
  47. opts := volume.VolumeCreateBody{
  48. Name: name,
  49. Driver: "local",
  50. Labels: labels,
  51. }
  52. vol, err := a.client.VolumeCreate(a.ctx, opts)
  53. if err != nil {
  54. return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
  55. }
  56. return &vol, nil
  57. }
  58. // RemoveLocalVolume removes a volume by name
  59. func (a *Agent) RemoveLocalVolume(name string) error {
  60. return a.client.VolumeRemove(a.ctx, name, true)
  61. }
  62. // CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
  63. // given name if it does not exist. If the volume does exist but does not contain
  64. // the required label (a.label), an error is thrown.
  65. func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error) {
  66. networks, err := a.client.NetworkList(a.ctx, types.NetworkListOptions{})
  67. if err != nil {
  68. return "", a.handleDockerClientErr(err, "Could not list volumes")
  69. }
  70. for _, net := range networks {
  71. if contains, ok := net.Labels[a.label]; ok && contains == "true" && net.Name == name {
  72. return net.ID, nil
  73. } else if !ok && net.Name == name {
  74. return "", fmt.Errorf("network conflict for %s: please remove existing network and try again", name)
  75. }
  76. }
  77. return a.CreateBridgeNetwork(name)
  78. }
  79. // CreateBridgeNetwork creates a volume using the default driver type (bridge)
  80. // with the CLI label attached
  81. func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
  82. labels := make(map[string]string)
  83. labels[a.label] = "true"
  84. opts := types.NetworkCreate{
  85. Labels: labels,
  86. Attachable: true,
  87. }
  88. net, err := a.client.NetworkCreate(a.ctx, name, opts)
  89. if err != nil {
  90. return "", a.handleDockerClientErr(err, "Could not create network "+name)
  91. }
  92. return net.ID, nil
  93. }
  94. // ConnectContainerToNetwork attaches a container to a specified network
  95. func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName string) error {
  96. // check if the container is connected already
  97. net, err := a.client.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
  98. if err != nil {
  99. return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
  100. }
  101. for _, cont := range net.Containers {
  102. // if container is connected, just return
  103. if cont.Name == containerName {
  104. return nil
  105. }
  106. }
  107. return a.client.NetworkConnect(a.ctx, networkID, containerID, &network.EndpointSettings{})
  108. }
  109. // PullImageEvent represents a response from the Docker API with an image pull event
  110. type PullImageEvent struct {
  111. Status string `json:"status"`
  112. Error string `json:"error"`
  113. Progress string `json:"progress"`
  114. ProgressDetail struct {
  115. Current int `json:"current"`
  116. Total int `json:"total"`
  117. } `json:"progressDetail"`
  118. }
  119. // PullImage pulls an image specified by the image string
  120. func (a *Agent) PullImage(image string) error {
  121. fmt.Println("Pulling image:", image)
  122. // pull the specified image
  123. out, err := a.client.ImagePull(a.ctx, image, types.ImagePullOptions{})
  124. if err != nil {
  125. return a.handleDockerClientErr(err, "Could not pull image"+image)
  126. }
  127. decoder := json.NewDecoder(out)
  128. var event *PullImageEvent
  129. for {
  130. if err := decoder.Decode(&event); err != nil {
  131. if err == io.EOF {
  132. break
  133. }
  134. return err
  135. }
  136. }
  137. fmt.Println("Finished pulling image:", image)
  138. return nil
  139. }
  140. // WaitForContainerStop waits until a container has stopped to exit
  141. func (a *Agent) WaitForContainerStop(id string) error {
  142. // wait for container to stop before exit
  143. statusCh, errCh := a.client.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
  144. select {
  145. case err := <-errCh:
  146. if err != nil {
  147. return a.handleDockerClientErr(err, "Error waiting for stopped container")
  148. }
  149. case <-statusCh:
  150. }
  151. return nil
  152. }
  153. // WaitForContainerHealthy waits until a container is returning a healthy status. Streak
  154. // is the maximum number of failures in a row, while timeout is the length of time between
  155. // checks.
  156. func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
  157. for {
  158. cont, err := a.client.ContainerInspect(a.ctx, id)
  159. if err != nil {
  160. return a.handleDockerClientErr(err, "Error waiting for stopped container")
  161. }
  162. health := cont.State.Health
  163. if health == nil || health.Status == "healthy" || health.FailingStreak >= streak {
  164. break
  165. }
  166. time.Sleep(time.Second)
  167. }
  168. return nil
  169. }
  170. // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //
  171. func (a *Agent) handleDockerClientErr(err error, errPrefix string) error {
  172. if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
  173. return fmt.Errorf("The Docker daemon must be running in order to start Porter: connection to %s failed", a.client.DaemonHost())
  174. }
  175. return fmt.Errorf("%s:%s", errPrefix, err.Error())
  176. }