Browse Source

init on startup

Alexander Belanger 5 years ago
parent
commit
3ca584ab73
10 changed files with 407 additions and 48 deletions
  1. 121 1
      cli/cmd/docker/agent.go
  2. 99 26
      cli/cmd/docker/porter.go
  3. 71 10
      cli/cmd/start.go
  4. 2 0
      cli/main.go
  5. 77 0
      cmd/app/main.go
  6. 1 1
      docker/.env
  7. 2 1
      docker/Dockerfile
  8. 25 1
      internal/adapter/gorm.go
  9. 5 1
      internal/config/config.go
  10. 4 7
      internal/forms/user.go

+ 121 - 1
cli/cmd/docker/agent.go

@@ -2,11 +2,15 @@ package docker
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"io"
 	"strings"
 
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/volume"
 	"github.com/docker/docker/client"
 )
@@ -60,7 +64,123 @@ func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
 		return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
 	}
 
-	return &vol, err
+	return &vol, nil
+}
+
+// CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
+// given name if it does not exist. If the volume does exist but does not contain
+// the required label (a.label), an error is thrown.
+func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error) {
+	networks, err := a.client.NetworkList(a.ctx, types.NetworkListOptions{})
+
+	if err != nil {
+		return "", a.handleDockerClientErr(err, "Could not list volumes")
+	}
+
+	for _, net := range networks {
+		if contains, ok := net.Labels[a.label]; ok && contains == "true" && net.Name == name {
+			return net.ID, nil
+		} else if !ok && net.Name == name {
+			return "", fmt.Errorf("network conflict for %s: please remove existing network and try again", name)
+		}
+	}
+
+	return a.CreateBridgeNetwork(name)
+}
+
+// CreateBridgeNetwork creates a volume using the default driver type (bridge)
+// with the CLI label attached
+func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
+	labels := make(map[string]string)
+	labels[a.label] = "true"
+
+	opts := types.NetworkCreate{
+		Labels:     labels,
+		Attachable: true,
+	}
+
+	net, err := a.client.NetworkCreate(a.ctx, name, opts)
+
+	if err != nil {
+		return "", a.handleDockerClientErr(err, "Could not create network "+name)
+	}
+
+	return net.ID, nil
+}
+
+// ConnectContainerToNetwork attaches a container to a specified network
+func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName string) error {
+	// check if the container is connected already
+	net, err := a.client.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
+
+	if err != nil {
+		return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
+	}
+
+	for _, cont := range net.Containers {
+		// if container is connected, just return
+		if cont.Name == containerName {
+			return nil
+		}
+	}
+
+	return a.client.NetworkConnect(a.ctx, networkID, containerID, &network.EndpointSettings{})
+}
+
+// PullImageEvent represents a response from the Docker API with an image pull event
+type PullImageEvent struct {
+	Status         string `json:"status"`
+	Error          string `json:"error"`
+	Progress       string `json:"progress"`
+	ProgressDetail struct {
+		Current int `json:"current"`
+		Total   int `json:"total"`
+	} `json:"progressDetail"`
+}
+
+// PullImage pulls an image specified by the image string
+func (a *Agent) PullImage(image string) error {
+	fmt.Println("Pulling image:", image)
+
+	// pull the specified image
+	out, err := a.client.ImagePull(a.ctx, image, types.ImagePullOptions{})
+
+	if err != nil {
+		return a.handleDockerClientErr(err, "Could not pull image"+image)
+	}
+
+	decoder := json.NewDecoder(out)
+
+	var event *PullImageEvent
+
+	for {
+		if err := decoder.Decode(&event); err != nil {
+			if err == io.EOF {
+				break
+			}
+
+			return err
+		}
+	}
+
+	fmt.Println("Finished pulling image:", image)
+
+	return nil
+}
+
+func (a *Agent) WaitForContainerStop(id string) error {
+	// wait for container to stop before exit
+	statusCh, errCh := a.client.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
+
+	select {
+	case err := <-errCh:
+		if err != nil {
+			return a.handleDockerClientErr(err, "Error waiting for stopped container")
+		}
+	case <-statusCh:
+	}
+
+	return nil
 }
 
 // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //

+ 99 - 26
cli/cmd/docker/porter.go

