Просмотр исходного кода

Merge pull request #114 from porter-dev/beta.2.cli-update

Beta.2.cli update
abelanger5 5 лет назад
Родитель
Сommit
1a24542f06

+ 2 - 0
.darwin.goreleaser.yml

@@ -4,6 +4,8 @@ before:
 builds:
   - id: "porter-cli"
     binary: porter
+    ldflags:
+    - -X 'github.com/porter-dev/porter/cli/cmd.Version={{.Version}}'
     env:
       - CGO_ENABLED=1
     dir: cli

+ 2 - 0
.goreleaser.yml

@@ -3,6 +3,8 @@ before:
     - go mod download
 builds:
   - id: "porter-cli"
+    ldflags:
+    - -X 'github.com/porter-dev/porter/cli/cmd.Version={{.Version}}'
     binary: porter
     dir: cli
     main: ./main.go

+ 29 - 0
cli/cmd/api/project.go

@@ -272,6 +272,35 @@ func (c *Client) CreateProjectCluster(
 	return bodyResp, nil
 }
 
+// DeleteProjectCluster deletes a cluster given a project id and cluster id
+func (c *Client) DeleteProjectCluster(
+	ctx context.Context,
+	projectID uint,
+	clusterID uint,
+) error {
+	req, err := http.NewRequest(
+		"DELETE",
+		fmt.Sprintf("%s/projects/%d/clusters/%d", c.BaseURL, projectID, clusterID),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 // DeleteProjectResponse is the object returned after project deletion
 type DeleteProjectResponse models.ProjectExternal
 

+ 61 - 0
cli/cmd/api/registry.go

@@ -104,6 +104,67 @@ func (c *Client) CreateGCR(
 	return bodyResp, nil
 }
 
+// ListRegistryResponse is the list of registries for a project
+type ListRegistryResponse []models.RegistryExternal
+
+// ListRegistries returns a list of registries for a project
+func (c *Client) ListRegistries(
+	ctx context.Context,
+	projectID uint,
+) (ListRegistryResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListRegistryResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return *bodyResp, nil
+}
+
+// DeleteProjectRegistry deletes a registry given a project id and registry id
+func (c *Client) DeleteProjectRegistry(
+	ctx context.Context,
+	projectID uint,
+	registryID uint,
+) error {
+	req, err := http.NewRequest(
+		"DELETE",
+		fmt.Sprintf("%s/projects/%d/registries/%d", c.BaseURL, projectID, registryID),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 type ListRegistryRepositoryResponse []registry.Repository
 

+ 105 - 6
cli/cmd/cluster.go

@@ -4,22 +4,58 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 )
 
 // clusterCmd represents the "porter cluster" base command when called
 // without any subcommands
 var clusterCmd = &cobra.Command{
-	Use:   "cluster",
-	Short: "Commands that read from a connected cluster",
+	Use:     "cluster",
+	Aliases: []string{"clusters"},
+	Short:   "Commands that read from a connected cluster",
 }
 
-var listClusterNSCmd = &cobra.Command{
-	Use:   "namespace list",
-	Short: "Lists the namespaces in a cluster (used for testing connection)",
+var clusterListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the linked clusters in the current project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listClusters)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var clusterDeleteCmd = &cobra.Command{
+	Use:   "delete [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Deletes the cluster with the given id",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deleteCluster)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var clusterNamespaceCmd = &cobra.Command{
+	Use:     "namespace",
+	Aliases: []string{"namespaces"},
+	Short:   "Commands that perform operations on cluster namespaces",
+}
+
+var clusterNamespaceListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the namespaces in a cluster",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listNamespaces)
 
@@ -39,7 +75,70 @@ func init() {
 		"id of the cluster",
 	)
 
-	clusterCmd.AddCommand(listClusterNSCmd)
+	clusterCmd.AddCommand(clusterNamespaceCmd)
+	clusterCmd.AddCommand(clusterListCmd)
+	clusterCmd.AddCommand(clusterDeleteCmd)
+
+	clusterNamespaceCmd.AddCommand(clusterNamespaceListCmd)
+}
+
+func listClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVER")
+
+	currClusterID := getClusterID()
+
+	for _, cluster := range clusters {
+		if currClusterID == cluster.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current cluster)\n", cluster.ID, cluster.Name, cluster.Server)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\n", cluster.ID, cluster.Name, cluster.Server)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func deleteCluster(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the cluster with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			return err
+		}
+
+		err = client.DeleteProjectCluster(context.Background(), getProjectID(), uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted cluster with id %d\n", id)
+	}
+
+	return nil
 }
 
 func listNamespaces(user *api.AuthCheckResponse, client *api.Client, args []string) error {

+ 21 - 0
cli/cmd/config.go

@@ -1,7 +1,10 @@
 package cmd
 
 import (
+	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strconv"
 
 	"github.com/fatih/color"
@@ -21,6 +24,12 @@ var (
 var configCmd = &cobra.Command{
 	Use:   "config",
 	Short: "Commands that control local configuration settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		if err := printConfig(); err != nil {
+			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
 }
 
 var setProjectCmd = &cobra.Command{
@@ -128,6 +137,18 @@ func getDriver() string {
 	return viper.GetString("driver")
 }
 
+func printConfig() error {
+	config, err := ioutil.ReadFile(filepath.Join(home, ".porter", "porter.yaml"))
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf(string(config))
+
+	return nil
+}
+
 func setProject(id uint) error {
 	viper.Set("project", id)
 	color.New(color.FgGreen).Printf("Set the current project id as %d\n", id)

+ 12 - 6
cli/cmd/connect.go

@@ -21,7 +21,7 @@ var connectCmd = &cobra.Command{
 
 var connectKubeconfigCmd = &cobra.Command{
 	Use:   "kubeconfig",
-	Short: "Uses the local kubeconfig to connect to a cluster",
+	Short: "Uses the local kubeconfig to add a cluster",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectKubeconfig)
 
@@ -33,7 +33,7 @@ var connectKubeconfigCmd = &cobra.Command{
 
 var connectECRCmd = &cobra.Command{
 	Use:   "ecr",
-	Short: "Connects an ECR instance to a project",
+	Short: "Adds an ECR instance to a project",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectECR)
 
@@ -45,7 +45,7 @@ var connectECRCmd = &cobra.Command{
 
 var connectGCRCmd = &cobra.Command{
 	Use:   "gcr",
-	Short: "Connects a GCR instance to a project",
+	Short: "Adds a GCR instance to a project",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectGCR)
 
@@ -83,9 +83,9 @@ func init() {
 	)
 
 	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
-		"contexts",
+		"context",
 		nil,
-		"the list of contexts to connect (defaults to the current context)",
+		"the context to connect (defaults to the current context)",
 	)
 
 	connectCmd.AddCommand(connectECRCmd)
@@ -99,13 +99,19 @@ func runConnectKubeconfig(_ *api.AuthCheckResponse, client *api.Client, _ []stri
 		isLocal = true
 	}
 
-	return connect.Kubeconfig(
+	id, err := connect.Kubeconfig(
 		client,
 		kubeconfigPath,
 		*contexts,
 		getProjectID(),
 		isLocal,
 	)
+
+	if err != nil {
+		return err
+	}
+
+	return setCluster(id)
 }
 
 func runConnectECR(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {

+ 28 - 25
cli/cmd/connect/kubeconfig.go

@@ -27,17 +27,17 @@ func Kubeconfig(
 	contexts []string,
 	projectID uint,
 	isLocal bool,
-) error {
+) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	if projectID == 0 {
-		return fmt.Errorf("no project set, please run porter project set [id]")
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
 	}
 
 	// get the kubeconfig
 	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
 	// send kubeconfig to client
@@ -51,9 +51,11 @@ func Kubeconfig(
 	)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
+	var lastClusterID uint
+
 	for _, cc := range ccs {
 		var cluster *models.ClusterExternal
 
@@ -66,7 +68,7 @@ func Kubeconfig(
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -75,25 +77,25 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClusterCAAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClusterLocalhost:
 					err := resolveLocalhostAction(allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClientCertData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -102,19 +104,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClientCertAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClientKeyData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -123,19 +125,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClientKeyAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.OIDCIssuerData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -144,19 +146,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveOIDCIssuerAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.TokenData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -165,13 +167,13 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveTokenDataAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.GCPKeyData:
 					err := resolveGCPKeyAction(
@@ -181,7 +183,7 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.AWSData:
 					err := resolveAWSAction(
@@ -194,7 +196,7 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				}
 			}
@@ -207,7 +209,7 @@ func Kubeconfig(
 			)
 
 			if err != nil {
-				return err
+				return 0, err
 			}
 
 			clExt := models.ClusterExternal(*resp)
@@ -221,7 +223,7 @@ func Kubeconfig(
 			)
 
 			if err != nil {
-				return err
+				return 0, err
 			}
 
 			clExt := models.ClusterExternal(*resp)
@@ -230,9 +232,10 @@ func Kubeconfig(
 		}
 
 		color.New(color.FgGreen).Printf("created cluster %s with id %d\n", cluster.Name, cluster.ID)
+		lastClusterID = cluster.ID
 	}
 
-	return nil
+	return lastClusterID, nil
 }
 
 // resolves a cluster ca data action

+ 3 - 37
cli/cmd/project.go

@@ -17,8 +17,9 @@ import (
 // projectCmd represents the "porter project" base command when called
 // without any subcommands
 var projectCmd = &cobra.Command{
-	Use:   "project",
-	Short: "Commands that control Porter project settings",
+	Use:     "project",
+	Aliases: []string{"projects"},
+	Short:   "Commands that control Porter project settings",
 }
 
 var createProjectCmd = &cobra.Command{
@@ -59,18 +60,6 @@ var listProjectCmd = &cobra.Command{
 	},
 }
 
-var listProjectClustersCmd = &cobra.Command{
-	Use:   "clusters list",
-	Short: "Lists the linked clusters for a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listProjectClusters)
-
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
 func init() {
 	rootCmd.AddCommand(projectCmd)
 
@@ -86,8 +75,6 @@ func init() {
 	projectCmd.AddCommand(deleteProjectCmd)
 
 	projectCmd.AddCommand(listProjectCmd)
-
-	projectCmd.AddCommand(listProjectClustersCmd)
 }
 
 func createProject(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
@@ -162,24 +149,3 @@ func deleteProject(_ *api.AuthCheckResponse, client *api.Client, args []string)
 
 	return nil
 }
-
-func listProjectClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
-	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
-
-	if err != nil {
-		return err
-	}
-
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
-
-	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVER")
-
-	for _, cluster := range clusters {
-		fmt.Fprintf(w, "%d\t%s\t%s\n", cluster.ID, cluster.Name, cluster.Server)
-	}
-
-	w.Flush()
-
-	return nil
-}

+ 123 - 10
cli/cmd/registry.go

@@ -4,22 +4,58 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 )
 
 // registryCmd represents the "porter registry" base command when called
 // without any subcommands
 var registryCmd = &cobra.Command{
-	Use:   "registry",
-	Short: "Commands that read from a connected registry",
+	Use:     "registry",
+	Aliases: []string{"registries"},
+	Short:   "Commands that read from a connected registry",
 }
 
-var registryCmdListRepos = &cobra.Command{
-	Use:   "repos list",
-	Short: "Lists the repositories in a registry",
+var registryListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the registries linked to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listRegistries)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var registryDeleteCmd = &cobra.Command{
+	Use:   "delete [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Deletes the registry with the given id",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deleteRegistry)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var registryReposCmd = &cobra.Command{
+	Use:     "repo",
+	Aliases: []string{"repos", "repository", "repositories"},
+	Short:   "Commands that perform operations on image registry repositories",
+}
+
+var registryReposListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the repositories in an image registry",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listRepos)
 
@@ -29,9 +65,16 @@ var registryCmdListRepos = &cobra.Command{
 	},
 }
 
-var registryCmdListImages = &cobra.Command{
-	Use:   "images list [REPO_NAME]",
-	Short: "Lists the images in an image repository",
+var registryImageCmd = &cobra.Command{
+	Use:     "image",
+	Aliases: []string{"images"},
+	Short:   "Commands that perform operations on image in a repository",
+}
+
+var registryImageListCmd = &cobra.Command{
+	Use:   "list [repo_name]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Lists the images the specified image repository",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listImages)
 
@@ -51,9 +94,79 @@ func init() {
 		"id of the registry",
 	)
 
-	registryCmd.AddCommand(registryCmdListRepos)
+	registryCmd.AddCommand(registryReposCmd)
+	registryCmd.AddCommand(registryListCmd)
+	registryCmd.AddCommand(registryDeleteCmd)
+
+	registryReposCmd.AddCommand(registryReposListCmd)
+
+	registryCmd.AddCommand(registryImageCmd)
+	registryImageCmd.AddCommand(registryImageListCmd)
+}
+
+func listRegistries(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := getProjectID()
+
+	// get the list of namespaces
+	registries, err := client.ListRegistries(
+		context.Background(),
+		pID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVICE")
+
+	currRegistryID := getRegistryID()
+
+	for _, registry := range registries {
+		if currRegistryID == registry.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current registry)\n", registry.ID, registry.Name, registry.Service)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\n", registry.ID, registry.Name, registry.Service)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func deleteRegistry(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the registry with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			return err
+		}
+
+		err = client.DeleteProjectRegistry(context.Background(), getProjectID(), uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted registry with id %d\n", id)
+	}
 
-	registryCmd.AddCommand(registryCmdListImages)
+	return nil
 }
 
 func listRepos(user *api.AuthCheckResponse, client *api.Client, args []string) error {

+ 6 - 5
cli/cmd/server.go

@@ -23,14 +23,15 @@ type startOps struct {
 var opts = &startOps{}
 
 var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "Commands to control a local Porter server",
+	Use:     "server",
+	Aliases: []string{"svr"},
+	Short:   "Commands to control a local Porter server",
 }
 
 // startCmd represents the start command
 var startCmd = &cobra.Command{
 	Use:   "start",
-	Short: "Starts a Porter instance using the Docker engine",
+	Short: "Starts a Porter server instance on the host",
 	Run: func(cmd *cobra.Command, args []string) {
 		if getDriver() == "docker" {
 			setDriver("docker")
@@ -100,14 +101,14 @@ func init() {
 		&opts.driver,
 		"driver",
 		"local",
-		"the db to use, one of local or docker",
+		"the driver to use, one of \"local\" or \"docker\"",
 	)
 
 	startCmd.PersistentFlags().StringVar(
 		&opts.imageTag,
 		"image-tag",
 		"latest",
-		"the Porter image tag to use",
+		"the Porter image tag to use (if using docker driver)",
 	)
 
 	opts.port = startCmd.PersistentFlags().IntP(

+ 23 - 0
cli/cmd/version.go

@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// Version will be linked by an ldflag during build
+var Version string = "dev"
+
+var versionCmd = &cobra.Command{
+	Use:     "version",
+	Aliases: []string{"v", "--version"},
+	Short:   "Prints the version of the Porter CLI",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(Version)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+}