| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- package docker
- import (
- "context"
- "fmt"
- "strings"
- "time"
- "github.com/docker/docker/api/types"
- "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/api/types/mount"
- "github.com/docker/go-connections/nat"
- specs "github.com/opencontainers/image-spec/specs-go/v1"
- )
- // PorterDB is used for enumerating DB types
- type PorterDB int
- // The supported DB types
- const (
- Postgres PorterDB = iota
- SQLite
- )
- // PorterStartOpts are the options for starting the Porter stack
- type PorterStartOpts struct {
- ProcessID string
- ServerImageTag string
- ServerPort int
- DB PorterDB
- Env []string
- }
- // StartPorter creates a new Docker agent using the host environment, and creates a
- // new Porter instance
- func StartPorter(ctx context.Context, opts *PorterStartOpts) (agent *Agent, id string, err error) {
- agent, err = NewAgentFromEnv(ctx)
- if err != nil {
- return nil, "", err
- }
- // the volume mounts to use
- mounts := make([]mount.Mount, 0)
- // the volumes passed to the Porter container
- volumesMap := make(map[string]struct{})
- netID, err := agent.CreateBridgeNetworkIfNotExist(ctx, "porter_network_"+opts.ProcessID)
- if err != nil {
- return nil, "", err
- }
- switch opts.DB {
- case SQLite:
- // check if sqlite volume exists, create it if not
- vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_sqlite_"+opts.ProcessID)
- if err != nil {
- return nil, "", err
- }
- // create mount
- mount := mount.Mount{
- Type: mount.TypeVolume,
- Source: vol.Name,
- Target: "/sqlite",
- ReadOnly: false,
- Consistency: mount.ConsistencyFull,
- }
- mounts = append(mounts, mount)
- volumesMap[vol.Name] = struct{}{}
- opts.Env = append(opts.Env, []string{
- "SQL_LITE=true",
- "SQL_LITE_PATH=/sqlite/porter.db",
- }...)
- case Postgres:
- // check if postgres volume exists, create it if not
- vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_postgres_"+opts.ProcessID)
- if err != nil {
- return nil, "", err
- }
- // pgMount is mount for postgres container
- pgMount := []mount.Mount{
- {
- Type: mount.TypeVolume,
- Source: vol.Name,
- Target: "/var/lib/postgresql/data",
- ReadOnly: false,
- Consistency: mount.ConsistencyFull,
- },
- }
- // create postgres container with mount
- startOpts := PostgresOpts{
- Name: "porter_postgres_" + opts.ProcessID,
- Image: "postgres:latest",
- Mounts: pgMount,
- VolumeMap: map[string]struct{}{
- "porter_postgres": {},
- },
- NetworkID: netID,
- Env: []string{
- "POSTGRES_USER=porter",
- "POSTGRES_PASSWORD=porter",
- "POSTGRES_DB=porter",
- },
- }
- pgID, err := agent.StartPostgresContainer(ctx, startOpts)
- if err != nil {
- return nil, "", err
- }
- err = agent.WaitForContainerHealthy(ctx, pgID, 10)
- if err != nil {
- return nil, "", err
- }
- opts.Env = append(opts.Env, []string{
- "SQL_LITE=false",
- "DB_USER=porter",
- "DB_PASS=porter",
- "DB_NAME=porter",
- "DB_HOST=porter_postgres_" + opts.ProcessID,
- "DB_PORT=5432",
- }...)
- }
- opts.Env = append(opts.Env, "REDIS_ENABLED=false")
- // create Porter container
- startOpts := PorterServerStartOpts{
- Name: "porter_server_" + opts.ProcessID,
- Image: "porter1/porter:" + opts.ServerImageTag,
- HostPort: uint(opts.ServerPort),
- ContainerPort: 8080,
- Mounts: mounts,
- VolumeMap: volumesMap,
- NetworkID: netID,
- Env: opts.Env,
- }
- id, err = agent.StartPorterContainer(ctx, startOpts)
- if err != nil {
- return nil, "", err
- }
- err = agent.WaitForContainerHealthy(ctx, id, 10)
- if err != nil {
- return nil, "", err
- }
- return agent, id, nil
- }
- // PorterServerStartOpts are the options for starting the Porter server
- type PorterServerStartOpts struct {
- Name string
- Image string
- StartCmd []string
- HostPort uint
- ContainerPort uint
- Mounts []mount.Mount
- VolumeMap map[string]struct{}
- Env []string
- NetworkID string
- }
- // StartPorterContainer pulls a specific Porter image and starts a container
- // using the Docker engine. It returns the container ID
- func (a *Agent) StartPorterContainer(ctx context.Context, opts PorterServerStartOpts) (string, error) {
- id, err := a.upsertPorterContainer(ctx, opts)
- if err != nil {
- return "", err
- }
- err = a.startPorterContainer(ctx, id)
- if err != nil {
- return "", err
- }
- // attach container to network
- err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
- if err != nil {
- return "", err
- }
- return id, nil
- }
- // detect if container exists and is running, and stop
- // if spec has changed, remove and recreate container
- // if container does not exist, create the container
- // otherwise, return stopped container
- func (a *Agent) upsertPorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
- containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
- // remove the matching container
- for _, container := range containers {
- if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
- timeout, _ := time.ParseDuration("15s")
- err := a.ContainerStop(ctx, container.ID, &timeout)
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not stop container "+container.ID)
- }
- err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
- }
- }
- }
- return a.pullAndCreatePorterContainer(ctx, opts)
- }
- // create the container and return its id
- func (a *Agent) pullAndCreatePorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
- _ = a.PullImage(ctx, opts.Image)
- // format the port array for binding to host machine
- ports := []string{fmt.Sprintf("127.0.0.1:%d:%d/tcp", opts.HostPort, opts.ContainerPort)}
- _, portBindings, err := nat.ParsePortSpecs(ports)
- if err != nil {
- return "", fmt.Errorf("Unable to parse port specification %s", ports)
- }
- labels := make(map[string]string)
- labels[a.label] = "true"
- // create the container with a label specifying this was created via the CLI
- resp, err := a.ContainerCreate(ctx, &container.Config{
- Image: opts.Image,
- Cmd: opts.StartCmd,
- Tty: false,
- Labels: labels,
- Volumes: opts.VolumeMap,
- Env: opts.Env,
- Healthcheck: &container.HealthConfig{
- Test: []string{"CMD-SHELL", "/porter/ready"},
- Interval: 10 * time.Second,
- Timeout: 5 * time.Second,
- Retries: 3,
- },
- }, &container.HostConfig{
- PortBindings: portBindings,
- Mounts: opts.Mounts,
- }, nil, &specs.Platform{}, opts.Name)
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not create Porter container")
- }
- return resp.ID, nil
- }
- // start the container
- func (a *Agent) startPorterContainer(ctx context.Context, id string) error {
- if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
- return a.handleDockerClientErr(err, "Could not start Porter container")
- }
- return nil
- }
- // PostgresOpts are the options for starting the Postgres DB
- type PostgresOpts struct {
- Name string
- Image string
- Env []string
- VolumeMap map[string]struct{}
- Mounts []mount.Mount
- NetworkID string
- }
- // StartPostgresContainer pulls a specific Porter image and starts a container
- // using the Docker engine
- func (a *Agent) StartPostgresContainer(ctx context.Context, opts PostgresOpts) (string, error) {
- id, err := a.upsertPostgresContainer(ctx, opts)
- if err != nil {
- return "", err
- }
- err = a.startPostgresContainer(ctx, id)
- if err != nil {
- return "", err
- }
- // attach container to network
- err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
- if err != nil {
- return "", err
- }
- return id, nil
- }
- // detect if container exists and is running, and stop
- // if it is running, stop it
- // if it is stopped, return id
- // if it does not exist, create it and return it
- func (a *Agent) upsertPostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
- containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
- // stop the matching container and return it
- for _, container := range containers {
- if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
- timeout, _ := time.ParseDuration("15s")
- err := a.ContainerStop(ctx, container.ID, &timeout)
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not stop postgres container "+container.ID)
- }
- return container.ID, nil
- }
- }
- return a.pullAndCreatePostgresContainer(ctx, opts)
- }
- // create the container and return it
- func (a *Agent) pullAndCreatePostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
- _ = a.PullImage(ctx, opts.Image) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
- labels := make(map[string]string)
- labels[a.label] = "true"
- // create the container with a label specifying this was created via the CLI
- resp, err := a.ContainerCreate(ctx, &container.Config{
- Image: opts.Image,
- Tty: false,
- Labels: labels,
- Volumes: opts.VolumeMap,
- Env: opts.Env,
- ExposedPorts: nat.PortSet{
- "5432": struct{}{},
- },
- Healthcheck: &container.HealthConfig{
- Test: []string{"CMD-SHELL", "pg_isready"},
- Interval: 10 * time.Second,
- Timeout: 5 * time.Second,
- Retries: 3,
- },
- }, &container.HostConfig{
- Mounts: opts.Mounts,
- }, nil, &specs.Platform{}, opts.Name)
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not create Porter container")
- }
- return resp.ID, nil
- }
- // start the container in the background
- func (a *Agent) startPostgresContainer(ctx context.Context, id string) error {
- if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
- return a.handleDockerClientErr(err, "Could not start Postgres container")
- }
- return nil
- }
- // StopPorterContainers finds all containers that were started via the CLI and stops them
- // -- removes the container if remove is set to true
- func (a *Agent) StopPorterContainers(ctx context.Context, remove bool) error {
- containers, err := a.getContainersCreatedByStart(ctx)
- if err != nil {
- return err
- }
- // remove all Porter containers
- for _, container := range containers {
- timeout, _ := time.ParseDuration("15s")
- err := a.ContainerStop(ctx, container.ID, &timeout)
- if err != nil {
- return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
- }
- if remove {
- err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
- if err != nil {
- return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
- }
- }
- }
- return nil
- }
- // StopPorterContainersWithProcessID finds all containers that were started via the CLI
- // and have a given process id and stops them -- removes the container if remove is set
- // to true
- func (a *Agent) StopPorterContainersWithProcessID(ctx context.Context, processID string, remove bool) error {
- containers, err := a.getContainersCreatedByStart(ctx)
- if err != nil {
- return err
- }
- // remove all Porter containers
- for _, container := range containers {
- if strings.Contains(container.Names[0], "_"+processID) {
- timeout, _ := time.ParseDuration("15s")
- err := a.ContainerStop(ctx, container.ID, &timeout)
- if err != nil {
- return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
- }
- if remove {
- err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
- if err != nil {
- return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
- }
- }
- }
- }
- return nil
- }
- // getContainersCreatedByStart gets all containers that were created by the "porter start"
- // command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
- func (a *Agent) getContainersCreatedByStart(ctx context.Context) ([]types.Container, error) {
- containers, err := a.ContainerList(ctx, types.ContainerListOptions{
- All: true,
- })
- if err != nil {
- return nil, a.handleDockerClientErr(err, "Could not list containers")
- }
- res := make([]types.Container, 0)
- for _, container := range containers {
- if contains, ok := container.Labels[a.label]; ok && contains == "true" {
- res = append(res, container)
- }
- }
- return res, nil
- }
|