agent.go 13 KB

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