@@ -20,35 +20,32 @@ type PorterStartOpts struct {
 	Mounts        []mount.Mount
 	VolumeMap     map[string]struct{}
 	Env           []string
+	NetworkID     string
 }
 
-// StartPorterContainerAndWait pulls a specific Porter image and starts a container
-// using the Docker engine. It returns when the container has stopped.
-func (a *Agent) StartPorterContainerAndWait(opts PorterStartOpts) error {
+// StartPorterContainer pulls a specific Porter image and starts a container
+// using the Docker engine. It returns the container ID
+func (a *Agent) StartPorterContainer(opts PorterStartOpts) (string, error) {
 	id, err := a.upsertPorterContainer(opts)
 
 	if err != nil {
-		return err
+		return "", err
 	}
 
 	err = a.startPorterContainer(id)
 
 	if err != nil {
-		return err
+		return "", err
 	}
 
-	// wait for container to stop before exit
-	statusCh, errCh := a.client.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
+	// attach container to network
+	err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
 
-	select {
-	case err := <-errCh:
-		if err != nil {
-			return a.handleDockerClientErr(err, "Error waiting for stopped container")
-		}
-	case <-statusCh:
+	if err != nil {
+		return "", err
 	}
 
-	return nil
+	return id, nil
 }
 
 // detect if container exists and is running, and stop
@@ -82,12 +79,7 @@ func (a *Agent) upsertPorterContainer(opts PorterStartOpts) (id string, err erro
 
 // create the container and return its id
 func (a *Agent) pullAndCreatePorterContainer(opts PorterStartOpts) (id string, err error) {
-	// pull the specified image
-	_, err = a.client.ImagePull(a.ctx, opts.Image, types.ImagePullOptions{})
-
-	if err != nil {
-		return "", a.handleDockerClientErr(err, "Could not pull Porter image")
-	}
+	a.PullImage(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)}
@@ -130,27 +122,108 @@ func (a *Agent) startPorterContainer(id string) error {
 	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(opts PostgresOpts) (string, error) {
+	id, err := a.upsertPostgresContainer(opts)
+
+	if err != nil {
+		return "", err
+	}
+
+	err = a.startPostgresContainer(id)
+
+	if err != nil {
+		return "", err
+	}
+
+	// attach container to network
+	err = a.ConnectContainerToNetwork(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() error {
+func (a *Agent) upsertPostgresContainer(opts PostgresOpts) (id string, err error) {
+	containers, err := a.getContainersCreatedByStart()
+
+	// 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.client.ContainerStop(a.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(opts)
+}
 
 // create the container and return it
-// func (a *Agent) createPostgresContainer() error {
+func (a *Agent) pullAndCreatePostgresContainer(opts PostgresOpts) (id string, err error) {
+	a.PullImage(opts.Image)
 
-// }
+	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.client.ContainerCreate(a.ctx, &container.Config{
+		Image:   opts.Image,
+		Tty:     false,
+		Labels:  labels,
+		Volumes: opts.VolumeMap,
+		Env:     opts.Env,
+		ExposedPorts: nat.PortSet{
+			"5432": struct{}{},
+		},
+	}, &container.HostConfig{
+		Mounts: opts.Mounts,
+	}, nil, 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() error {
+func (a *Agent) startPostgresContainer(id string) error {
+	if err := a.client.ContainerStart(a.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
 // without removal.
 func (a *Agent) StopPorterContainers() error {
+	fmt.Println("Stopping containers...")
+
 	containers, err := a.getContainersCreatedByStart()
 
 	if err != nil {

+ 71 - 10
cli/cmd/start.go

@@ -21,7 +21,7 @@ type startOps struct {
 	kubeconfigPath string
 	contexts       *[]string
 	imageTag       string `form:"required"`
-	db             string `form:"oneof=sqlite memory postgres"`
+	db             string `form:"oneof=sqlite postgres"`
 }
 
 var opts = &startOps{}
@@ -47,13 +47,20 @@ var startCmd = &cobra.Command{
 
 		if err != nil {
 			fmt.Println("Error running start:", err.Error())
+			fmt.Println("Shutting down...")
+
+			err = stop()
+
+			if err != nil {
+				fmt.Println("Shutdown unsuccessful:", err.Error())
+			}
+
 			os.Exit(1)
 		}
 	},
 }
 
 func init() {
-	// closeHandler(stop)
 	rootCmd.AddCommand(startCmd)
 
 	opts.insecure = startCmd.PersistentFlags().Bool(
@@ -78,7 +85,7 @@ func init() {
 		&opts.db,
 		"db",
 		"sqlite",
-		"the db to use",
+		"the db to use, one of sqlite or postgres",
 	)
 
 	startCmd.PersistentFlags().StringVar(
@@ -181,6 +188,20 @@ func start(
 		mounts = append(mounts, mount)
 	}
 
+	netID, err := agent.CreateBridgeNetworkIfNotExist("porter_network")
+
+	if err != nil {
+		return err
+	}
+
+	env := make([]string, 0)
+
+	env = append(env, []string{
+		"ADMIN_INIT=true",
+		"ADMIN_EMAIL=" + username,
+		"ADMIN_PASSWORD=" + pw,
+	}...)
+
 	switch db {
 	case "sqlite":
 		// check if sqlite volume exists, create it if not
@@ -201,6 +222,11 @@ func start(
 
 		mounts = append(mounts, mount)
 		volumesMap[vol.Name] = struct{}{}
+
+		env = append(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("porter_postgres")
@@ -221,8 +247,37 @@ func start(
 		}
 
 		// create postgres container with mount
-		// TODO
-		fmt.Println(pgMount)
+		startOpts := docker.PostgresOpts{
+			Name:   "porter_postgres",
+			Image:  "postgres:latest",
+			Mounts: pgMount,
+			VolumeMap: map[string]struct{}{
+				"porter_postgres": struct{}{},
+			},
+			NetworkID: netID,
+			Env: []string{
+				"POSTGRES_USER=porter",
+				"POSTGRES_PASSWORD=porter",
+				"POSTGRES_DB=porter",
+			},
+		}
+
+		pgID, err := agent.StartPostgresContainer(startOpts)
+
+		if err != nil {
+			return err
+		}
+
+		env = append(env, []string{
+			"SQL_LITE=false",
+			"DB_USER=porter",
+			"DB_PASS=porter",
+			"DB_NAME=porter",
+			"DB_HOST=porter_postgres",
+			"DB_PORT=5432",
+		}...)
+
+		defer agent.WaitForContainerStop(pgID)
 	}
 
 	// create Porter container
@@ -234,11 +289,17 @@ func start(
 		ContainerPort: 8080,
 		Mounts:        mounts,
 		VolumeMap:     volumesMap,
-		Env: []string{
-			"QUICK_START=true",
-			"SQL_LITE_PATH=/sqlite/porter.db",
-		},
+		NetworkID:     netID,
+		Env:           env,
 	}
 
-	return agent.StartPorterContainerAndWait(startOpts)
+	id, err := agent.StartPorterContainer(startOpts)
+
+	if err != nil {
+		return err
+	}
+
+	agent.WaitForContainerStop(id)
+
+	return nil
 }

+ 2 - 0
cli/main.go

@@ -1,3 +1,5 @@
+// +build cli
+
 package main
 
 import (

+ 77 - 0
cmd/app/main.go

@@ -2,10 +2,16 @@ package main
 
 import (
 	"fmt"
+	"io/ioutil"
 	"log"
 	"net/http"
+	"os"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
 
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 
 	"github.com/porter-dev/porter/server/api"
@@ -31,6 +37,15 @@ func main() {
 
 	repo := gorm.NewRepository(db)
 
+	// upsert admin if config requires
+	if appConf.Db.AdminInit {
+		err := upsertAdmin(repo.User, appConf.Db.AdminEmail, appConf.Db.AdminPassword)
+
+		if err != nil {
+			fmt.Println("Error while upserting admin: " + err.Error())
+		}
+	}
+
 	// declare as Store interface (methods Get, New, Save)
 	var store sessions.Store
 	store, _ = sessionstore.NewStore(repo, appConf.Server)
@@ -57,3 +72,65 @@ func main() {
 		log.Fatal("Server startup failed")
 	}
 }
+
+func upsertAdmin(repo repository.UserRepository, email, pw string) error {
+	admUser, err := repo.ReadUserByEmail(email)
+
+	// create the user in this case
+	if err != nil {
+		form := forms.CreateUserForm{
+			Email:    email,
+			Password: pw,
+		}
+
+		admUser, err = form.ToUser(repo)
+
+		if err != nil {
+			return err
+		}
+
+		admUser, err = repo.CreateUser(admUser)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	filename := "/porter/porter.kubeconfig"
+
+	// read if kubeconfig file exists, if it does update the user
+	if _, err := os.Stat(filename); !os.IsNotExist(err) {
+		fileBytes, err := ioutil.ReadFile(filename)
+
+		contexts := make([]string, 0)
+		allContexts, err := kubernetes.GetContextsFromBytes(fileBytes, []string{})
+
+		if err != nil {
+			return err
+		}
+
+		for _, context := range allContexts {
+			contexts = append(contexts, context.Name)
+		}
+
+		form := forms.UpdateUserForm{
+			ID:              admUser.ID,
+			RawKubeConfig:   string(fileBytes),
+			AllowedContexts: contexts,
+		}
+
+		admUser, err = form.ToUser(repo)
+
+		if err != nil {
+			return err
+		}
+
+		admUser, err = repo.UpdateUser(admUser)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 1 - 1
docker/.env

@@ -14,4 +14,4 @@ DB_PASS=porter
 DB_NAME=porter
 COOKIE_SECRETS=secret
 
-QUICK_START=false
+SQL_LITE=false

+ 2 - 1
docker/Dockerfile

@@ -63,7 +63,8 @@ ENV SERVER_TIMEOUT_IDLE=15s
 
 ENV COOKIE_SECRETS=secret
 
-ENV QUICK_START=true
+ENV SQL_LITE=true
+ENV ADMIN_INIT=false
 
 EXPOSE 8080
 CMD /porter/migrate && /porter/app

+ 25 - 1
internal/adapter/gorm.go

@@ -2,6 +2,7 @@ package gorm
 
 import (
 	"fmt"
+	"time"
 
 	"github.com/porter-dev/porter/internal/config"
 	"gorm.io/driver/postgres"
@@ -23,5 +24,28 @@ func New(conf *config.DBConf) (*gorm.DB, error) {
 		conf.Host,
 	)
 
-	return gorm.Open(postgres.Open(dsn), &gorm.Config{})
+	res, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
+
+	// retry the connection 3 times
+	retryCount := 0
+	timeout, _ := time.ParseDuration("5s")
+
+	if err != nil {
+		for {
+			time.Sleep(timeout)
+			res, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
+
+			if retryCount > 3 {
+				return nil, err
+			}
+
+			if err == nil {
+				return res, nil
+			}
+
+			retryCount++
+		}
+	}
+
+	return res, err
 }

+ 5 - 1
internal/config/config.go

@@ -35,7 +35,11 @@ type DBConf struct {
 	Password string `env:"DB_PASS,default=porter"`
 	DbName   string `env:"DB_NAME,default=porter"`
 
-	SQLLite     bool   `env:"QUICK_START,default=false"`
+	AdminInit     bool   `env:"ADMIN_INIT,default=true"`
+	AdminEmail    string `env:"ADMIN_EMAIL,default=admin@example.com"`
+	AdminPassword string `env:"ADMIN_PASSWORD,default=password"`
+
+	SQLLite     bool   `env:"SQL_LITE,default=false"`
 	SQLLitePath string `env:"SQL_LITE_PATH,default=/porter/porter.db"`
 }
 

+ 4 - 7
internal/forms/user.go

@@ -110,13 +110,10 @@ func (uuf *UpdateUserForm) ToUser(repo repository.UserRepository) (*models.User,
 
 	contextsJoin := strings.Join(contexts, ",")
 
-	return &models.User{
-		Model: gorm.Model{
-			ID: uuf.ID,
-		},
-		Contexts:      contextsJoin,
-		RawKubeConfig: rawBytes,
-	}, nil
+	savedUser.Contexts = contextsJoin
+	savedUser.RawKubeConfig = rawBytes
+
+	return savedUser, nil
 }
 
 // DeleteUserForm represents the accepted values for deleting a user