Kaynağa Gözat

code reorganisation and new build driver

Mohammed Nafees 4 yıl önce
ebeveyn
işleme
ca42abc069

+ 22 - 203
cli/cmd/apply.go

@@ -16,7 +16,9 @@ import (
 	"github.com/mitchellh/mapstructure"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
+	"github.com/porter-dev/porter/cli/cmd/preview"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
@@ -96,6 +98,8 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 
 	worker := worker.NewWorker()
 	worker.RegisterDriver("porter.deploy", NewPorterDriver)
+	worker.RegisterDriver("porter.build", preview.NewBuildDriver)
+
 	worker.SetDefaultDriver("porter.deploy")
 
 	if hasDeploymentHookEnvVars() {
@@ -154,20 +158,6 @@ func hasDeploymentHookEnvVars() bool {
 	return true
 }
 
-type Source struct {
-	Name          string
-	Repo          string
-	Version       string
-	IsApplication bool
-	SourceValues  map[string]interface{}
-}
-
-type Target struct {
-	Project   uint
-	Cluster   uint
-	Namespace string
-}
-
 type ApplicationConfig struct {
 	WaitForJob bool
 
@@ -176,9 +166,9 @@ type ApplicationConfig struct {
 	OnlyCreate bool
 
 	Build struct {
-		ForceBuild bool
-		ForcePush  bool
-		UseCache   bool
+		ForceBuild bool `mapstructure:"force_build"`
+		ForcePush  bool `mapstructure:"force_push"`
+		UseCache   bool `mapstructure:"use_cache"`
 		Method     string
 		Context    string
 		Dockerfile string
@@ -187,14 +177,14 @@ type ApplicationConfig struct {
 		Buildpacks []string
 	}
 
-	EnvGroups []types.EnvGroupMeta
+	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
 
 	Values map[string]interface{}
 }
 
 type Driver struct {
-	source      *Source
-	target      *Target
+	source      *preview.Source
+	target      *preview.Target
 	output      map[string]interface{}
 	lookupTable *map[string]drivers.Driver
 	logger      *zerolog.Logger
@@ -207,18 +197,14 @@ func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts)
 		output:      make(map[string]interface{}),
 	}
 
-	source := &Source{}
-
-	err := getSource(resource.Source, source)
+	source, err := preview.GetSource(resource.Source)
 	if err != nil {
 		return nil, err
 	}
 
 	driver.source = source
 
-	target := &Target{}
-
-	err = getTarget(resource.Target, target)
+	target, err := preview.GetTarget(resource.Target)
 	if err != nil {
 		return nil, err
 	}
@@ -233,7 +219,7 @@ func (d *Driver) ShouldApply(resource *models.Resource) bool {
 }
 
 func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
-	client := GetAPIClient(config)
+	client := config.GetAPIClient()
 	name := resource.Name
 
 	if name == "" {
@@ -398,12 +384,12 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
 		color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resource.Name)
 
-		prevProject := config.Project
-		prevCluster := config.Cluster
+		prevProject := cliConf.Project
+		prevCluster := cliConf.Cluster
 		name = resource.Name
 		namespace = d.target.Namespace
-		config.Project = d.target.Project
-		config.Cluster = d.target.Cluster
+		cliConf.Project = d.target.Project
+		cliConf.Cluster = d.target.Cluster
 
 		err = waitForJob(nil, client, []string{})
 
@@ -411,8 +397,8 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 			return nil, err
 		}
 
-		config.Project = prevProject
-		config.Cluster = prevCluster
+		cliConf.Project = prevProject
+		cliConf.Cluster = prevCluster
 	}
 
 	return resource, err
@@ -586,155 +572,6 @@ func (d *Driver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func getSource(input map[string]interface{}, output *Source) error {
-	// first read from env vars
-	output.Name = os.Getenv("PORTER_SOURCE_NAME")
-	output.Repo = os.Getenv("PORTER_SOURCE_REPO")
-	output.Version = os.Getenv("PORTER_SOURCE_VERSION")
-
-	// next, check for values in the YAML file
-	if output.Name == "" {
-		if name, ok := input["name"]; ok {
-			nameVal, ok := name.(string)
-			if !ok {
-				return fmt.Errorf("invalid name provided")
-			}
-			output.Name = nameVal
-		}
-	}
-
-	if output.Name == "" {
-		return fmt.Errorf("source name required")
-	}
-
-	if output.Repo == "" {
-		if repo, ok := input["repo"]; ok {
-			repoVal, ok := repo.(string)
-			if !ok {
-				return fmt.Errorf("invalid repo provided")
-			}
-			output.Repo = repoVal
-		}
-	}
-
-	if output.Version == "" {
-		if version, ok := input["version"]; ok {
-			versionVal, ok := version.(string)
-			if !ok {
-				return fmt.Errorf("invalid version provided")
-			}
-			output.Version = versionVal
-		}
-	}
-
-	// lastly, just put in the defaults
-	if output.Version == "" {
-		output.Version = "latest"
-	}
-
-	output.IsApplication = output.Repo == "https://charts.getporter.dev"
-
-	if output.Repo == "" {
-		output.Repo = "https://charts.getporter.dev"
-
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
-
-		if err == nil {
-			// found in "https://charts.getporter.dev"
-			output.SourceValues = values
-			output.IsApplication = true
-			return nil
-		}
-
-		output.Repo = "https://chart-addons.getporter.dev"
-
-		values, err = existsInRepo(output.Name, output.Version, output.Repo)
-
-		if err == nil {
-			// found in https://chart-addons.getporter.dev
-			output.SourceValues = values
-			return nil
-		}
-
-		return fmt.Errorf("source does not exist in any repo")
-	} else {
-		// we look in the passed-in repo
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
-
-		if err == nil {
-			output.SourceValues = values
-			return nil
-		}
-	}
-
-	return fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
-}
-
-func getTarget(input map[string]interface{}, output *Target) error {
-	// first read from env vars
-	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
-		project, err := strconv.Atoi(projectEnv)
-		if err != nil {
-			return err
-		}
-		output.Project = uint(project)
-	}
-
-	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
-		cluster, err := strconv.Atoi(clusterEnv)
-		if err != nil {
-			return err
-		}
-		output.Cluster = uint(cluster)
-	}
-
-	output.Namespace = os.Getenv("PORTER_NAMESPACE")
-
-	// next, check for values in the YAML file
-	if output.Project == 0 {
-		if project, ok := input["project"]; ok {
-			projectVal, ok := project.(uint)
-			if !ok {
-				return fmt.Errorf("project value must be an integer")
-			}
-			output.Project = projectVal
-		}
-	}
-
-	if output.Cluster == 0 {
-		if cluster, ok := input["cluster"]; ok {
-			clusterVal, ok := cluster.(uint)
-			if !ok {
-				return fmt.Errorf("cluster value must be an integer")
-			}
-			output.Cluster = clusterVal
-		}
-	}
-
-	if output.Namespace == "" {
-		if namespace, ok := input["namespace"]; ok {
-			namespaceVal, ok := namespace.(string)
-			if !ok {
-				return fmt.Errorf("invalid namespace provided")
-			}
-			output.Namespace = namespaceVal
-		}
-	}
-
-	// lastly, just put in the defaults
-	if output.Project == 0 {
-		output.Project = config.Project
-	}
-	if output.Cluster == 0 {
-		output.Cluster = config.Cluster
-	}
-	if output.Namespace == "" {
-		output.Namespace = "default"
-	}
-
-	return nil
-}
-
 func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
@@ -762,22 +599,6 @@ func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationCo
 	return config, nil
 }
 
