agent.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. package docker
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "os"
  9. "strings"
  10. "time"
  11. "github.com/docker/distribution/reference"
  12. "github.com/docker/docker/api/types"
  13. "github.com/docker/docker/api/types/container"
  14. "github.com/docker/docker/api/types/filters"
  15. "github.com/docker/docker/api/types/network"
  16. "github.com/docker/docker/api/types/volume"
  17. "github.com/docker/docker/client"
  18. "github.com/moby/moby/pkg/jsonmessage"
  19. "github.com/moby/term"
  20. )
  21. // Agent is a Docker client for performing operations that interact
  22. // with the Docker engine over REST
  23. type Agent struct {
  24. authGetter *AuthGetter
  25. client *client.Client
  26. ctx context.Context
  27. label string
  28. }
  29. // CreateLocalVolumeIfNotExist creates a volume using driver type "local" with the
  30. // given name if it does not exist. If the volume does exist but does not contain
  31. // the required label (a.label), an error is thrown.
  32. func (a *Agent) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error) {
  33. volListBody, err := a.client.VolumeList(a.ctx, filters.Args{})
  34. if err != nil {
  35. return nil, a.handleDockerClientErr(err, "Could not list volumes")
  36. }
  37. for _, _vol := range volListBody.Volumes {
  38. if contains, ok := _vol.Labels[a.label]; ok && contains == "true" && _vol.Name == name {
  39. return _vol, nil
  40. } else if !ok && _vol.Name == name {
  41. return nil, fmt.Errorf("volume conflict for %s: please remove existing volume and try again", name)
  42. }
  43. }
  44. return a.CreateLocalVolume(name)
  45. }
  46. // CreateLocalVolume creates a volume using driver type "local" with no
  47. // configured options. The equivalent of:
  48. //
  49. // docker volume create --driver local [name]
  50. func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
  51. labels := make(map[string]string)
  52. labels[a.label] = "true"
  53. opts := volume.VolumeCreateBody{
  54. Name: name,
  55. Driver: "local",
  56. Labels: labels,
  57. }
  58. vol, err := a.client.VolumeCreate(a.ctx, opts)
  59. if err != nil {
  60. return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
  61. }
  62. return &vol, nil
  63. }
  64. // RemoveLocalVolume removes a volume by name
  65. func (a *Agent) RemoveLocalVolume(name string) error {
  66. return a.client.VolumeRemove(a.ctx, name, true)
  67. }
  68. // CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
  69. // given name if it does not exist. If the volume does exist but does not contain
  70. // the required label (a.label), an error is thrown.
  71. func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error) {
  72. networks, err := a.client.NetworkList(a.ctx, types.NetworkListOptions{})
  73. if err != nil {
  74. return "", a.handleDockerClientErr(err, "Could not list volumes")
  75. }
  76. for _, net := range networks {
  77. if contains, ok := net.Labels[a.label]; ok && contains == "true" && net.Name == name {
  78. return net.ID, nil
  79. } else if !ok && net.Name == name {
  80. return "", fmt.Errorf("network conflict for %s: please remove existing network and try again", name)
  81. }
  82. }
  83. return a.CreateBridgeNetwork(name)
  84. }
  85. // CreateBridgeNetwork creates a volume using the default driver type (bridge)
  86. // with the CLI label attached
  87. func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
  88. labels := make(map[string]string)
  89. labels[a.label] = "true"
  90. opts := types.NetworkCreate{
  91. Labels: labels,
  92. Attachable: true,
  93. }
  94. net, err := a.client.NetworkCreate(a.ctx, name, opts)
  95. if err != nil {
  96. return "", a.handleDockerClientErr(err, "Could not create network "+name)
  97. }
  98. return net.ID, nil
  99. }
  100. // ConnectContainerToNetwork attaches a container to a specified network
  101. func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName string) error {
  102. // check if the container is connected already
  103. net, err := a.client.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
  104. if err != nil {
  105. return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
  106. }
  107. for _, cont := range net.Containers {
  108. // if container is connected, just return
  109. if cont.Name == containerName {
  110. return nil
  111. }
  112. }
  113. return a.client.NetworkConnect(a.ctx, networkID, containerID, &network.EndpointSettings{})
  114. }
  115. func (a *Agent) TagImage(old, new string) error {
  116. return a.client.ImageTag(a.ctx, old, new)
  117. }
  118. // PullImageEvent represents a response from the Docker API with an image pull event
  119. type PullImageEvent struct {
  120. Status string `json:"status"`
  121. Error string `json:"error"`
  122. Progress string `json:"progress"`
  123. ProgressDetail struct {
  124. Current int `json:"current"`
  125. Total int `json:"total"`
  126. } `json:"progressDetail"`
  127. }
  128. var PullImageErrNotFound = fmt.Errorf("Requested image not found")
  129. var PullImageErrUnauthorized = fmt.Errorf("Could not pull image: unauthorized")
  130. // CheckIfImageExists checks if the image exists in the registry
  131. func (a *Agent) CheckIfImageExists(image string) (bool, error) {
  132. encodedRegistryAuth, err := a.getEncodedRegistryAuth(image)
  133. if err != nil {
  134. return false, err
  135. }
  136. _, err = a.client.DistributionInspect(context.Background(), image, encodedRegistryAuth)
  137. if err == nil {
  138. return true, nil
  139. } else if strings.Contains(err.Error(), "image not found") ||
  140. strings.Contains(err.Error(), "does not exist in the registry") {
  141. return false, nil
  142. }
  143. return false, err
  144. }
  145. // PullImage pulls an image specified by the image string
  146. func (a *Agent) PullImage(image string) error {
  147. opts, err := a.getPullOptions(image)
  148. if err != nil {
  149. return err
  150. }
  151. // pull the specified image
  152. out, err := a.client.ImagePull(a.ctx, image, opts)
  153. if err != nil {
  154. if client.IsErrNotFound(err) {
  155. return PullImageErrNotFound
  156. } else if client.IsErrUnauthorized(err) {
  157. return PullImageErrUnauthorized
  158. } else {
  159. return a.handleDockerClientErr(err, "Could not pull image "+image)
  160. }
  161. }
  162. defer out.Close()
  163. termFd, isTerm := term.GetFdInfo(os.Stderr)
  164. return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)
  165. }
  166. // PushImage pushes an image specified by the image string
  167. func (a *Agent) PushImage(image string) error {
  168. opts, err := a.getPushOptions(image)
  169. if err != nil {
  170. return err
  171. }
  172. out, err := a.client.ImagePush(
  173. context.Background(),
  174. image,
  175. opts,
  176. )
  177. if out != nil {
  178. defer out.Close()
  179. }
  180. if err != nil {
  181. return err
  182. }
  183. termFd, isTerm := term.GetFdInfo(os.Stderr)
  184. return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)
  185. }
  186. func (a *Agent) getPullOptions(image string) (types.ImagePullOptions, error) {
  187. // check if agent has an auth getter; otherwise, assume public usage
  188. if a.authGetter == nil {
  189. return types.ImagePullOptions{}, nil
  190. }
  191. authConfigEncoded, err := a.getEncodedRegistryAuth(image)
  192. if err != nil {
  193. return types.ImagePullOptions{}, err
  194. }
  195. return types.ImagePullOptions{
  196. RegistryAuth: authConfigEncoded,
  197. Platform: "linux/amd64",
  198. }, nil
  199. }
  200. func (a *Agent) getEncodedRegistryAuth(image string) (string, error) {
  201. // get using server url
  202. serverURL, err := GetServerURLFromTag(image)
  203. if err != nil {
  204. return "", err
  205. }
  206. user, secret, err := a.authGetter.GetCredentials(serverURL)
  207. if err != nil {
  208. return "", err
  209. }
  210. var authConfig = types.AuthConfig{
  211. Username: user,
  212. Password: secret,
  213. ServerAddress: "https://" + serverURL,
  214. }
  215. authConfigBytes, _ := json.Marshal(authConfig)
  216. return base64.URLEncoding.EncodeToString(authConfigBytes), nil
  217. }
  218. func (a *Agent) getPushOptions(image string) (types.ImagePushOptions, error) {
  219. pullOpts, err := a.getPullOptions(image)
  220. return types.ImagePushOptions(pullOpts), err
  221. }
  222. func GetServerURLFromTag(image string) (string, error) {
  223. named, err := reference.ParseNamed(image)
  224. if err != nil {
  225. return "", err
  226. }
  227. domain := reference.Domain(named)
  228. if domain == "" {
  229. // if domain name is empty, use index.docker.io/v1
  230. return "index.docker.io/v1", nil
  231. } else if matches := ecrPattern.FindStringSubmatch(image); len(matches) >= 3 {
  232. // if this matches ECR, just use the domain name
  233. return domain, nil
  234. } else if strings.Contains(image, "gcr.io") || strings.Contains(image, "registry.digitalocean.com") {
  235. // if this matches GCR or DOCR, use the first path component
  236. return fmt.Sprintf("%s/%s", domain, strings.Split(reference.Path(named), "/")[0]), nil
  237. }
  238. // otherwise, best-guess is to get components of path that aren't the image name
  239. pathParts := strings.Split(reference.Path(named), "/")
  240. nonImagePath := ""
  241. if len(pathParts) > 1 {
  242. nonImagePath = strings.Join(pathParts[0:len(pathParts)-1], "/")
  243. }
  244. if err != nil {
  245. return "", err
  246. }
  247. return fmt.Sprintf("%s/%s", domain, nonImagePath), nil
  248. }
  249. // WaitForContainerStop waits until a container has stopped to exit
  250. func (a *Agent) WaitForContainerStop(id string) error {
  251. // wait for container to stop before exit
  252. statusCh, errCh := a.client.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
  253. select {
  254. case err := <-errCh:
  255. if err != nil {
  256. return a.handleDockerClientErr(err, "Error waiting for stopped container")
  257. }
  258. case <-statusCh:
  259. }
  260. return nil
  261. }
  262. // WaitForContainerHealthy waits until a container is returning a healthy status. Streak
  263. // is the maximum number of failures in a row, while timeout is the length of time between
  264. // checks.
  265. func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
  266. for {
  267. cont, err := a.client.ContainerInspect(a.ctx, id)
  268. if err != nil {
  269. return a.handleDockerClientErr(err, "Error waiting for stopped container")
  270. }
  271. health := cont.State.Health
  272. if health == nil || health.Status == "healthy" {
  273. return nil
  274. } else if health.FailingStreak >= streak {
  275. break
  276. }
  277. time.Sleep(time.Second)
  278. }
  279. return errors.New("container not healthy")
  280. }
  281. // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //
  282. func (a *Agent) handleDockerClientErr(err error, errPrefix string) error {
  283. if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
  284. return fmt.Errorf("The Docker daemon must be running in order to start Porter: connection to %s failed", a.client.DaemonHost())
  285. }
  286. return fmt.Errorf("%s:%s", errPrefix, err.Error())
  287. }