2
0

porter.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package docker
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/docker/docker/api/types"
  8. "github.com/docker/docker/api/types/container"
  9. "github.com/docker/docker/api/types/mount"
  10. "github.com/docker/go-connections/nat"
  11. specs "github.com/opencontainers/image-spec/specs-go/v1"
  12. )
  13. // PorterDB is used for enumerating DB types
  14. type PorterDB int
  15. // The supported DB types
  16. const (
  17. Postgres PorterDB = iota
  18. SQLite
  19. )
  20. // PorterStartOpts are the options for starting the Porter stack
  21. type PorterStartOpts struct {
  22. ProcessID string
  23. ServerImageTag string
  24. ServerPort int
  25. DB PorterDB
  26. Env []string
  27. }
  28. // StartPorter creates a new Docker agent using the host environment, and creates a
  29. // new Porter instance
  30. func StartPorter(ctx context.Context, opts *PorterStartOpts) (agent *Agent, id string, err error) {
  31. agent, err = NewAgentFromEnv(ctx)
  32. if err != nil {
  33. return nil, "", err
  34. }
  35. // the volume mounts to use
  36. mounts := make([]mount.Mount, 0)
  37. // the volumes passed to the Porter container
  38. volumesMap := make(map[string]struct{})
  39. netID, err := agent.CreateBridgeNetworkIfNotExist(ctx, "porter_network_"+opts.ProcessID)
  40. if err != nil {
  41. return nil, "", err
  42. }
  43. switch opts.DB {
  44. case SQLite:
  45. // check if sqlite volume exists, create it if not
  46. vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_sqlite_"+opts.ProcessID)
  47. if err != nil {
  48. return nil, "", err
  49. }
  50. // create mount
  51. mount := mount.Mount{
  52. Type: mount.TypeVolume,
  53. Source: vol.Name,
  54. Target: "/sqlite",
  55. ReadOnly: false,
  56. Consistency: mount.ConsistencyFull,
  57. }
  58. mounts = append(mounts, mount)
  59. volumesMap[vol.Name] = struct{}{}
  60. opts.Env = append(opts.Env, []string{
  61. "SQL_LITE=true",
  62. "SQL_LITE_PATH=/sqlite/porter.db",
  63. }...)
  64. case Postgres:
  65. // check if postgres volume exists, create it if not
  66. vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_postgres_"+opts.ProcessID)
  67. if err != nil {
  68. return nil, "", err
  69. }
  70. // pgMount is mount for postgres container
  71. pgMount := []mount.Mount{
  72. {
  73. Type: mount.TypeVolume,
  74. Source: vol.Name,
  75. Target: "/var/lib/postgresql/data",
  76. ReadOnly: false,
  77. Consistency: mount.ConsistencyFull,
  78. },
  79. }
  80. // create postgres container with mount
  81. startOpts := PostgresOpts{
  82. Name: "porter_postgres_" + opts.ProcessID,
  83. Image: "postgres:latest",
  84. Mounts: pgMount,
  85. VolumeMap: map[string]struct{}{
  86. "porter_postgres": {},
  87. },
  88. NetworkID: netID,
  89. Env: []string{
  90. "POSTGRES_USER=porter",
  91. "POSTGRES_PASSWORD=porter",
  92. "POSTGRES_DB=porter",
  93. },
  94. }
  95. pgID, err := agent.StartPostgresContainer(ctx, startOpts)
  96. if err != nil {
  97. return nil, "", err
  98. }
  99. err = agent.WaitForContainerHealthy(ctx, pgID, 10)
  100. if err != nil {
  101. return nil, "", err
  102. }
  103. opts.Env = append(opts.Env, []string{
  104. "SQL_LITE=false",
  105. "DB_USER=porter",
  106. "DB_PASS=porter",
  107. "DB_NAME=porter",
  108. "DB_HOST=porter_postgres_" + opts.ProcessID,
  109. "DB_PORT=5432",
  110. }...)
  111. }
  112. opts.Env = append(opts.Env, "REDIS_ENABLED=false")
  113. // create Porter container
  114. startOpts := PorterServerStartOpts{
  115. Name: "porter_server_" + opts.ProcessID,
  116. Image: "porter1/porter:" + opts.ServerImageTag,
  117. HostPort: uint(opts.ServerPort),
  118. ContainerPort: 8080,
  119. Mounts: mounts,
  120. VolumeMap: volumesMap,
  121. NetworkID: netID,
  122. Env: opts.Env,
  123. }
  124. id, err = agent.StartPorterContainer(ctx, startOpts)
  125. if err != nil {
  126. return nil, "", err
  127. }
  128. err = agent.WaitForContainerHealthy(ctx, id, 10)
  129. if err != nil {
  130. return nil, "", err
  131. }
  132. return agent, id, nil
  133. }
  134. // PorterServerStartOpts are the options for starting the Porter server
  135. type PorterServerStartOpts struct {
  136. Name string
  137. Image string
  138. StartCmd []string
  139. HostPort uint
  140. ContainerPort uint
  141. Mounts []mount.Mount
  142. VolumeMap map[string]struct{}
  143. Env []string
  144. NetworkID string
  145. }
  146. // StartPorterContainer pulls a specific Porter image and starts a container
  147. // using the Docker engine. It returns the container ID
  148. func (a *Agent) StartPorterContainer(ctx context.Context, opts PorterServerStartOpts) (string, error) {
  149. id, err := a.upsertPorterContainer(ctx, opts)
  150. if err != nil {
  151. return "", err
  152. }
  153. err = a.startPorterContainer(ctx, id)
  154. if err != nil {
  155. return "", err
  156. }
  157. // attach container to network
  158. err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
  159. if err != nil {
  160. return "", err
  161. }
  162. return id, nil
  163. }
  164. // detect if container exists and is running, and stop
  165. // if spec has changed, remove and recreate container
  166. // if container does not exist, create the container
  167. // otherwise, return stopped container
  168. func (a *Agent) upsertPorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
  169. containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
  170. // remove the matching container
  171. for _, container := range containers {
  172. if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
  173. timeout, _ := time.ParseDuration("15s")
  174. err := a.ContainerStop(ctx, container.ID, &timeout)
  175. if err != nil {
  176. return "", a.handleDockerClientErr(err, "Could not stop container "+container.ID)
  177. }
  178. err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
  179. if err != nil {
  180. return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
  181. }
  182. }
  183. }
  184. return a.pullAndCreatePorterContainer(ctx, opts)
  185. }
  186. // create the container and return its id
  187. func (a *Agent) pullAndCreatePorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
  188. _ = a.PullImage(ctx, opts.Image)
  189. // format the port array for binding to host machine
  190. ports := []string{fmt.Sprintf("127.0.0.1:%d:%d/tcp", opts.HostPort, opts.ContainerPort)}
  191. _, portBindings, err := nat.ParsePortSpecs(ports)
  192. if err != nil {
  193. return "", fmt.Errorf("Unable to parse port specification %s", ports)
  194. }
  195. labels := make(map[string]string)
  196. labels[a.label] = "true"
  197. // create the container with a label specifying this was created via the CLI
  198. resp, err := a.ContainerCreate(ctx, &container.Config{
  199. Image: opts.Image,
  200. Cmd: opts.StartCmd,
  201. Tty: false,
  202. Labels: labels,
  203. Volumes: opts.VolumeMap,
  204. Env: opts.Env,
  205. Healthcheck: &container.HealthConfig{
  206. Test: []string{"CMD-SHELL", "/porter/ready"},
  207. Interval: 10 * time.Second,
  208. Timeout: 5 * time.Second,
  209. Retries: 3,
  210. },
  211. }, &container.HostConfig{
  212. PortBindings: portBindings,
  213. Mounts: opts.Mounts,
  214. }, nil, &specs.Platform{}, opts.Name)
  215. if err != nil {
  216. return "", a.handleDockerClientErr(err, "Could not create Porter container")
  217. }
  218. return resp.ID, nil
  219. }
  220. // start the container
  221. func (a *Agent) startPorterContainer(ctx context.Context, id string) error {
  222. if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
  223. return a.handleDockerClientErr(err, "Could not start Porter container")
  224. }
  225. return nil
  226. }
  227. // PostgresOpts are the options for starting the Postgres DB
  228. type PostgresOpts struct {
  229. Name string
  230. Image string
  231. Env []string
  232. VolumeMap map[string]struct{}
  233. Mounts []mount.Mount
  234. NetworkID string
  235. }
  236. // StartPostgresContainer pulls a specific Porter image and starts a container
  237. // using the Docker engine
  238. func (a *Agent) StartPostgresContainer(ctx context.Context, opts PostgresOpts) (string, error) {
  239. id, err := a.upsertPostgresContainer(ctx, opts)
  240. if err != nil {
  241. return "", err
  242. }
  243. err = a.startPostgresContainer(ctx, id)
  244. if err != nil {
  245. return "", err
  246. }
  247. // attach container to network
  248. err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
  249. if err != nil {
  250. return "", err
  251. }
  252. return id, nil
  253. }
  254. // detect if container exists and is running, and stop
  255. // if it is running, stop it
  256. // if it is stopped, return id
  257. // if it does not exist, create it and return it
  258. func (a *Agent) upsertPostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
  259. containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
  260. // stop the matching container and return it
  261. for _, container := range containers {
  262. if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
  263. timeout, _ := time.ParseDuration("15s")
  264. err := a.ContainerStop(ctx, container.ID, &timeout)
  265. if err != nil {
  266. return "", a.handleDockerClientErr(err, "Could not stop postgres container "+container.ID)
  267. }
  268. return container.ID, nil
  269. }
  270. }
  271. return a.pullAndCreatePostgresContainer(ctx, opts)
  272. }
  273. // create the container and return it
  274. func (a *Agent) pullAndCreatePostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
  275. _ = a.PullImage(ctx, opts.Image) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  276. labels := make(map[string]string)
  277. labels[a.label] = "true"
  278. // create the container with a label specifying this was created via the CLI
  279. resp, err := a.ContainerCreate(ctx, &container.Config{
  280. Image: opts.Image,
  281. Tty: false,
  282. Labels: labels,
  283. Volumes: opts.VolumeMap,
  284. Env: opts.Env,
  285. ExposedPorts: nat.PortSet{
  286. "5432": struct{}{},
  287. },
  288. Healthcheck: &container.HealthConfig{
  289. Test: []string{"CMD-SHELL", "pg_isready"},
  290. Interval: 10 * time.Second,
  291. Timeout: 5 * time.Second,
  292. Retries: 3,
  293. },
  294. }, &container.HostConfig{
  295. Mounts: opts.Mounts,
  296. }, nil, &specs.Platform{}, opts.Name)
  297. if err != nil {
  298. return "", a.handleDockerClientErr(err, "Could not create Porter container")
  299. }
  300. return resp.ID, nil
  301. }
  302. // start the container in the background
  303. func (a *Agent) startPostgresContainer(ctx context.Context, id string) error {
  304. if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
  305. return a.handleDockerClientErr(err, "Could not start Postgres container")
  306. }
  307. return nil
  308. }
  309. // StopPorterContainers finds all containers that were started via the CLI and stops them
  310. // -- removes the container if remove is set to true
  311. func (a *Agent) StopPorterContainers(ctx context.Context, remove bool) error {
  312. containers, err := a.getContainersCreatedByStart(ctx)
  313. if err != nil {
  314. return err
  315. }
  316. // remove all Porter containers
  317. for _, container := range containers {
  318. timeout, _ := time.ParseDuration("15s")
  319. err := a.ContainerStop(ctx, container.ID, &timeout)
  320. if err != nil {
  321. return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
  322. }
  323. if remove {
  324. err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
  325. if err != nil {
  326. return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
  327. }
  328. }
  329. }
  330. return nil
  331. }
  332. // StopPorterContainersWithProcessID finds all containers that were started via the CLI
  333. // and have a given process id and stops them -- removes the container if remove is set
  334. // to true
  335. func (a *Agent) StopPorterContainersWithProcessID(ctx context.Context, processID string, remove bool) error {
  336. containers, err := a.getContainersCreatedByStart(ctx)
  337. if err != nil {
  338. return err
  339. }
  340. // remove all Porter containers
  341. for _, container := range containers {
  342. if strings.Contains(container.Names[0], "_"+processID) {
  343. timeout, _ := time.ParseDuration("15s")
  344. err := a.ContainerStop(ctx, container.ID, &timeout)
  345. if err != nil {
  346. return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
  347. }
  348. if remove {
  349. err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
  350. if err != nil {
  351. return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
  352. }
  353. }
  354. }
  355. }
  356. return nil
  357. }
  358. // getContainersCreatedByStart gets all containers that were created by the "porter start"
  359. // command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
  360. func (a *Agent) getContainersCreatedByStart(ctx context.Context) ([]types.Container, error) {
  361. containers, err := a.ContainerList(ctx, types.ContainerListOptions{
  362. All: true,
  363. })
  364. if err != nil {
  365. return nil, a.handleDockerClientErr(err, "Could not list containers")
  366. }
  367. res := make([]types.Container, 0)
  368. for _, container := range containers {
  369. if contains, ok := container.Labels[a.label]; ok && contains == "true" {
  370. res = append(res, container)
  371. }
  372. }
  373. return res, nil
  374. }