-func existsInRepo(name, version, url string) (map[string]interface{}, error) {
-	chart, err := GetAPIClient(config).GetTemplate(
-		context.Background(),
-		name, version,
-		&types.GetTemplateRequest{
-			TemplateGetBaseRequest: types.TemplateGetBaseRequest{
-				RepoURL: url,
-			},
-		},
-	)
-	if err != nil {
-		return nil, err
-	}
-	return chart.Values, nil
-}
-
 type DeploymentHook struct {
 	client                                                    *api.Client
 	resourceGroup                                             *switchboardTypes.ResourceGroup
@@ -810,13 +631,13 @@ func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.Resou
 
 	res.prID = uint(prID)
 
-	res.projectID = config.Project
+	res.projectID = cliConf.Project
 
 	if res.projectID == 0 {
 		return nil, fmt.Errorf("project id must be set")
 	}
 
-	res.clusterID = config.Cluster
+	res.clusterID = cliConf.Cluster
 
 	if res.clusterID == 0 {
 		return nil, fmt.Errorf("cluster id must be set")
@@ -1009,9 +830,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 		}
 
 		if config != nil && len(config.EnvGroups) > 0 {
-			target := &Target{}
-
-			err = getTarget(res.Target, target)
+			target, err := preview.GetTarget(res.Target)
 
 			if err != nil {
 				return err

+ 15 - 14
cli/cmd/auth.go

@@ -9,6 +9,7 @@ import (
 
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	loginBrowser "github.com/porter-dev/porter/cli/cmd/login"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
@@ -75,17 +76,17 @@ func init() {
 }
 
 func login() error {
-	client := api.NewClientWithToken(config.Host+"/api", config.Token)
+	client := api.NewClientWithToken(cliConf.Host+"/api", cliConf.Token)
 
 	user, err := client.AuthCheck(context.Background())
 
 	if err == nil {
 		// set the token if the user calls login with the --token flag or the PORTER_TOKEN env
-		if config.Token != "" {
-			config.SetToken(config.Token)
+		if cliConf.Token != "" {
+			cliConf.SetToken(cliConf.Token)
 			color.New(color.FgGreen).Println("Successfully logged in!")
 
-			projID, exists, err := api.GetProjectIDFromToken(config.Token)
+			projID, exists, err := api.GetProjectIDFromToken(cliConf.Token)
 
 			if err != nil {
 				return err
@@ -102,7 +103,7 @@ func login() error {
 			} else {
 				// if the project ID does exist for the token, this is a project-issued token, and
 				// the project should be set automatically
-				err = config.SetProject(projID)
+				err = cliConf.SetProject(projID)
 
 				if err != nil {
 					return err
@@ -127,20 +128,20 @@ func login() error {
 	}
 
 	// log the user in
-	token, err := loginBrowser.Login(config.Host)
+	token, err := loginBrowser.Login(cliConf.Host)
 
 	if err != nil {
 		return err
 	}
 
 	// set the token in config
-	err = config.SetToken(token)
+	err = cliConf.SetToken(token)
 
 	if err != nil {
 		return err
 	}
 
-	client = api.NewClientWithToken(config.Host+"/api", token)
+	client = api.NewClientWithToken(cliConf.Host+"/api", token)
 
 	user, err = client.AuthCheck(context.Background())
 
@@ -165,7 +166,7 @@ func setProjectForUser(client *api.Client, userID uint) error {
 	projects := *resp
 
 	if len(projects) > 0 {
-		config.SetProject(projects[0].ID)
+		cliConf.SetProject(projects[0].ID)
 
 		err = setProjectCluster(client, projects[0].ID)
 
@@ -178,7 +179,7 @@ func setProjectForUser(client *api.Client, userID uint) error {
 }
 
 func loginManual() error {
-	client := api.NewClient(config.Host+"/api", "cookie.json")
+	client := api.NewClient(cliConf.Host+"/api", "cookie.json")
 
 	var username, pw string
 
@@ -206,7 +207,7 @@ func loginManual() error {
 	}
 
 	// set the token to empty since this is manual (cookie-based) login
-	config.SetToken("")
+	cliConf.SetToken("")
 
 	color.New(color.FgGreen).Println("Successfully logged in!")
 
@@ -220,7 +221,7 @@ func loginManual() error {
 	projects := *resp
 
 	if len(projects) > 0 {
-		config.SetProject(projects[0].ID)
+		cliConf.SetProject(projects[0].ID)
 
 		err = setProjectCluster(client, projects[0].ID)
 
@@ -247,7 +248,7 @@ func register() error {
 		return err
 	}
 
-	client := GetAPIClient(config)
+	client := config.GetAPIClient()
 
 	resp, err := client.CreateUser(context.Background(), &types.CreateUserRequest{
 		Email:    username,
@@ -270,7 +271,7 @@ func logout(user *types.GetAuthenticatedUserResponse, client *api.Client, args [
 		return err
 	}
 
-	config.SetToken("")
+	cliConf.SetToken("")
 
 	color.Green("Successfully logged out")
 

+ 1 - 1
cli/cmd/bluegreen.go

@@ -62,7 +62,7 @@ func init() {
 
 func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 	// get the web release
-	webRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, app)
+	webRelease, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, app)
 
 	if err != nil {
 		return err

+ 5 - 5
cli/cmd/cluster.go

@@ -77,7 +77,7 @@ func init() {
 }
 
 func listClusters(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	resp, err := client.ListProjectClusters(context.Background(), config.Project)
+	resp, err := client.ListProjectClusters(context.Background(), cliConf.Project)
 
 	if err != nil {
 		return err
@@ -90,7 +90,7 @@ func listClusters(user *types.GetAuthenticatedUserResponse, client *api.Client,
 
 	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVER")
 
-	currClusterID := config.Cluster
+	currClusterID := cliConf.Cluster
 
 	for _, cluster := range clusters {
 		if currClusterID == cluster.ID {
@@ -125,7 +125,7 @@ func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client,
 			return err
 		}
 
-		err = client.DeleteProjectCluster(context.Background(), config.Project, uint(id))
+		err = client.DeleteProjectCluster(context.Background(), cliConf.Project, uint(id))
 
 		if err != nil {
 			return err
@@ -138,10 +138,10 @@ func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client,
 }
 
 func listNamespaces(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	pID := config.Project
+	pID := cliConf.Project
 
 	// get the service account based on the cluster id
-	cID := config.Cluster
+	cID := cliConf.Cluster
 
 	// get the list of namespaces
 	namespaces, err := client.GetK8sNamespaces(

+ 12 - 262
cli/cmd/config.go

@@ -14,262 +14,12 @@ import (
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
+	cliConfig "github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-
-	flag "github.com/spf13/pflag"
 )
 
-// shared sets of flags used by multiple commands
-var driverFlagSet = flag.NewFlagSet("driver", flag.ExitOnError)
-var defaultFlagSet = flag.NewFlagSet("shared", flag.ExitOnError) // used by all commands
-var registryFlagSet = flag.NewFlagSet("registry", flag.ExitOnError)
-var helmRepoFlagSet = flag.NewFlagSet("helmrepo", flag.ExitOnError)
-
-// config is a shared object used by all commands
-var config = &CLIConfig{}
-
-// CLIConfig is the set of shared configuration options for the CLI commands.
-// This config is used by viper: calling Set() function for any parameter will
-// update the corresponding field in the viper config file.
-type CLIConfig struct {
-	// Driver can be either "docker" or "local", and represents which driver is
-	// used to run an instance of the server.
-	Driver string `yaml:"driver"`
-
-	Host    string `yaml:"host"`
-	Project uint   `yaml:"project"`
-	Cluster uint   `yaml:"cluster"`
-
-	Token string `yaml:"token"`
-
-	Registry uint `yaml:"registry"`
-	HelmRepo uint `yaml:"helm_repo"`
-}
-
-// InitAndLoadConfig populates the config object with the following precedence rules:
-// 1. flag
-// 2. env
-// 3. config
-// 4. default
-//
-// It populates the shared config object above
-func InitAndLoadConfig() {
-	initAndLoadConfig(config)
-}
-
-func InitAndLoadNewConfig() *CLIConfig {
-	newConfig := &CLIConfig{}
-
-	initAndLoadConfig(newConfig)
-
-	return newConfig
-}
-
-func initAndLoadConfig(_config *CLIConfig) {
-	initFlagSet()
-
-	// check that the .porter folder exists; create if not
-	porterDir := filepath.Join(home, ".porter")
-
-	if _, err := os.Stat(porterDir); os.IsNotExist(err) {
-		os.Mkdir(porterDir, 0700)
-	} else if err != nil {
-		color.New(color.FgRed).Printf("%v\n", err)
-		os.Exit(1)
-	}
-
-	viper.SetConfigName("porter")
-	viper.SetConfigType("yaml")
-	viper.AddConfigPath(porterDir)
-
-	// Bind the flagset initialized above
-	viper.BindPFlags(driverFlagSet)
-	viper.BindPFlags(defaultFlagSet)
-	viper.BindPFlags(registryFlagSet)
-	viper.BindPFlags(helmRepoFlagSet)
-
-	// Bind the environment variables with prefix "PORTER_"
-	viper.SetEnvPrefix("PORTER")
-	viper.BindEnv("host")
-	viper.BindEnv("project")
-	viper.BindEnv("cluster")
-	viper.BindEnv("token")
-
-	err := viper.ReadInConfig()
-
-	if err != nil {
-		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
-			// create blank config file
-			err := ioutil.WriteFile(filepath.Join(home, ".porter", "porter.yaml"), []byte{}, 0644)
-
-			if err != nil {
-				color.New(color.FgRed).Printf("%v\n", err)
-				os.Exit(1)
-			}
-		} else {
-			// Config file was found but another error was produced
-			color.New(color.FgRed).Printf("%v\n", err)
-			os.Exit(1)
-		}
-	}
-
-	// unmarshal the config into the shared config struct
-	viper.Unmarshal(_config)
-}
-
-// initFlagSet initializes the shared flags used by multiple commands
-func initFlagSet() {
-	driverFlagSet.StringVar(
-		&config.Driver,
-		"driver",
-		"local",
-		"driver to use (local or docker)",
-	)
-
-	defaultFlagSet.StringVar(
-		&config.Host,
-		"host",
-		"https://dashboard.getporter.dev",
-		"host URL of Porter instance",
-	)
-
-	defaultFlagSet.UintVar(
-		&config.Project,
-		"project",
-		0,
-		"project ID of Porter project",
-	)
-
-	defaultFlagSet.UintVar(
-		&config.Cluster,
-		"cluster",
-		0,
-		"cluster ID of Porter cluster",
-	)
-
-	defaultFlagSet.StringVar(
-		&config.Token,
-		"token",
-		"",
-		"token for Porter authentication",
-	)
-
-	registryFlagSet.UintVar(
-		&config.Registry,
-		"registry",
-		0,
-		"registry ID of connected Porter registry",
-	)
-
-	helmRepoFlagSet.UintVar(
-		&config.HelmRepo,
-		"helmrepo",
-		0,
-		"helm repo ID of connected Porter Helm repository",
-	)
-}
-
-func (c *CLIConfig) SetDriver(driver string) error {
-	viper.Set("driver", driver)
-	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Driver = driver
-
-	return nil
-}
-
-func (c *CLIConfig) SetHost(host string) error {
-	// a trailing / can lead to errors with the api server
-	host = strings.TrimRight(host, "/")
-
-	viper.Set("host", host)
-	color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Host = host
-
-	return nil
-}
-
-func (c *CLIConfig) SetProject(projectID uint) error {
-	viper.Set("project", projectID)
-	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Project = projectID
-
-	return nil
-}
-
-func (c *CLIConfig) SetCluster(clusterID uint) error {
-	viper.Set("cluster", clusterID)
-	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Cluster = clusterID
-
-	return nil
-}
-
-func (c *CLIConfig) SetToken(token string) error {
-	viper.Set("token", token)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Token = token
-
-	return nil
-}
-
-func (c *CLIConfig) SetRegistry(registryID uint) error {
-	viper.Set("registry", registryID)
-	color.New(color.FgGreen).Printf("Set the current registry as %d\n", registryID)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.Registry = registryID
-
-	return nil
-}
-
-func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
-	viper.Set("helm_repo", helmRepoID)
-	color.New(color.FgGreen).Printf("Set the current Helm repo as %d\n", helmRepoID)
-	err := viper.WriteConfig()
-
-	if err != nil {
-		return err
-	}
-
-	config.HelmRepo = helmRepoID
-
-	return nil
-}
+var cliConf = cliConfig.GetCLIConfig()
 
 var configCmd = &cobra.Command{
 	Use:   "config",
@@ -301,7 +51,7 @@ var configSetProjectCmd = &cobra.Command{
 				os.Exit(1)
 			}
 
-			err = config.SetProject(uint(projID))
+			err = cliConf.SetProject(uint(projID))
 
 			if err != nil {
 				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
@@ -330,7 +80,7 @@ var configSetClusterCmd = &cobra.Command{
 				os.Exit(1)
 			}
 
-			err = config.SetCluster(uint(clusterID))
+			err = cliConf.SetCluster(uint(clusterID))
 
 			if err != nil {
 				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
@@ -359,7 +109,7 @@ var configSetRegistryCmd = &cobra.Command{
 				os.Exit(1)
 			}
 
-			err = config.SetRegistry(uint(registryID))
+			err = cliConf.SetRegistry(uint(registryID))
 
 			if err != nil {
 				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
@@ -381,7 +131,7 @@ var configSetHelmRepoCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		err = config.SetHelmRepo(uint(hrID))
+		err = cliConf.SetHelmRepo(uint(hrID))
 
 		if err != nil {
 			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
@@ -395,7 +145,7 @@ var configSetHostCmd = &cobra.Command{
 	Args:  cobra.ExactArgs(1),
 	Short: "Saves the host in the default configuration",
 	Run: func(cmd *cobra.Command, args []string) {
-		err := config.SetHost(args[0])
+		err := cliConf.SetHost(args[0])
 
 		if err != nil {
 			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
@@ -463,7 +213,7 @@ func listAndSetProject(_ *types.GetAuthenticatedUserResponse, client *api.Client
 		projID = uint64((*resp)[0].ID)
 	}
 
-	config.SetProject(uint(projID))
+	cliConf.SetProject(uint(projID))
 
 	return nil
 }
@@ -474,7 +224,7 @@ func listAndSetCluster(_ *types.GetAuthenticatedUserResponse, client *api.Client
 	s.Suffix = " Loading list of clusters"
 	s.Start()
 
-	resp, err := client.ListProjectClusters(context.Background(), config.Project)
+	resp, err := client.ListProjectClusters(context.Background(), cliConf.Project)
 
 	s.Stop()
 
@@ -504,7 +254,7 @@ func listAndSetCluster(_ *types.GetAuthenticatedUserResponse, client *api.Client
 		clusterID = uint64((*resp)[0].ID)
 	}
 
-	config.SetCluster(uint(clusterID))
+	cliConf.SetCluster(uint(clusterID))
 
 	return nil
 }
@@ -515,7 +265,7 @@ func listAndSetRegistry(_ *types.GetAuthenticatedUserResponse, client *api.Clien
 	s.Suffix = " Loading list of registries"
 	s.Start()
 
-	resp, err := client.ListRegistries(context.Background(), config.Project)
+	resp, err := client.ListRegistries(context.Background(), cliConf.Project)
 
 	s.Stop()
 
@@ -545,7 +295,7 @@ func listAndSetRegistry(_ *types.GetAuthenticatedUserResponse, client *api.Clien
 		regID = uint64((*resp)[0].ID)
 	}
 
-	config.SetRegistry(uint(regID))
+	cliConf.SetRegistry(uint(regID))
 
 	return nil
 }

+ 278 - 0
cli/cmd/config/config.go

@@ -0,0 +1,278 @@
+package config
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/viper"
+	"k8s.io/client-go/util/homedir"
+)
+
+var home = homedir.HomeDir()
+
+// config is a shared object used by all commands
+var config = &CLIConfig{}
+
+// CLIConfig is the set of shared configuration options for the CLI commands.
+// This config is used by viper: calling Set() function for any parameter will
+// update the corresponding field in the viper config file.
+type CLIConfig struct {
+	// Driver can be either "docker" or "local", and represents which driver is
+	// used to run an instance of the server.
+	Driver string `yaml:"driver"`
+
+	Host    string `yaml:"host"`
+	Project uint   `yaml:"project"`
+	Cluster uint   `yaml:"cluster"`
+
+	Token string `yaml:"token"`
+
+	Registry uint `yaml:"registry"`
+	HelmRepo uint `yaml:"helm_repo"`
+}
+
+// InitAndLoadConfig populates the config object with the following precedence rules:
+// 1. flag
+// 2. env
+// 3. config
+// 4. default
+//
+// It populates the shared config object above
+func InitAndLoadConfig() {
+	initAndLoadConfig(config)
+}
+
+func InitAndLoadNewConfig() *CLIConfig {
+	newConfig := &CLIConfig{}
+
+	initAndLoadConfig(newConfig)
+
+	return newConfig
+}
+
+func initAndLoadConfig(_config *CLIConfig) {
+	initFlagSet()
+
+	// check that the .porter folder exists; create if not
+	porterDir := filepath.Join(home, ".porter")
+
+	if _, err := os.Stat(porterDir); os.IsNotExist(err) {
+		os.Mkdir(porterDir, 0700)
+	} else if err != nil {
+		color.New(color.FgRed).Printf("%v\n", err)
+		os.Exit(1)
+	}
+
+	viper.SetConfigName("porter")
+	viper.SetConfigType("yaml")
+	viper.AddConfigPath(porterDir)
+
+	// Bind the flagset initialized above
+	viper.BindPFlags(utils.DriverFlagSet)
+	viper.BindPFlags(utils.DefaultFlagSet)
+	viper.BindPFlags(utils.RegistryFlagSet)
+	viper.BindPFlags(utils.HelmRepoFlagSet)
+
+	// Bind the environment variables with prefix "PORTER_"
+	viper.SetEnvPrefix("PORTER")
+	viper.BindEnv("host")
+	viper.BindEnv("project")
+	viper.BindEnv("cluster")
+	viper.BindEnv("token")
+
+	err := viper.ReadInConfig()
+
+	if err != nil {
+		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
+			// create blank config file
+			err := ioutil.WriteFile(filepath.Join(home, ".porter", "porter.yaml"), []byte{}, 0644)
+
+			if err != nil {
+				color.New(color.FgRed).Printf("%v\n", err)
+				os.Exit(1)
+			}
+		} else {
+			// Config file was found but another error was produced
+			color.New(color.FgRed).Printf("%v\n", err)
+			os.Exit(1)
+		}
+	}
+
+	// unmarshal the config into the shared config struct
+	viper.Unmarshal(_config)
+}
+
+// initFlagSet initializes the shared flags used by multiple commands
+func initFlagSet() {
+	utils.DriverFlagSet.StringVar(
+		&config.Driver,
+		"driver",
+		"local",
+		"driver to use (local or docker)",
+	)
+
+	utils.DefaultFlagSet.StringVar(
+		&config.Host,
+		"host",
+		"https://dashboard.getporter.dev",
+		"host URL of Porter instance",
+	)
+
+	utils.DefaultFlagSet.UintVar(
+		&config.Project,
+		"project",
+		0,
+		"project ID of Porter project",
+	)
+
+	utils.DefaultFlagSet.UintVar(
+		&config.Cluster,
+		"cluster",
+		0,
+		"cluster ID of Porter cluster",
+	)
+
+	utils.DefaultFlagSet.StringVar(
+		&config.Token,
+		"token",
+		"",
+		"token for Porter authentication",
+	)
+
+	utils.RegistryFlagSet.UintVar(
+		&config.Registry,
+		"registry",
+		0,
+		"registry ID of connected Porter registry",
+	)
+
+	utils.HelmRepoFlagSet.UintVar(
+		&config.HelmRepo,
+		"helmrepo",
+		0,
+		"helm repo ID of connected Porter Helm repository",
+	)
+}
+
+func GetCLIConfig() *CLIConfig {
+	if config == nil {
+		panic("GetCLIConfig() called before initialisation")
+	}
+
+	return config
+}
+
+func GetAPIClient() *api.Client {
+	config := GetCLIConfig()
+
+	if token := config.Token; token != "" {
+		return api.NewClientWithToken(config.Host+"/api", token)
+	}
+
+	return api.NewClient(config.Host+"/api", "cookie.json")
+}
+
+func (c *CLIConfig) SetDriver(driver string) error {
+	viper.Set("driver", driver)
+	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Driver = driver
+
+	return nil
+}
+
+func (c *CLIConfig) SetHost(host string) error {
+	// a trailing / can lead to errors with the api server
+	host = strings.TrimRight(host, "/")
+
+	viper.Set("host", host)
+	color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Host = host
+
+	return nil
+}
+
+func (c *CLIConfig) SetProject(projectID uint) error {
+	viper.Set("project", projectID)
+	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Project = projectID
+
+	return nil
+}
+
+func (c *CLIConfig) SetCluster(clusterID uint) error {
+	viper.Set("cluster", clusterID)
+	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Cluster = clusterID
+
+	return nil
+}
+
+func (c *CLIConfig) SetToken(token string) error {
+	viper.Set("token", token)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Token = token
+
+	return nil
+}
+
+func (c *CLIConfig) SetRegistry(registryID uint) error {
+	viper.Set("registry", registryID)
+	color.New(color.FgGreen).Printf("Set the current registry as %d\n", registryID)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.Registry = registryID
+
+	return nil
+}
+
+func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
+	viper.Set("helm_repo", helmRepoID)
+	color.New(color.FgGreen).Printf("Set the current Helm repo as %d\n", helmRepoID)
+	err := viper.WriteConfig()
+
+	if err != nil {
+		return err
+	}
+
+	config.HelmRepo = helmRepoID
+
+	return nil
+}

+ 15 - 15
cli/cmd/connect.go

@@ -134,7 +134,7 @@ func init() {
 func runConnectKubeconfig(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	isLocal := false
 
-	if config.Driver == "local" {
+	if cliConf.Driver == "local" {
 		isLocal = true
 	}
 
@@ -142,7 +142,7 @@ func runConnectKubeconfig(_ *types.GetAuthenticatedUserResponse, client *api.Cli
 		client,
 		kubeconfigPath,
 		*contexts,
-		config.Project,
+		cliConf.Project,
 		isLocal,
 	)
 
@@ -150,83 +150,83 @@ func runConnectKubeconfig(_ *types.GetAuthenticatedUserResponse, client *api.Cli
 		return err
 	}
 
-	return config.SetCluster(id)
+	return cliConf.SetCluster(id)
 }
 
 func runConnectECR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.ECR(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetRegistry(regID)
+	return cliConf.SetRegistry(regID)
 }
 
 func runConnectGCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.GCR(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetRegistry(regID)
+	return cliConf.SetRegistry(regID)
 }
 
 func runConnectDOCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.DOCR(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetRegistry(regID)
+	return cliConf.SetRegistry(regID)
 }
 
 func runConnectDockerhub(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.Dockerhub(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetRegistry(regID)
+	return cliConf.SetRegistry(regID)
 }
 
 func runConnectRegistry(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.Registry(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetRegistry(regID)
+	return cliConf.SetRegistry(regID)
 }
 
 func runConnectHelmRepo(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	hrID, err := connect.HelmRepo(
 		client,
-		config.Project,
+		cliConf.Project,
 	)
 
 	if err != nil {
 		return err
 	}
 
-	return config.SetHelmRepo(hrID)
+	return cliConf.SetHelmRepo(hrID)
 }

+ 3 - 3
cli/cmd/create.go

@@ -232,8 +232,8 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 		Client: client,
 		CreateOpts: &deploy.CreateOpts{
 			SharedOpts: &deploy.SharedOpts{
-				ProjectID:       config.Project,
-				ClusterID:       config.Cluster,
+				ProjectID:       cliConf.Project,
+				ClusterID:       cliConf.Cluster,
 				Namespace:       namespace,
 				LocalPath:       fullPath,
 				LocalDockerfile: dockerfile,
@@ -257,7 +257,7 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 			err = client.CreateRepository(
 				context.Background(),
-				config.Project,
+				cliConf.Project,
 				regID,
 				&types.CreateRegistryRepositoryRequest{
 					ImageRepoURI: imageURL,

+ 2 - 2
cli/cmd/delete.go

@@ -47,13 +47,13 @@ func init() {
 }
 
 func delete(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	projectID := config.Project
+	projectID := cliConf.Project
 
 	if projectID == 0 {
 		return fmt.Errorf("project id must be set")
 	}
 
-	clusterID := config.Cluster
+	clusterID := cliConf.Cluster
 
 	if clusterID == 0 {
 		return fmt.Errorf("cluster id must be set")

+ 2 - 2
cli/cmd/deploy.go

@@ -452,8 +452,8 @@ func updateGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
 	// initialize the update agent
 	return deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
 		SharedOpts: &deploy.SharedOpts{
-			ProjectID:       config.Project,
-			ClusterID:       config.Cluster,
+			ProjectID:       cliConf.Project,
+			ClusterID:       cliConf.Cluster,
 			Namespace:       namespace,
 			LocalPath:       localPath,
 			LocalDockerfile: dockerfile,

+ 12 - 12
cli/cmd/deploy/build.go

@@ -16,10 +16,10 @@ import (
 type BuildAgent struct {
 	*SharedOpts
 
-	client      *api.Client
-	imageRepo   string
-	env         map[string]string
-	imageExists bool
+	APIClient   *api.Client
+	ImageRepo   string
+	Env         map[string]string
+	ImageExists bool
 }
 
 // BuildDocker uses the local Docker daemon to build the image
@@ -42,11 +42,11 @@ func (b *BuildAgent) BuildDocker(
 	}
 
 	opts := &docker.BuildOpts{
-		ImageRepo:         b.imageRepo,
+		ImageRepo:         b.ImageRepo,
 		Tag:               tag,
 		CurrentTag:        currentTag,
 		BuildContext:      buildCtx,
-		Env:               b.env,
+		Env:               b.Env,
 		DockerfilePath:    dockerfilePath,
 		IsDockerfileInCtx: isDockerfileInCtx,
 		UseCache:          b.UseCache,
@@ -60,10 +60,10 @@ func (b *BuildAgent) BuildDocker(
 // BuildPack uses the cloud-native buildpack client to build a container image
 func (b *BuildAgent) BuildPack(dockerAgent *docker.Agent, dst, tag, prevTag string, buildConfig *types.BuildConfig) error {
 	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
-	if b.imageExists {
+	if b.ImageExists {
 		err := dockerAgent.TagImage(
-			fmt.Sprintf("%s:%s", b.imageRepo, prevTag),
-			fmt.Sprintf("%s:%s", b.imageRepo, "pack-cache"),
+			fmt.Sprintf("%s:%s", b.ImageRepo, prevTag),
+			fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"),
 		)
 
 		if err != nil {
@@ -75,15 +75,15 @@ func (b *BuildAgent) BuildPack(dockerAgent *docker.Agent, dst, tag, prevTag stri
 	packAgent := &pack.Agent{}
 
 	opts := &docker.BuildOpts{
-		ImageRepo:    b.imageRepo,
+		ImageRepo:    b.ImageRepo,
 		Tag:          tag,
 		BuildContext: dst,
-		Env:          b.env,
+		Env:          b.Env,
 		UseCache:     b.UseCache,
 	}
 
 	// call builder
-	return packAgent.Build(opts, buildConfig, fmt.Sprintf("%s:%s", b.imageRepo, "pack-cache"))
+	return packAgent.Build(opts, buildConfig, fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"))
 }
 
 // ResolveDockerPaths returns a path to the dockerfile that is either relative or absolute, and a path

+ 14 - 8
cli/cmd/deploy/create.go

@@ -92,7 +92,7 @@ func (c *CreateAgent) CreateFromGithub(
 		return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
 	}
 
-	latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
 
 	if err != nil {
 		return "", err
@@ -173,7 +173,7 @@ func (c *CreateAgent) CreateFromRegistry(
 
 	opts := c.CreateOpts
 
-	latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
 
 	if err != nil {
 		return "", err
@@ -255,7 +255,7 @@ func (c *CreateAgent) CreateFromDocker(
 		return "", err
 	}
 
-	latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
 
 	if err != nil {
 		return "", err
@@ -291,10 +291,10 @@ func (c *CreateAgent) CreateFromDocker(
 
 		buildAgent := &BuildAgent{
 			SharedOpts:  opts.SharedOpts,
-			client:      c.Client,
-			imageRepo:   imageURL,
-			env:         env,
-			imageExists: false,
+			APIClient:   c.Client,
+			ImageRepo:   imageURL,
+			Env:         env,
+			ImageExists: false,
 		}
 
 		if opts.Method == DeployBuildTypeDocker {
@@ -472,7 +472,7 @@ func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersi
 	return chart.Values, nil
 }
 
-func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
+func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
 	// deploy the template
 	latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
 
@@ -480,6 +480,8 @@ func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (st
 		return "", nil, err
 	}
 
+	fmt.Printf("latestVersion: %v\n", latestVersion)
+
 	// get the values of the template
 	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
 
@@ -487,6 +489,8 @@ func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (st
 		return "", nil, err
 	}
 
+	fmt.Printf("values: %v\n", values)
+
 	err = coalesceEnvGroups(c.Client, c.CreateOpts.ProjectID, c.CreateOpts.ClusterID,
 		c.CreateOpts.Namespace, c.CreateOpts.EnvGroups, values)
 
@@ -494,6 +498,8 @@ func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (st
 		return "", nil, err
 	}
 
+	fmt.Printf("values: %v\n", values)
+
 	// merge existing values with overriding values
 	mergedValues := utils.CoalesceValues(values, overrideValues)
 

+ 4 - 4
cli/cmd/deploy/deploy.go

@@ -292,10 +292,10 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig, forceBuild b
 
 	buildAgent := &BuildAgent{
 		SharedOpts:  d.opts.SharedOpts,
-		client:      d.Client,
-		imageRepo:   d.imageRepo,
-		env:         d.env,
-		imageExists: d.imageExists,
+		APIClient:   d.Client,
+		ImageRepo:   d.imageRepo,
+		Env:         d.env,
+		ImageExists: d.imageExists,
 	}
 
 	if d.opts.Method == DeployBuildTypeDocker {

+ 4 - 3
cli/cmd/docker.go

@@ -15,6 +15,7 @@ import (
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	ptypes "github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/github"
 	"github.com/spf13/cobra"
 
@@ -50,7 +51,7 @@ func dockerConfig(user *ptypes.GetAuthenticatedUserResponse, client *api.Client,
 }
 
 func setDockerConfig(client *api.Client) error {
-	pID := config.Project
+	pID := cliConf.Project
 
 	// get all registries that should be added
 	regToAdd := make([]string, 0)
@@ -146,7 +147,7 @@ func setDockerConfig(client *api.Client) error {
 		Filename: dockerConfigFile,
 	}
 
-	err = json.Unmarshal(configBytes, config)
+	err = json.Unmarshal(configBytes, config.GetCLIConfig())
 
 	if err != nil {
 		return err
@@ -174,7 +175,7 @@ func setDockerConfig(client *api.Client) error {
 
 			if !isAuthenticated {
 				// get a dockerhub token from the Porter API
-				tokenResp, err := client.GetDockerhubAuthorizationToken(context.Background(), config.Project)
+				tokenResp, err := client.GetDockerhubAuthorizationToken(context.Background(), cliConf.Project)
 
 				if err != nil {
 					return err

+ 4 - 3
cli/cmd/errors.go

@@ -8,13 +8,14 @@ import (
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 )
 
 var ErrNotLoggedIn error = errors.New("You are not logged in.")
 var ErrCannotConnect error = errors.New("Unable to connect to the Porter server.")
 
 func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error) error {
-	client := GetAPIClient(config)
+	client := config.GetAPIClient()
 
 	user, err := client.AuthCheck(context.Background())
 
@@ -25,7 +26,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			red.Print("You are not logged in. Log in using \"porter auth login\"\n")
 			return ErrNotLoggedIn
 		} else if strings.Contains(err.Error(), "connection refused") {
-			red.Printf("Unable to connect to the Porter server at %s\n", config.Host)
+			red.Printf("Unable to connect to the Porter server at %s\n", cliConf.Host)
 			red.Print("To set a different host, run \"porter config set-host [HOST]\"\n")
 			red.Print("To start a local server, run \"porter server start\"\n")
 			return ErrCannotConnect
@@ -44,7 +45,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			red.Print("You do not have the necessary permissions to view this resource")
 			return nil
 		} else if strings.Contains(err.Error(), "connection refused") {
-			red.Printf("Unable to connect to the Porter server at %s\n", config.Host)
+			red.Printf("Unable to connect to the Porter server at %s\n", cliConf.Host)
 			red.Print("To set a different host, run \"porter config set-host [HOST]\"")
 			red.Print("To start a local server, run \"porter server start\"")
 			return nil

+ 2 - 2
cli/cmd/get.go

@@ -72,7 +72,7 @@ type getReleaseInfo struct {
 }
 
 func get(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	rel, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, args[0])
+	rel, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0])
 
 	if err != nil {
 		return err
@@ -112,7 +112,7 @@ func get(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 }
 
 func getValues(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	rel, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, args[0])
+	rel, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0])
 
 	if err != nil {
 		return err

+ 4 - 4
cli/cmd/job.go

@@ -135,8 +135,8 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 
 	return client.UpdateBatchImage(
 		context.TODO(),
-		config.Project,
-		config.Cluster,
+		cliConf.Project,
+		cliConf.Cluster,
 		namespace,
 		&types.UpdateImageBatchRequest{
 			ImageRepoURI: imageRepoURI,
@@ -148,7 +148,7 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 // waits for a job with a given name/namespace
 func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 	// get the job release
-	jobRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, name)
+	jobRelease, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, name)
 
 	if err != nil {
 		return err
@@ -177,7 +177,7 @@ func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 	for time.Now().Before(timeWait) {
 		// get the jobs for that job chart
-		jobs, err := client.GetJobs(context.Background(), config.Project, config.Cluster, namespace, name)
+		jobs, err := client.GetJobs(context.Background(), cliConf.Project, cliConf.Cluster, namespace, name)
 
 		if err != nil {
 			return err

+ 4 - 3
cli/cmd/open.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 
 	"github.com/spf13/cobra"
@@ -13,14 +14,14 @@ var openCmd = &cobra.Command{
 	Use:   "open",
 	Short: "Opens the browser at the currently set Porter instance",
 	Run: func(cmd *cobra.Command, args []string) {
-		client := GetAPIClient(config)
+		client := config.GetAPIClient()
 
 		user, err := client.AuthCheck(context.Background())
 
 		if err == nil {
-			utils.OpenBrowser(fmt.Sprintf("%s/login?email=%s", config.Host, user.Email))
+			utils.OpenBrowser(fmt.Sprintf("%s/login?email=%s", cliConf.Host, user.Email))
 		} else {
-			utils.OpenBrowser(fmt.Sprintf("%s/register", config.Host))
+			utils.OpenBrowser(fmt.Sprintf("%s/register", cliConf.Host))
 		}
 	},
 }

+ 301 - 0
cli/cmd/preview/build_driver.go

@@ -0,0 +1,301 @@
+package preview
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/cli/cli/git"
+	"github.com/docker/distribution/reference"
+	"github.com/mitchellh/mapstructure"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/deploy"
+	"github.com/porter-dev/porter/cli/cmd/docker"
+	"github.com/porter-dev/switchboard/pkg/drivers"
+	"github.com/porter-dev/switchboard/pkg/models"
+)
+
+type BuildDriverConfig struct {
+	Build struct {
+		ForceBuild bool `mapstructure:"force_build"`
+		UseCache   bool `mapstructure:"use_cache"`
+		Method     string
+		Context    string
+		Dockerfile string
+		Builder    string
+		Buildpacks []string
+		Image      string
+	}
+
+	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
+
+	Values map[string]interface{}
+}
+
+type BuildDriver struct {
+	source      *Source
+	target      *Target
+	config      *BuildDriverConfig
+	lookupTable *map[string]drivers.Driver
+	output      map[string]interface{}
+}
+
+func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	driver := &BuildDriver{
+		lookupTable: opts.DriverLookupTable,
+		output:      make(map[string]interface{}),
+	}
+
+	source, err := GetSource(resource.Source)
+	if err != nil {
+		return nil, err
+	}
+
+	driver.source = source
+
+	target, err := GetTarget(resource.Target)
+	if err != nil {
+		return nil, err
+	}
+
+	driver.target = target
+
+	buildDriverConfig, err := driver.getConfig(resource)
+	if err != nil {
+		return nil, err
+	}
+
+	driver.config = buildDriverConfig
+
+	return driver, nil
+}
+
+func (d *BuildDriver) ShouldApply(resource *models.Resource) bool {
+	return true
+}
+
+func (d *BuildDriver) Apply(resource *models.Resource) (*models.Resource, error) {
+	client := config.GetAPIClient()
+
+	tag := os.Getenv("PORTER_TAG")
+
+	if tag == "" {
+		commit, err := git.LastCommit()
+
+		if err != nil {
+			return nil, err
+		}
+
+		tag = commit.Sha[:7]
+	}
+
+	// if the method is registry and a tag is defined, we use the provided tag
+	if d.config.Build.Method == "registry" {
+		imageSpl := strings.Split(d.config.Build.Image, ":")
+
+		if len(imageSpl) == 2 {
+			tag = imageSpl[1]
+		}
+
+		if tag == "" {
+			tag = "latest"
+		}
+	}
+
+	regList, err := client.ListRegistries(context.Background(), d.target.Project)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var registryURL string
+
+	if len(*regList) == 0 {
+		return nil, fmt.Errorf("no registry found")
+	} else {
+		registryURL = (*regList)[0].URL
+	}
+
+	var repoSuffix string
+
+	if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
+		if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
+			repoSuffix = strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-")
+		}
+	}
+
+	createAgent := &deploy.CreateAgent{
+		Client: client,
+		CreateOpts: &deploy.CreateOpts{
+			SharedOpts: &deploy.SharedOpts{
+				ProjectID:       d.target.Project,
+				ClusterID:       d.target.Cluster,
+				OverrideTag:     tag,
+				Namespace:       d.target.Namespace,
+				LocalPath:       d.config.Build.Context,
+				LocalDockerfile: d.config.Build.Dockerfile,
+				Method:          deploy.DeployBuildType(d.config.Build.Method),
+				EnvGroups:       d.config.EnvGroups,
+				UseCache:        d.config.Build.UseCache,
+			},
+			Kind:        d.source.Name,
+			ReleaseName: d.target.AppName,
+			RegistryURL: registryURL,
+			RepoSuffix:  repoSuffix,
+		},
+	}
+
+	_, imageURL, err := createAgent.GetImageRepoURL(d.target.AppName, d.target.Namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	if d.config.Build.Method != "" {
+		if d.config.Build.Method == string(deploy.DeployBuildTypeDocker) {
+			if d.config.Build.Dockerfile == "" {
+				hasDockerfile := createAgent.HasDefaultDockerfile(d.config.Build.Context)
+
+				if !hasDockerfile {
+					return nil, fmt.Errorf("dockerfile not found")
+				}
+
+				d.config.Build.Dockerfile = "Dockerfile"
+			}
+		}
+	} else {
+		// try to detect dockerfile, otherwise fall back to `pack`
+		hasDockerfile := createAgent.HasDefaultDockerfile(d.config.Build.Context)
+
+		if !hasDockerfile {
+			d.config.Build.Method = string(deploy.DeployBuildTypePack)
+		} else {
+			d.config.Build.Method = string(deploy.DeployBuildTypeDocker)
+			d.config.Build.Dockerfile = "Dockerfile"
+		}
+	}
+
+	// create docker agent
+	agent, err := docker.NewAgentWithAuthGetter(client, d.target.Project)
+
+	if err != nil {
+		return nil, err
+	}
+
+	imageExists := agent.CheckIfImageExists(imageURL, tag) // FIXME: does not seem to work with gcr.io images
+
+	fmt.Printf("imageExists: %v\n", imageExists)
+	fmt.Printf("force_build: %v\n", d.config.Build.ForceBuild)
+
+	if imageExists && tag != "latest" && !d.config.Build.ForceBuild {
+		fmt.Printf("%s:%s already exists in the registry, so skipping build\n", imageURL, tag)
+	} else {
+		_, mergedValues, err := createAgent.GetMergedValues(d.config.Values)
+
+		if err != nil {
+			return nil, err
+		}
+
+		fmt.Printf("mergedValues: %v\n", mergedValues)
+
+		env, err := deploy.GetEnvForRelease(
+			client,
+			mergedValues,
+			d.target.Project,
+			d.target.Cluster,
+			d.target.Namespace,
+		)
+
+		if err != nil {
+			env = map[string]string{}
+		}
+
+		fmt.Printf("env: %v\n", env)
+
+		buildAgent := &deploy.BuildAgent{
+			SharedOpts:  createAgent.CreateOpts.SharedOpts,
+			APIClient:   client,
+			ImageRepo:   imageURL,
+			Env:         env,
+			ImageExists: false,
+		}
+
+		if d.config.Build.Method == string(deploy.DeployBuildTypeDocker) {
+			basePath, err := filepath.Abs(".")
+
+			if err != nil {
+				return nil, err
+			}
+
+			err = buildAgent.BuildDocker(
+				agent,
+				basePath,
+				d.config.Build.Context,
+				d.config.Build.Dockerfile,
+				tag,
+				"",
+			)
+		} else {
+			var buildConfig *types.BuildConfig
+
+			if d.config.Build.Builder != "" {
+				buildConfig = &types.BuildConfig{
+					Builder:    d.config.Build.Builder,
+					Buildpacks: d.config.Build.Buildpacks,
+				}
+			}
+
+			err = buildAgent.BuildPack(
+				agent,
+				d.config.Build.Context,
+				tag,
+				"",
+				buildConfig,
+			)
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	named, _ := reference.ParseNamed(imageURL)
+	domain := reference.Domain(named)
+	imageRepo := reference.Path(named)
+
+	d.output["registry_url"] = domain
+	d.output["image_repo"] = imageRepo
+	d.output["image_tag"] = tag
+
+	fmt.Println(d.output)
+
+	return resource, nil
+}
+
+func (d *BuildDriver) Output() (map[string]interface{}, error) {
+	return d.output, nil
+}
+
+func (d *BuildDriver) getConfig(resource *models.Resource) (*BuildDriverConfig, error) {
+	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
+		RawConf:      resource.Config,
+		LookupTable:  *d.lookupTable,
+		Dependencies: resource.Dependencies,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	config := &BuildDriverConfig{}
+
+	err = mapstructure.Decode(populatedConf, config)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return config, nil
+}

+ 28 - 0
cli/cmd/preview/push_driver.go

@@ -0,0 +1,28 @@
+package preview
+
+import (
+	"github.com/porter-dev/switchboard/pkg/drivers"
+	"github.com/porter-dev/switchboard/pkg/models"
+)
+
+type PushDriver struct {
+	output map[string]interface{}
+}
+
+func NewPushDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	return &PushDriver{
+		output: make(map[string]interface{}),
+	}, nil
+}
+
+func (d *PushDriver) ShouldApply(resource *models.Resource) bool {
+	return true
+}
+
+func (d *PushDriver) Apply(resource *models.Resource) (*models.Resource, error) {
+	return resource, nil
+}
+
+func (d *PushDriver) Output() (map[string]interface{}, error) {
+	return d.output, nil
+}

+ 28 - 0
cli/cmd/preview/upgrade_driver.go

@@ -0,0 +1,28 @@
+package preview
+
+import (
+	"github.com/porter-dev/switchboard/pkg/drivers"
+	"github.com/porter-dev/switchboard/pkg/models"
+)
+
+type UpgradeDriver struct {
+	output map[string]interface{}
+}
+
+func NewUpgradeDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	return &UpgradeDriver{
+		output: make(map[string]interface{}),
+	}, nil
+}
+
+func (d *UpgradeDriver) ShouldApply(resource *models.Resource) bool {
+	return true
+}
+
+func (d *UpgradeDriver) Apply(resource *models.Resource) (*models.Resource, error) {
+	return resource, nil
+}
+
+func (d *UpgradeDriver) Output() (map[string]interface{}, error) {
+	return d.output, nil
+}

+ 203 - 0
cli/cmd/preview/utils.go

@@ -0,0 +1,203 @@
+package preview
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+)
+
+type Source struct {
+	Name          string
+	Repo          string
+	Version       string
+	IsApplication bool
+	SourceValues  map[string]interface{}
+}
+
+type Target struct {
+	AppName   string
+	Project   uint
+	Cluster   uint
+	Namespace string
+}
+
+func GetSource(input map[string]interface{}) (*Source, error) {
+	output := &Source{}
+
+	// first read from env vars
+	output.Name = os.Getenv("PORTER_SOURCE_NAME")
+	output.Repo = os.Getenv("PORTER_SOURCE_REPO")
+	output.Version = os.Getenv("PORTER_SOURCE_VERSION")
+
+	// next, check for values in the YAML file
+	if output.Name == "" {
+		if name, ok := input["name"]; ok {
+			nameVal, ok := name.(string)
+			if !ok {
+				return nil, fmt.Errorf("invalid name provided")
+			}
+			output.Name = nameVal
+		}
+	}
+
+	if output.Name == "" {
+		return nil, fmt.Errorf("source name required")
+	}
+
+	if output.Repo == "" {
+		if repo, ok := input["repo"]; ok {
+			repoVal, ok := repo.(string)
+			if !ok {
+				return nil, fmt.Errorf("invalid repo provided")
+			}
+			output.Repo = repoVal
+		}
+	}
+
+	if output.Version == "" {
+		if version, ok := input["version"]; ok {
+			versionVal, ok := version.(string)
+			if !ok {
+				return nil, fmt.Errorf("invalid version provided")
+			}
+			output.Version = versionVal
+		}
+	}
+
+	// lastly, just put in the defaults
+	if output.Version == "" {
+		output.Version = "latest"
+	}
+
+	output.IsApplication = output.Repo == "https://charts.getporter.dev"
+
+	if output.Repo == "" {
+		output.Repo = "https://charts.getporter.dev"
+
+		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+
+		if err == nil {
+			// found in "https://charts.getporter.dev"
+			output.SourceValues = values
+			output.IsApplication = true
+			return output, nil
+		}
+
+		output.Repo = "https://chart-addons.getporter.dev"
+
+		values, err = existsInRepo(output.Name, output.Version, output.Repo)
+
+		if err == nil {
+			// found in https://chart-addons.getporter.dev
+			output.SourceValues = values
+			return output, nil
+		}
+
+		return nil, fmt.Errorf("source does not exist in any repo")
+	} else {
+		// we look in the passed-in repo
+		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+
+		if err == nil {
+			output.SourceValues = values
+			return output, nil
+		}
+	}
+
+	return nil, fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
+}
+
+func GetTarget(input map[string]interface{}) (*Target, error) {
+	output := &Target{}
+
+	// first read from env vars
+	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
+		project, err := strconv.Atoi(projectEnv)
+		if err != nil {
+			return nil, err
+		}
+		output.Project = uint(project)
+	}
+
+	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
+		cluster, err := strconv.Atoi(clusterEnv)
+		if err != nil {
+			return nil, err
+		}
+		output.Cluster = uint(cluster)
+	}
+
+	output.Namespace = os.Getenv("PORTER_NAMESPACE")
+
+	// next, check for values in the YAML file
+	if output.Project == 0 {
+		if project, ok := input["project"]; ok {
+			projectVal, ok := project.(uint)
+			if !ok {
+				return nil, fmt.Errorf("project value must be an integer")
+			}
+			output.Project = projectVal
+		}
+	}
+
+	if output.Cluster == 0 {
+		if cluster, ok := input["cluster"]; ok {
+			clusterVal, ok := cluster.(uint)
+			if !ok {
+				return nil, fmt.Errorf("cluster value must be an integer")
+			}
+			output.Cluster = clusterVal
+		}
+	}
+
+	if output.Namespace == "" {
+		if namespace, ok := input["namespace"]; ok {
+			namespaceVal, ok := namespace.(string)
+			if !ok {
+				return nil, fmt.Errorf("invalid namespace provided")
+			}
+			output.Namespace = namespaceVal
+		}
+	}
+
+	if appName, ok := input["app_name"]; ok {
+		appNameVal, ok := appName.(string)
+		if !ok {
+			return nil, fmt.Errorf("invalid app_name provided")
+		}
+		output.AppName = appNameVal
+	}
+
+	// lastly, just put in the defaults
+	if output.Project == 0 {
+		output.Project = config.GetCLIConfig().Project
+	}
+	if output.Cluster == 0 {
+		output.Cluster = config.GetCLIConfig().Cluster
+	}
+	if output.Namespace == "" {
+		output.Namespace = "default"
+	}
+
+	return output, nil
+}
+
+func existsInRepo(name, version, url string) (map[string]interface{}, error) {
+	chart, err := config.GetAPIClient().GetTemplate(
+		context.Background(),
+		name, version,
+		&types.GetTemplateRequest{
+			TemplateGetBaseRequest: types.TemplateGetBaseRequest{
+				RepoURL: url,
+			},
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+	return chart.Values, nil
+}

+ 31 - 0
cli/cmd/preview/uuid_driver.go

@@ -0,0 +1,31 @@
+package preview
+
+import (
+	"github.com/google/uuid"
+	"github.com/porter-dev/switchboard/pkg/drivers"
+	"github.com/porter-dev/switchboard/pkg/models"
+)
+
+type UUIDDriver struct {
+	output map[string]interface{}
+}
+
+func NewUUIDDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	return &BuildDriver{
+		output: make(map[string]interface{}),
+	}, nil
+}
+
+func (d *UUIDDriver) ShouldApply(resource *models.Resource) bool {
+	return true
+}
+
+func (d *UUIDDriver) Apply(resource *models.Resource) (*models.Resource, error) {
+	d.output["uuid"] = uuid.NewString()
+
+	return resource, nil
+}
+
+func (d *UUIDDriver) Output() (map[string]interface{}, error) {
+	return d.output, nil
+}

+ 3 - 3
cli/cmd/project.go

@@ -80,7 +80,7 @@ func createProject(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 
 	color.New(color.FgGreen).Printf("Created project with name %s and id %d\n", args[0], resp.ID)
 
-	return config.SetProject(resp.ID)
+	return cliConf.SetProject(resp.ID)
 }
 
 func listProjects(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
@@ -97,7 +97,7 @@ func listProjects(user *types.GetAuthenticatedUserResponse, client *api.Client,
 
 	fmt.Fprintf(w, "%s\t%s\n", "ID", "NAME")
 
-	currProjectID := config.Project
+	currProjectID := cliConf.Project
 
 	for _, project := range projects {
 		if currProjectID == project.ID {
@@ -154,7 +154,7 @@ func setProjectCluster(client *api.Client, projectID uint) error {
 	clusters := *resp
 
 	if len(clusters) > 0 {
-		config.SetCluster(clusters[0].ID)
+		cliConf.SetCluster(clusters[0].ID)
 	}
 
 	return nil

+ 8 - 8
cli/cmd/registry.go

@@ -88,7 +88,7 @@ var registryImageListCmd = &cobra.Command{
 func init() {
 	rootCmd.AddCommand(registryCmd)
 
-	registryCmd.PersistentFlags().AddFlagSet(registryFlagSet)
+	registryCmd.PersistentFlags().AddFlagSet(utils.RegistryFlagSet)
 
 	registryCmd.AddCommand(registryReposCmd)
 	registryCmd.AddCommand(registryListCmd)
@@ -101,7 +101,7 @@ func init() {
 }
 
 func listRegistries(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	pID := config.Project
+	pID := cliConf.Project
 
 	// get the list of namespaces
 	resp, err := client.ListRegistries(
@@ -120,7 +120,7 @@ func listRegistries(user *types.GetAuthenticatedUserResponse, client *api.Client
 
 	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "URL", "SERVICE")
 
-	currRegistryID := config.Registry
+	currRegistryID := cliConf.Registry
 
 	for _, registry := range registries {
 		if currRegistryID == registry.ID {
@@ -155,7 +155,7 @@ func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client
 			return err
 		}
 
-		err = client.DeleteProjectRegistry(context.Background(), config.Project, uint(id))
+		err = client.DeleteProjectRegistry(context.Background(), cliConf.Project, uint(id))
 
 		if err != nil {
 			return err
@@ -168,8 +168,8 @@ func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client
 }
 
 func listRepos(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	pID := config.Project
-	rID := config.Registry
+	pID := cliConf.Project
+	rID := cliConf.Registry
 
 	// get the list of namespaces
 	resp, err := client.ListRegistryRepositories(
@@ -199,8 +199,8 @@ func listRepos(user *types.GetAuthenticatedUserResponse, client *api.Client, arg
 }
 
 func listImages(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	pID := config.Project
-	rID := config.Registry
+	pID := cliConf.Project
+	rID := cliConf.Registry
 	repoName := args[0]
 
 	// get the list of namespaces

+ 4 - 11
cli/cmd/root.go

@@ -11,7 +11,8 @@ import (
 	"github.com/Masterminds/semver/v3"
 	"github.com/fatih/color"
 	"github.com/google/go-github/v41/github"
-	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"k8s.io/client-go/util/homedir"
 )
@@ -30,7 +31,7 @@ var home = homedir.HomeDir()
 func Execute() {
 	Setup()
 
-	rootCmd.PersistentFlags().AddFlagSet(defaultFlagSet)
+	rootCmd.PersistentFlags().AddFlagSet(utils.DefaultFlagSet)
 
 	if Version != "dev" {
 		ghClient := github.NewClient(nil)
@@ -65,13 +66,5 @@ func Execute() {
 }
 
 func Setup() {
-	InitAndLoadConfig()
-}
-
-func GetAPIClient(config *CLIConfig) *api.Client {
-	if token := config.Token; token != "" {
-		return api.NewClientWithToken(config.Host+"/api", token)
-	}
-
-	return api.NewClient(config.Host+"/api", "cookie.json")
+	config.InitAndLoadConfig()
 }

+ 4 - 4
cli/cmd/run.go

@@ -261,8 +261,8 @@ type PorterRunSharedConfig struct {
 }
 
 func (p *PorterRunSharedConfig) setSharedConfig() error {
-	pID := config.Project
-	cID := config.Cluster
+	pID := cliConf.Project
+	cID := cliConf.Cluster
 
 	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
 
@@ -318,8 +318,8 @@ type podSimple struct {
 }
 
 func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, error) {
-	pID := config.Project
-	cID := config.Cluster
+	pID := cliConf.Project
+	cID := cliConf.Cluster
 
 	resp, err := client.GetK8sAllPods(context.TODO(), pID, cID, namespace, releaseName)
 

+ 8 - 7
cli/cmd/server.go

@@ -10,6 +10,7 @@ import (
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/docker"
 	"github.com/porter-dev/porter/cli/cmd/github"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 
 	"github.com/spf13/cobra"
 )
@@ -34,8 +35,8 @@ var startCmd = &cobra.Command{
 	Use:   "start",
 	Short: "Starts a Porter server instance on the host",
 	Run: func(cmd *cobra.Command, args []string) {
-		if config.Driver == "docker" {
-			config.SetDriver("docker")
+		if cliConf.Driver == "docker" {
+			cliConf.SetDriver("docker")
 
 			err := startDocker(
 				opts.imageTag,
@@ -57,7 +58,7 @@ var startCmd = &cobra.Command{
 				os.Exit(1)
 			}
 		} else {
-			config.SetDriver("local")
+			cliConf.SetDriver("local")
 			err := startLocal(
 				opts.db,
 				*opts.port,
@@ -76,7 +77,7 @@ var stopCmd = &cobra.Command{
 	Use:   "stop",
 	Short: "Stops a Porter instance running on the Docker engine",
 	Run: func(cmd *cobra.Command, args []string) {
-		if config.Driver == "docker" {
+		if cliConf.Driver == "docker" {
 			if err := stopDocker(); err != nil {
 				color.New(color.FgRed).Println("Shutdown unsuccessful:", err.Error())
 				os.Exit(1)
@@ -91,7 +92,7 @@ func init() {
 	serverCmd.AddCommand(startCmd)
 	serverCmd.AddCommand(stopCmd)
 
-	serverCmd.PersistentFlags().AddFlagSet(driverFlagSet)
+	serverCmd.PersistentFlags().AddFlagSet(utils.DriverFlagSet)
 
 	startCmd.PersistentFlags().StringVar(
 		&opts.db,
@@ -152,7 +153,7 @@ func startDocker(
 
 	green.Printf("Server ready: listening on localhost:%d\n", port)
 
-	return config.SetHost(fmt.Sprintf("http://localhost:%d", port))
+	return cliConf.SetHost(fmt.Sprintf("http://localhost:%d", port))
 }
 
 func startLocal(
@@ -163,7 +164,7 @@ func startLocal(
 		return fmt.Errorf("postgres not available for local driver, run \"porter server start --db postgres --driver docker\"")
 	}
 
-	config.SetHost(fmt.Sprintf("http://localhost:%d", port))
+	cliConf.SetHost(fmt.Sprintf("http://localhost:%d", port))
 
 	porterDir := filepath.Join(home, ".porter")
 	cmdPath := filepath.Join(home, ".porter", "portersvr")

+ 9 - 0
cli/cmd/utils/flags.go

@@ -0,0 +1,9 @@
+package utils
+
+import flag "github.com/spf13/pflag"
+
+// shared sets of flags used by multiple commands
+var DriverFlagSet = flag.NewFlagSet("driver", flag.ExitOnError)
+var DefaultFlagSet = flag.NewFlagSet("shared", flag.ExitOnError) // used by all commands
+var RegistryFlagSet = flag.NewFlagSet("registry", flag.ExitOnError)
+var HelmRepoFlagSet = flag.NewFlagSet("helmrepo", flag.ExitOnError)

+ 5 - 5
cmd/docker-credential-porter/helper/helper.go

@@ -2,7 +2,7 @@ package helper
 
 import (
 	"github.com/docker/docker-credential-helpers/credentials"
-	"github.com/porter-dev/porter/cli/cmd"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/docker"
 )
 
@@ -18,16 +18,16 @@ type PorterHelper struct {
 
 func NewPorterHelper(debug bool) *PorterHelper {
 	// get the current project ID
-	config := cmd.InitAndLoadNewConfig()
+	cliConfig := config.InitAndLoadNewConfig()
 	cache := docker.NewFileCredentialsCache()
 
 	return &PorterHelper{
 		Debug:     debug,
-		ProjectID: config.Project,
+		ProjectID: cliConfig.Project,
 		AuthGetter: &docker.AuthGetter{
-			Client:    cmd.GetAPIClient(config),
+			Client:    config.GetAPIClient(),
 			Cache:     cache,
-			ProjectID: config.Project,
+			ProjectID: cliConfig.Project,
 		},
 		Cache: cache,
 	}