porter.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package docker
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/docker/docker/api/types"
  6. "github.com/docker/docker/api/types/container"
  7. "github.com/docker/docker/api/types/mount"
  8. "github.com/docker/go-connections/nat"
  9. )
  10. // PorterStartOpts are the options for starting the Porter container
  11. type PorterStartOpts struct {
  12. Name string
  13. Image string
  14. StartCmd []string
  15. HostPort uint
  16. ContainerPort uint
  17. Mounts []mount.Mount
  18. VolumeMap map[string]struct{}
  19. Env []string
  20. NetworkID string
  21. }
  22. // StartPorterContainer pulls a specific Porter image and starts a container
  23. // using the Docker engine. It returns the container ID
  24. func (a *Agent) StartPorterContainer(opts PorterStartOpts) (string, error) {
  25. id, err := a.upsertPorterContainer(opts)
  26. if err != nil {
  27. return "", err
  28. }
  29. err = a.startPorterContainer(id)
  30. if err != nil {
  31. return "", err
  32. }
  33. // attach container to network
  34. err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
  35. if err != nil {
  36. return "", err
  37. }
  38. return id, nil
  39. }
  40. // detect if container exists and is running, and stop
  41. // if spec has changed, remove and recreate container
  42. // if container does not exist, create the container
  43. // otherwise, return stopped container
  44. func (a *Agent) upsertPorterContainer(opts PorterStartOpts) (id string, err error) {
  45. containers, err := a.getContainersCreatedByStart()
  46. // remove the matching container
  47. for _, container := range containers {
  48. if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
  49. timeout, _ := time.ParseDuration("15s")
  50. err := a.client.ContainerStop(a.ctx, container.ID, &timeout)
  51. if err != nil {
  52. return "", a.handleDockerClientErr(err, "Could not stop container "+container.ID)
  53. }
  54. err = a.client.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
  55. if err != nil {
  56. return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
  57. }
  58. }
  59. }
  60. return a.pullAndCreatePorterContainer(opts)
  61. }
  62. // create the container and return its id
  63. func (a *Agent) pullAndCreatePorterContainer(opts PorterStartOpts) (id string, err error) {
  64. a.PullImage(opts.Image)
  65. // format the port array for binding to host machine
  66. ports := []string{fmt.Sprintf("127.0.0.1:%d:%d/tcp", opts.HostPort, opts.ContainerPort)}
  67. _, portBindings, err := nat.ParsePortSpecs(ports)
  68. if err != nil {
  69. return "", fmt.Errorf("Unable to parse port specification %s", ports)
  70. }
  71. labels := make(map[string]string)
  72. labels[a.label] = "true"
  73. // create the container with a label specifying this was created via the CLI
  74. resp, err := a.client.ContainerCreate(a.ctx, &container.Config{
  75. Image: opts.Image,
  76. Cmd: opts.StartCmd,
  77. Tty: false,
  78. Labels: labels,
  79. Volumes: opts.VolumeMap,
  80. Env: opts.Env,
  81. }, &container.HostConfig{
  82. PortBindings: portBindings,
  83. Mounts: opts.Mounts,
  84. }, nil, opts.Name)
  85. if err != nil {
  86. return "", a.handleDockerClientErr(err, "Could not create Porter container")
  87. }
  88. return resp.ID, nil
  89. }
  90. // start the container
  91. func (a *Agent) startPorterContainer(id string) error {
  92. if err := a.client.ContainerStart(a.ctx, id, types.ContainerStartOptions{}); err != nil {
  93. return a.handleDockerClientErr(err, "Could not start Porter container")
  94. }
  95. return nil
  96. }
  97. // PostgresOpts are the options for starting the Postgres DB
  98. type PostgresOpts struct {
  99. Name string
  100. Image string
  101. Env []string
  102. VolumeMap map[string]struct{}
  103. Mounts []mount.Mount
  104. NetworkID string
  105. }
  106. // StartPostgresContainer pulls a specific Porter image and starts a container
  107. // using the Docker engine
  108. func (a *Agent) StartPostgresContainer(opts PostgresOpts) (string, error) {
  109. id, err := a.upsertPostgresContainer(opts)
  110. if err != nil {
  111. return "", err
  112. }
  113. err = a.startPostgresContainer(id)
  114. if err != nil {
  115. return "", err
  116. }
  117. // attach container to network
  118. err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
  119. if err != nil {
  120. return "", err
  121. }
  122. return id, nil
  123. }
  124. // detect if container exists and is running, and stop
  125. // if it is running, stop it
  126. // if it is stopped, return id
  127. // if it does not exist, create it and return it
  128. func (a *Agent) upsertPostgresContainer(opts PostgresOpts) (id string, err error) {
  129. containers, err := a.getContainersCreatedByStart()
  130. // stop the matching container and return it
  131. for _, container := range containers {
  132. if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
  133. timeout, _ := time.ParseDuration("15s")
  134. err := a.client.ContainerStop(a.ctx, container.ID, &timeout)
  135. if err != nil {
  136. return "", a.handleDockerClientErr(err, "Could not stop postgres container "+container.ID)
  137. }
  138. return container.ID, nil
  139. }
  140. }
  141. return a.pullAndCreatePostgresContainer(opts)
  142. }
  143. // create the container and return it
  144. func (a *Agent) pullAndCreatePostgresContainer(opts PostgresOpts) (id string, err error) {
  145. a.PullImage(opts.Image)
  146. labels := make(map[string]string)
  147. labels[a.label] = "true"
  148. // create the container with a label specifying this was created via the CLI
  149. resp, err := a.client.ContainerCreate(a.ctx, &container.Config{
  150. Image: opts.Image,
  151. Tty: false,
  152. Labels: labels,
  153. Volumes: opts.VolumeMap,
  154. Env: opts.Env,
  155. ExposedPorts: nat.PortSet{
  156. "5432": struct{}{},
  157. },
  158. Healthcheck: &container.HealthConfig{
  159. Test: []string{"CMD-SHELL", "pg_isready"},
  160. Interval: 10 * time.Second,
  161. Timeout: 5 * time.Second,
  162. Retries: 3,
  163. },
  164. }, &container.HostConfig{
  165. Mounts: opts.Mounts,
  166. }, nil, opts.Name)
  167. if err != nil {
  168. return "", a.handleDockerClientErr(err, "Could not create Porter container")
  169. }
  170. return resp.ID, nil
  171. }
  172. // start the container in the background
  173. func (a *Agent) startPostgresContainer(id string) error {
  174. if err := a.client.ContainerStart(a.ctx, id, types.ContainerStartOptions{}); err != nil {
  175. return a.handleDockerClientErr(err, "Could not start Postgres container")
  176. }
  177. return nil
  178. }
  179. // StopPorterContainers finds all containers that were started via the CLI and stops them
  180. // without removal.
  181. func (a *Agent) StopPorterContainers() error {
  182. fmt.Println("Stopping containers...")
  183. containers, err := a.getContainersCreatedByStart()
  184. if err != nil {
  185. return err
  186. }
  187. // remove all Porter containers
  188. for _, container := range containers {
  189. timeout, _ := time.ParseDuration("15s")
  190. err := a.client.ContainerStop(a.ctx, container.ID, &timeout)
  191. if err != nil {
  192. return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
  193. }
  194. }
  195. return nil
  196. }
  197. // getContainersCreatedByStart gets all containers that were created by the "porter start"
  198. // command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
  199. func (a *Agent) getContainersCreatedByStart() ([]types.Container, error) {
  200. containers, err := a.client.ContainerList(a.ctx, types.ContainerListOptions{
  201. All: true,
  202. })
  203. if err != nil {
  204. return nil, a.handleDockerClientErr(err, "Could not list containers")
  205. }
  206. res := make([]types.Container, 0)
  207. for _, container := range containers {
  208. if contains, ok := container.Labels[a.label]; ok && contains == "true" {
  209. res = append(res, container)
  210. }
  211. }
  212. return res, nil
  213. }