瀏覽代碼

start barely usable

Alexander Belanger 5 年之前
父節點
當前提交
f26a8df214
共有 5 個文件被更改,包括 376 次插入63 次删除
  1. 47 0
      cmd/cli/cmd/docker/agent.go
  2. 103 34
      cmd/cli/cmd/docker/porter.go
  3. 9 7
      cmd/cli/cmd/generate.go
  4. 216 21
      cmd/cli/cmd/start.go
  5. 1 1
      go.mod

+ 47 - 0
cmd/cli/cmd/docker/agent.go

@@ -5,6 +5,9 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/volume"
 	"github.com/docker/docker/client"
 )
 
@@ -16,6 +19,50 @@ type Agent struct {
 	label  string
 }
 
+// CreateLocalVolumeIfNotExist 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) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error) {
+	volListBody, err := a.client.VolumeList(a.ctx, filters.Args{})
+
+	if err != nil {
+		return nil, a.handleDockerClientErr(err, "Could not list volumes")
+	}
+
+	for _, _vol := range volListBody.Volumes {
+		if contains, ok := _vol.Labels[a.label]; ok && contains == "true" && _vol.Name == name {
+			return _vol, nil
+		} else if !ok && _vol.Name == name {
+			return nil, fmt.Errorf("volume conflict for %s: please remove existing volume and try again", name)
+		}
+	}
+
+	return a.CreateLocalVolume(name)
+}
+
+// CreateLocalVolume creates a volume using driver type "local" with no
+// configured options. The equivalent of:
+//
+// docker volume create --driver local [name]
+func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
+	labels := make(map[string]string)
+	labels[a.label] = "true"
+
+	opts := volume.VolumeCreateBody{
+		Name:   name,
+		Driver: "local",
+		Labels: labels,
+	}
+
+	vol, err := a.client.VolumeCreate(a.ctx, opts)
+
+	if err != nil {
+		return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
+	}
+
+	return &vol, err
+}
+
 // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //
 
 func (a *Agent) handleDockerClientErr(err error, errPrefix string) error {

+ 103 - 34
cmd/cli/cmd/docker/porter.go

@@ -1,32 +1,92 @@
 package docker
 
 import (
-	"context"
 	"fmt"
 	"time"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/client"
+	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/go-connections/nat"
 )
 
 // PorterStartOpts are the options for starting the Porter container
 type PorterStartOpts struct {
+	Name          string
 	Image         string
 	StartCmd      []string
 	HostPort      uint
 	ContainerPort uint
+	Mounts        []mount.Mount
+	VolumeMap     map[string]struct{}
+	Env           []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 {
+	id, err := a.upsertPorterContainer(opts)
+
+	if err != nil {
+		return err
+	}
+
+	err = a.startPorterContainer(id)
+
+	if err != nil {
+		return err
+	}
+
+	// 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
+}
+
+// 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(opts PorterStartOpts) (id string, err error) {
+	containers, err := a.getContainersCreatedByStart()
+
+	// remove the matching container
+	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 container "+container.ID)
+			}
+
+			err = a.client.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+
+			if err != nil {
+				return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
+			}
+		}
+	}
+
+	return a.pullAndCreatePorterContainer(opts)
+}
+
+// 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{})
+	_, err = a.client.ImagePull(a.ctx, opts.Image, types.ImagePullOptions{})
 
 	if err != nil {
-		return a.handleDockerClientErr(err, "Could not pull Porter image")
+		return "", a.handleDockerClientErr(err, "Could not pull Porter image")
 	}
 
 	// format the port array for binding to host machine
@@ -35,52 +95,62 @@ func (a *Agent) StartPorterContainerAndWait(opts PorterStartOpts) error {
 	_, portBindings, err := nat.ParsePortSpecs(ports)
 
 	if err != nil {
-		return fmt.Errorf("Unable to parse port specification %s", ports)
+		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.client.ContainerCreate(a.ctx, &container.Config{
-		Image: opts.Image,
-		Cmd:   opts.StartCmd,
-		Tty:   false,
-		Labels: map[string]string{
-			"CreatedByPorterCLI": "true",
-		},
+		Image:   opts.Image,
+		Cmd:     opts.StartCmd,
+		Tty:     false,
+		Labels:  labels,
+		Volumes: opts.VolumeMap,
+		Env:     opts.Env,
 	}, &container.HostConfig{
 		PortBindings: portBindings,
-	}, nil, "")
+		Mounts:       opts.Mounts,
+	}, nil, opts.Name)
 
 	if err != nil {
-		return a.handleDockerClientErr(err, "Could not create Porter container")
+		return "", a.handleDockerClientErr(err, "Could not create Porter container")
 	}
 
-	// start the container and listen until the container is stopped
-	if err := a.client.ContainerStart(a.ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
-		return a.handleDockerClientErr(err, "Could not start Porter container")
-	}
+	return resp.ID, nil
+}
 
-	statusCh, errCh := a.client.ContainerWait(a.ctx, resp.ID, container.WaitConditionNotRunning)
-	select {
-	case err := <-errCh:
-		if err != nil {
-			return a.handleDockerClientErr(err, "Error waiting for stopped container")
-		}
-	case <-statusCh:
+// start the container
+func (a *Agent) startPorterContainer(id string) error {
+	if err := a.client.ContainerStart(a.ctx, id, types.ContainerStartOptions{}); err != nil {
+		return a.handleDockerClientErr(err, "Could not start Porter container")
 	}
 
 	return 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 {
+
+// }
+
+// create the container and return it
+// func (a *Agent) createPostgresContainer() error {
+
+// }
+
+// start the container in the background
+// func (a *Agent) startPostgresContainer() error {
+
+// }
+
 // StopPorterContainers finds all containers that were started via the CLI and stops them
 // without removal.
 func (a *Agent) StopPorterContainers() error {
-	ctx := context.Background()
-	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
-
-	if err != nil {
-		panic(err)
-	}
-
 	containers, err := a.getContainersCreatedByStart()
 
 	if err != nil {
@@ -89,10 +159,9 @@ func (a *Agent) StopPorterContainers() error {
 
 	// remove all Porter containers
 	for _, container := range containers {
-		fmt.Println("removing container", container.ID)
 		timeout, _ := time.ParseDuration("15s")
 
-		err := cli.ContainerStop(ctx, container.ID, &timeout)
+		err := a.client.ContainerStop(a.ctx, container.ID, &timeout)
 
 		if err != nil {
 			return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
@@ -103,7 +172,7 @@ func (a *Agent) StopPorterContainers() error {
 }
 
 // getContainersCreatedByStart gets all containers that were created by the "porter start"
-// command by looking for the label "CreatedByPorterCLI"
+// command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
 func (a *Agent) getContainersCreatedByStart() ([]types.Container, error) {
 	containers, err := a.client.ContainerList(a.ctx, types.ContainerListOptions{
 		All: true,

+ 9 - 7
cmd/cli/cmd/generate.go

@@ -58,7 +58,7 @@ func init() {
 		"print",
 		"p",
 		false,
-		"print result to stdout",
+		"print result to stdout, without writing to the fs",
 	)
 }
 
@@ -75,12 +75,6 @@ func generate(kubeconfigPath string, output string, print bool, contexts []strin
 		return err
 	}
 
-	err = clientcmd.WriteToFile(rawConf, output)
-
-	if err != nil {
-		return err
-	}
-
 	if print {
 		bytes, err := clientcmd.Write(rawConf)
 
@@ -89,6 +83,14 @@ func generate(kubeconfigPath string, output string, print bool, contexts []strin
 		}
 
 		fmt.Printf(string(bytes))
+
+		return nil
+	}
+
+	err = clientcmd.WriteToFile(rawConf, output)
+
+	if err != nil {
+		return err
 	}
 
 	return nil

+ 216 - 21
cmd/cli/cmd/start.go

@@ -2,12 +2,30 @@ package cmd
 
 import (
 	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/porter-dev/porter/cmd/cli/cmd/docker"
+	"k8s.io/client-go/util/homedir"
 
 	"github.com/porter-dev/porter/cmd/cli/cmd/credstore"
 
 	"github.com/spf13/cobra"
+
+	"github.com/docker/docker/api/types/mount"
 )
 
+type startOps struct {
+	insecure       *bool
+	skipKubeconfig *bool
+	kubeconfigPath string
+	contexts       *[]string
+	imageTag       string `form:"required"`
+	db             string `form:"oneof=sqlite memory postgres"`
+}
+
+var opts = &startOps{}
+
 // startCmd represents the start command
 var startCmd = &cobra.Command{
 	Args: func(cmd *cobra.Command, args []string) error {
@@ -16,34 +34,211 @@ var startCmd = &cobra.Command{
 	Use:   "start",
 	Short: "Starts a Porter instance using the Docker engine.",
 	Run: func(cmd *cobra.Command, args []string) {
-		var username, pw string
-		var err error
-
-		// if not insecure, or username/pw set incorrectly, prompt for new username/pw
-		if username, pw, err = credstore.Get(); !cmd.Flag("insecure").Changed && err != nil {
-			username, err = promptPlaintext("Email: ")
-
-			if err != nil {
-				fmt.Println(err.Error())
-				return
-			}
+		closeHandler(stop)
 
-			pw, err = promptPasswordWithConfirmation()
+		err := start(
+			opts.imageTag,
+			opts.kubeconfigPath,
+			opts.db,
+			*opts.contexts,
+			*opts.insecure,
+			*opts.skipKubeconfig,
+		)
 
-			if err != nil {
-				fmt.Println(err.Error())
-				return
-			}
-
-			credstore.Set(username, pw)
+		if err != nil {
+			fmt.Println("Error running start:", err.Error())
+			os.Exit(1)
 		}
-
-		// start()
 	},
 }
 
 func init() {
 	// closeHandler(stop)
 	rootCmd.AddCommand(startCmd)
-	startCmd.PersistentFlags().Bool("insecure", false, "skip admin setup and authorization")
+
+	opts.insecure = startCmd.PersistentFlags().Bool(
+		"insecure",
+		false,
+		"skip admin setup and authorization",
+	)
+
+	opts.skipKubeconfig = startCmd.PersistentFlags().Bool(
+		"skip-kubeconfig",
+		false,
+		"skip initialization of the kubeconfig",
+	)
+
+	opts.contexts = startCmd.PersistentFlags().StringArray(
+		"contexts",
+		nil,
+		"the list of contexts to use (defaults to the current context)",
+	)
+
+	startCmd.PersistentFlags().StringVar(
+		&opts.db,
+		"db",
+		"sqlite",
+		"the db to use",
+	)
+
+	startCmd.PersistentFlags().StringVar(
+		&opts.kubeconfigPath,
+		"kubeconfig",
+		"",
+		"path to kubeconfig",
+	)
+
+	startCmd.PersistentFlags().StringVar(
+		&opts.imageTag,
+		"image-tag",
+		"latest",
+		"the Porter image tag to use",
+	)
+}
+
+func stop() error {
+	agent, err := docker.NewAgentFromEnv()
+
+	if err != nil {
+		return err
+	}
+
+	err = agent.StopPorterContainers()
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func start(
+	imageTag string,
+	kubeconfigPath string,
+	db string,
+	contexts []string,
+	insecure bool,
+	skipKubeconfig bool,
+) error {
+	var username, pw string
+	var err error
+	home := homedir.HomeDir()
+	outputConfPath := filepath.Join(home, ".porter", "porter.kubeconfig")
+	containerConfPath := "/porter/porter.kubeconfig"
+
+	// if not insecure, or username/pw set incorrectly, prompt for new username/pw
+	if username, pw, err = credstore.Get(); !insecure && err != nil {
+		username, err = promptPlaintext("Email: ")
+
+		if err != nil {
+			return err
+		}
+
+		pw, err = promptPasswordWithConfirmation()
+
+		if err != nil {
+			return err
+		}
+
+		credstore.Set(username, pw)
+	}
+
+	if !skipKubeconfig {
+		err = generate(
+			kubeconfigPath,
+			outputConfPath,
+			false,
+			contexts,
+		)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	agent, err := docker.NewAgentFromEnv()
+
+	if err != nil {
+		return err
+	}
+
+	// the volume mounts to use
+	mounts := make([]mount.Mount, 0)
+
+	// the volumes passed to the Porter container
+	volumesMap := make(map[string]struct{})
+
+	if !skipKubeconfig {
+		// add a bind mount with the kubeconfig
+		mount := mount.Mount{
+			Type:        mount.TypeBind,
+			Source:      outputConfPath,
+			Target:      containerConfPath,
+			ReadOnly:    true,
+			Consistency: mount.ConsistencyFull,
+		}
+
+		mounts = append(mounts, mount)
+	}
+
+	switch db {
+	case "sqlite":
+		// check if sqlite volume exists, create it if not
+		vol, err := agent.CreateLocalVolumeIfNotExist("porter_sqlite")
+
+		if err != nil {
+			return 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{}{}
+	case "postgres":
+		// check if postgres volume exists, create it if not
+		vol, err := agent.CreateLocalVolumeIfNotExist("porter_postgres")
+
+		if err != nil {
+			return err
+		}
+
+		// pgMount is mount for postgres container
+		pgMount := []mount.Mount{
+			mount.Mount{
+				Type:        mount.TypeVolume,
+				Source:      vol.Name,
+				Target:      "/var/lib/postgresql/data",
+				ReadOnly:    false,
+				Consistency: mount.ConsistencyFull,
+			},
+		}
+
+		// create postgres container with mount
+		// TODO
+		fmt.Println(pgMount)
+	}
+
+	// create Porter container
+	// TODO -- look for unused port
+	startOpts := docker.PorterStartOpts{
+		Name:          "porter_server",
+		Image:         "porter1/porter:" + imageTag,
+		HostPort:      8080,
+		ContainerPort: 8080,
+		Mounts:        mounts,
+		VolumeMap:     volumesMap,
+		Env: []string{
+			"QUICK_START=true",
+			"SQL_LITE_PATH=/sqlite/porter.db",
+		},
+	}
+
+	return agent.StartPorterContainerAndWait(startOpts)
 }

+ 1 - 1
go.mod

@@ -6,7 +6,7 @@ require (
 	github.com/Azure/go-autorest/autorest v0.11.1 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/Masterminds/semver v1.5.0 // indirect
-	github.com/containerd/containerd v1.4.1 // indirect
+	github.com/containerd/containerd v1.4.1
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
 	github.com/danieljoos/wincred v1.1.0 // indirect