소스 검색

cli major update

Alexander Belanger 5 년 전
부모
커밋
f1ac0e4b69

+ 1 - 1
.air.toml

@@ -7,7 +7,7 @@ tmp_dir = "tmp"
 
 [build]
 # Just plain old shell command. You could use `make` as well.
-cmd = "go build -o ./tmp/migrate ./cmd/migrate; go build -o ./tmp/app ./cmd/app"
+cmd = "go build -o ./tmp/ready ./cmd/ready; go build -o ./tmp/migrate ./cmd/migrate; go build -o ./tmp/app ./cmd/app"
 # Binary file yields from `cmd`.
 bin = "tmp/migrate; tmp/app"
 # Customize binary.

+ 0 - 2
cli/cmd/api/helper_test.go

@@ -42,8 +42,6 @@ func startPorterServerWithDocker(processID string, port int, db docker.PorterDB)
 		ServerImageTag: "testing",
 		ServerPort:     port,
 		DB:             db,
-		KubeconfigPath: "",
-		SkipKubeconfig: true,
 		Env:            env,
 	}
 

+ 153 - 0
cli/cmd/auth.go

@@ -0,0 +1,153 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+)
+
+var authCmd = &cobra.Command{
+	Use:   "auth",
+	Short: "Commands for authenticating to a Porter server",
+}
+
+var loginCmd = &cobra.Command{
+	Use:   "login",
+	Short: "Authorizes a user for a given Porter server",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := login()
+
+		if err != nil {
+			fmt.Println("Error logging in:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
+var registerCmd = &cobra.Command{
+	Use:   "register",
+	Short: "Creates a user for a given Porter server",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := register()
+
+		if err != nil {
+			fmt.Println("Error registering:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
+var logoutCmd = &cobra.Command{
+	Use:   "logout",
+	Short: "Logs a user out of a given Porter server",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := logout()
+
+		if err != nil {
+			fmt.Println("Error logging out:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(authCmd)
+
+	authCmd.AddCommand(loginCmd)
+	authCmd.AddCommand(registerCmd)
+	authCmd.AddCommand(logoutCmd)
+
+	authCmd.PersistentFlags().StringVar(
+		&host,
+		"host",
+		getHost(),
+		"host url of Porter instance",
+	)
+}
+
+func login() error {
+	host := getHost()
+	var username, pw string
+
+	fmt.Println("Please log in with an email and password:")
+
+	username, err := utils.PromptPlaintext("Email: ")
+
+	if err != nil {
+		return err
+	}
+
+	pw, err = utils.PromptPassword("Password: ")
+
+	if err != nil {
+		return err
+	}
+
+	client := api.NewClient(host+"/api", "cookie.json")
+
+	_, err = client.Login(context.Background(), &api.LoginRequest{
+		Email:    username,
+		Password: pw,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("Successfully logged in!")
+
+	return nil
+}
+
+func register() error {
+	host := getHost()
+
+	fmt.Println("Please register your admin account with an email and password:")
+
+	username, err := utils.PromptPlaintext("Email: ")
+
+	if err != nil {
+		return err
+	}
+
+	pw, err := utils.PromptPasswordWithConfirmation()
+
+	if err != nil {
+		return err
+	}
+
+	client := api.NewClient(host+"/api", "cookie.json")
+
+	resp, err := client.CreateUser(context.Background(), &api.CreateUserRequest{
+		Email:    username,
+		Password: pw,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("Created user with email %s and id %d\n", username, resp.ID)
+
+	return nil
+}
+
+func logout() error {
+	host := getHost()
+
+	client := api.NewClient(host+"/api", "cookie.json")
+
+	err := client.Logout(context.Background())
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("Successfully logged out")
+
+	return nil
+}

+ 91 - 0
cli/cmd/config.go

@@ -0,0 +1,91 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+// a set of shared flags
+var (
+	host      string
+	projectID uint
+)
+
+var configCmd = &cobra.Command{
+	Use:   "config",
+	Short: "Commands that control local configuration settings",
+}
+
+var setProjectCmd = &cobra.Command{
+	Use:   "set-project [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Saves the project id in the default configuration",
+	Run: func(cmd *cobra.Command, args []string) {
+		projID, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			fmt.Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+
+		err = setProject(uint(projID))
+
+		if err != nil {
+			fmt.Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
+}
+
+var setHostCmd = &cobra.Command{
+	Use:   "set-host [host]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Saves the host in the default configuration",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := setHost(args[0])
+
+		if err != nil {
+			fmt.Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(configCmd)
+
+	configCmd.AddCommand(setProjectCmd)
+	configCmd.AddCommand(setHostCmd)
+}
+
+func setProject(id uint) error {
+	viper.Set("project", id)
+	fmt.Printf("Set the current project id as %d\n", id)
+	return viper.WriteConfig()
+}
+
+func setHost(host string) error {
+	viper.Set("host", host)
+	fmt.Printf("Set the current host as %s\n", host)
+	return viper.WriteConfig()
+}
+
+func getHost() string {
+	if host != "" {
+		return host
+	}
+
+	return viper.GetString("host")
+}
+
+func getProjectID() uint {
+	if projectID != 0 {
+		return projectID
+	}
+
+	return viper.GetUint("project")
+}

+ 75 - 0
cli/cmd/connect.go

@@ -0,0 +1,75 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/porter-dev/porter/cli/cmd/connect"
+	"github.com/spf13/cobra"
+)
+
+var (
+	kubeconfigPath string
+	print          *bool
+	contexts       *[]string
+)
+
+var connectCmd = &cobra.Command{
+	Use:   "connect",
+	Short: "Commands that connect to external clusters and providers",
+}
+
+var connectKubeconfigCmd = &cobra.Command{
+	Use:   "kubeconfig",
+	Short: "Uses the local kubeconfig to connect to a cluster",
+	Run: func(cmd *cobra.Command, args []string) {
+		host := getHost()
+		projectID := getProjectID()
+
+		err := connect.Kubeconfig(
+			kubeconfigPath,
+			*contexts,
+			host,
+			projectID,
+		)
+
+		if err != nil {
+			fmt.Printf("Error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(connectCmd)
+
+	connectCmd.AddCommand(connectKubeconfigCmd)
+
+	connectCmd.PersistentFlags().StringVar(
+		&host,
+		"host",
+		getHost(),
+		"host url of Porter instance",
+	)
+
+	projectID = *connectCmd.PersistentFlags().UintP(
+		"project-id",
+		"p",
+		getProjectID(),
+		"project id to use",
+	)
+
+	connectKubeconfigCmd.PersistentFlags().StringVarP(
+		&kubeconfigPath,
+		"kubeconfig",
+		"k",
+		"",
+		"path to kubeconfig",
+	)
+
+	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
+		"contexts",
+		nil,
+		"the list of contexts to connect (defaults to the current context)",
+	)
+}

+ 81 - 70
cli/cmd/setconfig.go → cli/cmd/connect/kubeconfig.go

@@ -1,74 +1,37 @@
-package cmd
+package connect
 
 import (
 	"context"
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"os"
+	"strings"
 
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/internal/kubernetes/local"
 	gcpLocal "github.com/porter-dev/porter/internal/providers/gcp/local"
-	"github.com/porter-dev/porter/internal/utils"
-
-	"github.com/spf13/viper"
 
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/spf13/cobra"
 )
 
-var setConfigCmd = &cobra.Command{
-	Use:   "set-config",
-	Short: "Uses the local kubeconfig to set the configuration for a cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := setConfig()
-
-		if err != nil {
-			fmt.Printf("Error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(setConfigCmd)
-
-	setConfigCmd.PersistentFlags().StringVarP(
-		&kubeconfigPath,
-		"kubeconfig",
-		"k",
-		"",
-		"path to kubeconfig",
-	)
-
-	setConfigCmd.PersistentFlags().StringVar(
-		&host,
-		"host",
-		"http://localhost:10000",
-		"host url of Porter instance",
-	)
-
-	contexts = setConfigCmd.PersistentFlags().StringArray(
-		"contexts",
-		nil,
-		"the list of contexts to use (defaults to the current context)",
-	)
-}
-
-func setConfig() error {
-	// TODO: construct the kubeconfig based on the passed contexts
-
-	// get the current project ID
-	projectID := viper.GetUint("project")
-
+// Kubeconfig creates a service account for a project by parsing the local
+// kubeconfig and resolving actions that must be performed.
+func Kubeconfig(
+	kubeconfigPath string,
+	contexts []string,
+	host string,
+	projectID 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]")
 	}
 
 	// get the kubeconfig
-	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, *contexts)
+	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
 
 	if err != nil {
 		return err
@@ -135,7 +98,10 @@ func setConfig() error {
 
 				resolvers = append(resolvers, resolveAction)
 			case models.GCPKeyDataAction:
-				resolveAction, err := resolveGCPKeyAction(saCandidate.ClusterEndpoint)
+				resolveAction, err := resolveGCPKeyAction(
+					saCandidate.ClusterEndpoint,
+					saCandidate.ClusterName,
+				)
 
 				if err != nil {
 					return err
@@ -164,7 +130,7 @@ func setConfig() error {
 			// namespaces, err := client.GetK8sNamespaces(
 			// 	context.Background(),
 			// 	projectID,
-			// 	saCandidate.ID,
+			// 	sa.ID,
 			// 	cluster.ID,
 			// )
 
@@ -262,42 +228,87 @@ func resolveTokenDataAction(
 }
 
 // resolves a gcp key data action
-func resolveGCPKeyAction(endpoint string) (*models.ServiceAccountAllActions, error) {
-	agent, _ := gcpLocal.NewDefaultAgent()
-	projID, err := agent.GetProjectIDForGKECluster(endpoint)
+func resolveGCPKeyAction(endpoint string, clusterName string) (*models.ServiceAccountAllActions, error) {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Detected GKE cluster in kubeconfig for the endpoint %s (%s). 
+Porter can set up a service account in your GCP project to connect to this cluster automatically.
+Would you like to proceed? [y/n] `,
+			endpoint,
+			clusterName,
+		),
+	)
 
 	if err != nil {
 		return nil, err
 	}
 
-	agent.ProjectID = projID
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		agent, _ := gcpLocal.NewDefaultAgent()
+		projID, err := agent.GetProjectIDForGKECluster(endpoint)
 
-	name := "porter-dashboard-" + utils.StringWithCharset(6, "abcdefghijklmnopqrstuvwxyz1234567890")
+		if err != nil {
+			return nil, err
+		}
 
-	// create the service account and give it the correct iam permissions
-	resp, err := agent.CreateServiceAccount(name)
+		agent.ProjectID = projID
 
-	if err != nil {
-		return nil, err
+		name := "porter-dashboard-" + utils.StringWithCharset(6, "abcdefghijklmnopqrstuvwxyz1234567890")
+
+		// create the service account and give it the correct iam permissions
+		resp, err := agent.CreateServiceAccount(name)
+
+		if err != nil {
+			fmt.Println("Automatic creation failed.")
+			return resolveGCPKeyActionManual(endpoint, clusterName)
+		}
+
+		err = agent.SetServiceAccountIAMPolicy(resp)
+
+		if err != nil {
+			return nil, err
+		}
+
+		// get the service account key data to send to the server
+		bytes, err := agent.CreateServiceAccountKey(resp)
+
+		if err != nil {
+			return nil, err
+		}
+
+		return &models.ServiceAccountAllActions{
+			Name:       models.GCPKeyDataAction,
+			GCPKeyData: string(bytes),
+		}, nil
 	}
 
-	err = agent.SetServiceAccountIAMPolicy(resp)
+	return resolveGCPKeyActionManual(endpoint, clusterName)
+}
+
+func resolveGCPKeyActionManual(endpoint string, clusterName string) (*models.ServiceAccountAllActions, error) {
+	keyFileLocation, err := utils.PromptPlaintext(`Please provide the full path to a service account key file.
+Key file location: `)
 
 	if err != nil {
 		return nil, err
 	}
 
-	// get the service account key data to send to the server
-	bytes, err := agent.CreateServiceAccountKey(resp)
+	// attempt to read the key file location
+	if info, err := os.Stat(keyFileLocation); !os.IsNotExist(err) && !info.IsDir() {
+		// read the file
+		bytes, err := ioutil.ReadFile(keyFileLocation)
 
-	if err != nil {
-		return nil, err
+		if err != nil {
+			return nil, err
+		}
+
+		return &models.ServiceAccountAllActions{
+			Name:       models.GCPKeyDataAction,
+			GCPKeyData: string(bytes),
+		}, nil
 	}
 
-	return &models.ServiceAccountAllActions{
-		Name:       models.GCPKeyDataAction,
-		GCPKeyData: string(bytes),
-	}, nil
+	return nil, errors.New("Key file not found")
 }
 
 // resolves an aws key data action

+ 0 - 36
cli/cmd/credstore/credstore.go

@@ -1,36 +0,0 @@
-package credstore
-
-import "github.com/docker/docker-credential-helpers/credentials"
-
-const (
-	url   = "https://github.com/porter-dev/porter"
-	label = "Porter Credentials"
-)
-
-// Set stores a given username/pw with a given credentials label in the OS-specific
-// credentials store
-func Set(username, pw string) error {
-	cr := &credentials.Credentials{
-		ServerURL: url,
-		Username:  username,
-		Secret:    pw,
-	}
-
-	credentials.SetCredsLabel(label)
-
-	return ns.Add(cr)
-}
-
-// Get retrieves a given username/pw with a given credentials label in the OS-specific
-// credentials store
-func Get() (string, string, error) {
-	credentials.SetCredsLabel(label)
-	return ns.Get(url)
-}
-
-// Del removes a given credential that uses a label in the OS-specific
-// credentials store
-func Del() error {
-	credentials.SetCredsLabel(label)
-	return ns.Delete(url)
-}

+ 0 - 5
cli/cmd/credstore/credstore_darwin.go

@@ -1,5 +0,0 @@
-package credstore
-
-import "github.com/docker/docker-credential-helpers/osxkeychain"
-
-var ns = osxkeychain.Osxkeychain{}

+ 0 - 5
cli/cmd/credstore/credstore_linux.go

@@ -1,5 +0,0 @@
-package credstore
-
-import "github.com/docker/docker-credential-helpers/pass"
-
-var ns = pass.Pass{}

+ 0 - 34
cli/cmd/credstore/credstore_test.go

@@ -1,34 +0,0 @@
-package credstore_test
-
-import (
-	"log"
-	"testing"
-
-	"github.com/porter-dev/porter/cli/cmd/credstore"
-)
-
-func TestSetGet(t *testing.T) {
-	credstore.Set("user", "password")
-
-	user, secret, err := credstore.Get()
-	if err == nil {
-		if user != "user" {
-			t.Errorf("Expecting user, got %s", user)
-		}
-
-		if secret != "password" {
-			t.Errorf("Expecting password, got %s", secret)
-		}
-	} else {
-		log.Println("got error:", err)
-	}
-
-	credstore.Del()
-
-	_, _, err = credstore.Get()
-
-	if err == nil {
-		t.Fatalf("Expecting an error, got nil")
-	}
-
-}

+ 0 - 5
cli/cmd/credstore/credstore_windows.go

@@ -1,5 +0,0 @@
-package credstore
-
-import "github.com/docker/docker-credential-helpers/wincred"
-
-var ns = wincred.Wincred{}

+ 5 - 2
cli/cmd/docker/agent.go

@@ -3,6 +3,7 @@ package docker
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -203,14 +204,16 @@ func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
 
 		health := cont.State.Health
 
-		if health == nil || health.Status == "healthy" || health.FailingStreak >= streak {
+		if health == nil || health.Status == "healthy" {
+			return nil
+		} else if health.FailingStreak >= streak {
 			break
 		}
 
 		time.Sleep(time.Second)
 	}
 
-	return nil
+	return errors.New("container not healthy")
 }
 
 // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //

+ 18 - 24
cli/cmd/docker/porter.go

@@ -2,7 +2,6 @@ package docker
 
 import (
 	"fmt"
-	"path/filepath"
 	"strings"
 	"time"
 
@@ -10,7 +9,6 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/go-connections/nat"
-	"k8s.io/client-go/util/homedir"
 )
 
 // PorterDB is used for enumerating DB types
@@ -28,18 +26,12 @@ type PorterStartOpts struct {
 	ServerImageTag string
 	ServerPort     int
 	DB             PorterDB
-	KubeconfigPath string
-	SkipKubeconfig bool
 	Env            []string
 }
 
 // StartPorter creates a new Docker agent using the host environment, and creates a
 // new Porter instance
 func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
-	home := homedir.HomeDir()
-	outputConfPath := filepath.Join(home, ".porter", "porter.kubeconfig")
-	containerConfPath := "/porter/porter.kubeconfig"
-
 	agent, err = NewAgentFromEnv()
 
 	if err != nil {
@@ -52,19 +44,6 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 	// the volumes passed to the Porter container
 	volumesMap := make(map[string]struct{})
 
-	if !opts.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)
-	}
-
 	netID, err := agent.CreateBridgeNetworkIfNotExist("porter_network_" + opts.ProcessID)
 
 	if err != nil {
@@ -133,8 +112,12 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 
 		pgID, err := agent.StartPostgresContainer(startOpts)
 
+		if err != nil {
+			return nil, "", err
+		}
+
 		fmt.Println("Waiting for postgres:latest to be healthy...")
-		agent.WaitForContainerHealthy(pgID, 10)
+		err = agent.WaitForContainerHealthy(pgID, 10)
 
 		if err != nil {
 			return nil, "", err
@@ -148,8 +131,6 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 			"DB_HOST=porter_postgres_" + opts.ProcessID,
 			"DB_PORT=5432",
 		}...)
-
-		defer agent.WaitForContainerStop(pgID)
 	}
 
 	// create Porter container
@@ -170,6 +151,13 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		return nil, "", err
 	}
 
+	fmt.Println("Waiting for porter to be healthy...")
+	err = agent.WaitForContainerHealthy(id, 10)
+
+	if err != nil {
+		return nil, "", err
+	}
+
 	return agent, id, nil
 }
 
@@ -264,6 +252,12 @@ func (a *Agent) pullAndCreatePorterContainer(opts PorterServerStartOpts) (id str
 		Labels:  labels,
 		Volumes: opts.VolumeMap,
 		Env:     opts.Env,
+		Healthcheck: &container.HealthConfig{
+			Test:     []string{"CMD-SHELL", "/porter/ready"},
+			Interval: 10 * time.Second,
+			Timeout:  5 * time.Second,
+			Retries:  3,
+		},
 	}, &container.HostConfig{
 		PortBindings: portBindings,
 		Mounts:       opts.Mounts,

+ 0 - 98
cli/cmd/generate.go

@@ -1,98 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"path/filepath"
-
-	"github.com/porter-dev/porter/internal/kubernetes/local"
-
-	"k8s.io/client-go/tools/clientcmd"
-	"k8s.io/client-go/util/homedir"
-
-	"github.com/spf13/cobra"
-)
-
-var (
-	outputFile     string
-	kubeconfigPath string
-	print          *bool
-	contexts       *[]string
-)
-
-// generateCmd represents the generate command
-var generateCmd = &cobra.Command{
-	Use:   "generate",
-	Short: "Generates a kubeconfig with certificate data added",
-	Run: func(cmd *cobra.Command, args []string) {
-		generate(kubeconfigPath, outputFile, *print, *contexts)
-	},
-}
-
-func init() {
-	home := homedir.HomeDir()
-
-	rootCmd.AddCommand(generateCmd)
-
-	generateCmd.PersistentFlags().StringVarP(
-		&outputFile,
-		"output",
-		"o",
-		filepath.Join(home, ".porter", "porter.kubeconfig"),
-		"output file location",
-	)
-
-	generateCmd.PersistentFlags().StringVarP(
-		&kubeconfigPath,
-		"kubeconfig",
-		"k",
-		"",
-		"path to kubeconfig",
-	)
-
-	contexts = generateCmd.PersistentFlags().StringArray(
-		"contexts",
-		nil,
-		"the list of contexts to use (defaults to the current context)",
-	)
-
-	print = generateCmd.PersistentFlags().BoolP(
-		"print",
-		"p",
-		false,
-		"print result to stdout, without writing to the fs",
-	)
-}
-
-func generate(kubeconfigPath string, output string, print bool, contexts []string) error {
-	conf, err := local.GetConfigFromHostWithCertData(kubeconfigPath, contexts)
-
-	if err != nil {
-		return err
-	}
-
-	rawConf, err := conf.RawConfig()
-
-	if err != nil {
-		return err
-	}
-
-	if print {
-		bytes, err := clientcmd.Write(rawConf)
-
-		if err != nil {
-			return err
-		}
-
-		fmt.Printf(string(bytes))
-
-		return nil
-	}
-
-	err = clientcmd.WriteToFile(rawConf, output)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

+ 0 - 63
cli/cmd/helpers.go

@@ -1,63 +0,0 @@
-package cmd
-
-import (
-	"bufio"
-	"errors"
-	"fmt"
-	"os"
-	"os/signal"
-	"strings"
-	"syscall"
-
-	"golang.org/x/crypto/ssh/terminal"
-)
-
-func closeHandler(closer func() error) {
-	sig := make(chan os.Signal)
-	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
-	go func() {
-		<-sig
-		err := closer()
-
-		if err == nil {
-			fmt.Println("shutdown successful")
-			os.Exit(0)
-		}
-
-		fmt.Printf("shutdown unsuccessful: %s\n", err.Error())
-		os.Exit(1)
-	}()
-}
-
-func promptPlaintext(prompt string) (string, error) {
-	reader := bufio.NewReader(os.Stdin)
-
-	fmt.Print(prompt)
-	text, err := reader.ReadString('\n')
-
-	if err != nil {
-		return "", err
-	}
-
-	return strings.TrimSpace(text), nil
-}
-
-func promptPasswordWithConfirmation() (string, error) {
-	fmt.Print("Password: ")
-	pw, err := terminal.ReadPassword(0)
-	fmt.Print("\r")
-
-	if err != nil {
-		return "", err
-	}
-
-	fmt.Print("Confirm password: ")
-	confirmPw, err := terminal.ReadPassword(0)
-	fmt.Print("\n")
-
-	if strings.TrimSpace(string(pw)) != strings.TrimSpace(string(confirmPw)) {
-		return "", errors.New("Passwords do not match")
-	}
-
-	return strings.TrimSpace(string(pw)), nil
-}

+ 0 - 72
cli/cmd/login.go

@@ -1,72 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"os"
-
-	"github.com/porter-dev/porter/cli/cmd/api"
-	"github.com/spf13/cobra"
-)
-
-var (
-	host string
-)
-
-// loginCmd represents the login command
-var loginCmd = &cobra.Command{
-	Use:   "login",
-	Short: "Authorizes a user for a given Porter server",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := login(host)
-
-		if err != nil {
-			fmt.Println("Error logging in:", err.Error())
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(loginCmd)
-
-	loginCmd.PersistentFlags().StringVar(
-		&host,
-		"host",
-		"http://localhost:10000",
-		"host url of Porter instance",
-	)
-}
-
-func login(host string) error {
-	var username, pw string
-
-	fmt.Println("Please log in with an email and password:")
-
-	username, err := promptPlaintext("Email: ")
-
-	if err != nil {
-		return err
-	}
-
-	pw, err = promptPasswordWithConfirmation()
-
-	if err != nil {
-		return err
-	}
-
-	client := api.NewClient(host+"/api", "cookie.json")
-
-	_, err = client.Login(context.Background(), &api.LoginRequest{
-		Email:    username,
-		Password: pw,
-	})
-
-	if err != nil {
-		return err
-	}
-
-	fmt.Println("Successfully logged in!")
-
-	return nil
-}

+ 4 - 34
cli/cmd/project.go

@@ -4,9 +4,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"strconv"
-
-	"github.com/spf13/viper"
 
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/spf13/cobra"
@@ -16,7 +13,7 @@ import (
 // without any subcommands
 var projectCmd = &cobra.Command{
 	Use:   "project",
-	Short: "The commands that can be run for a project",
+	Short: "Commands that control Porter project settings",
 }
 
 var createProjectCmd = &cobra.Command{
@@ -24,28 +21,7 @@ var createProjectCmd = &cobra.Command{
 	Args:  cobra.ExactArgs(1),
 	Short: "Creates a project with the authorized user as admin",
 	Run: func(cmd *cobra.Command, args []string) {
-		err := createProject(host, args[0])
-
-		if err != nil {
-			fmt.Printf("An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-var setProjectCmd = &cobra.Command{
-	Use:   "set [id]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Sets the current project as the project id.",
-	Run: func(cmd *cobra.Command, args []string) {
-		projID, err := strconv.ParseUint(args[0], 10, 64)
-
-		if err != nil {
-			fmt.Printf("An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-
-		err = setProject(uint(projID))
+		err := createProject(getHost(), args[0])
 
 		if err != nil {
 			fmt.Printf("An error occurred: %v\n", err)
@@ -59,10 +35,10 @@ func init() {
 
 	projectCmd.AddCommand(createProjectCmd)
 
-	createProjectCmd.PersistentFlags().StringVar(
+	projectCmd.PersistentFlags().StringVar(
 		&host,
 		"host",
-		"http://localhost:10000",
+		getHost(),
 		"host url of Porter instance",
 	)
 
@@ -84,9 +60,3 @@ func createProject(host string, name string) error {
 
 	return setProject(resp.ID)
 }
-
-func setProject(id uint) error {
-	fmt.Printf("Set the current project id as %d\n", id)
-	viper.Set("project", id)
-	return viper.WriteConfig()
-}

+ 141 - 0
cli/cmd/server.go

@@ -0,0 +1,141 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/porter-dev/porter/cli/cmd/docker"
+
+	"github.com/spf13/cobra"
+)
+
+type startOps struct {
+	imageTag string `form:"required"`
+	db       string `form:"oneof=sqlite postgres"`
+	port     *int   `form:"required"`
+}
+
+var opts = &startOps{}
+
+var serverCmd = &cobra.Command{
+	Use:   "server",
+	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",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := start(
+			opts.imageTag,
+			opts.db,
+			*opts.port,
+		)
+
+		if err != nil {
+			fmt.Println("Error running start:", err.Error())
+			fmt.Println("Shutting down...")
+
+			err = stop()
+
+			if err != nil {
+				fmt.Println("Shutdown unsuccessful:", err.Error())
+			}
+
+			os.Exit(1)
+		}
+	},
+}
+
+var stopCmd = &cobra.Command{
+	Use:   "stop",
+	Short: "Stops a Porter instance running on the Docker engine",
+	Run: func(cmd *cobra.Command, args []string) {
+		if err := stop(); err != nil {
+			fmt.Println("Shutdown unsuccessful:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(serverCmd)
+
+	serverCmd.AddCommand(startCmd)
+	serverCmd.AddCommand(stopCmd)
+
+	startCmd.PersistentFlags().StringVar(
+		&opts.db,
+		"db",
+		"sqlite",
+		"the db to use, one of sqlite or postgres",
+	)
+
+	startCmd.PersistentFlags().StringVar(
+		&opts.imageTag,
+		"image-tag",
+		"latest",
+		"the Porter image tag to use",
+	)
+
+	opts.port = startCmd.PersistentFlags().IntP(
+		"port",
+		"p",
+		8080,
+		"the host port to run the server on",
+	)
+}
+
+func start(
+	imageTag string,
+	db string,
+	port int,
+) error {
+	env := make([]string, 0)
+	var porterDB docker.PorterDB
+
+	switch db {
+	case "postgres":
+		porterDB = docker.Postgres
+	case "sqlite":
+		porterDB = docker.SQLite
+	}
+
+	startOpts := &docker.PorterStartOpts{
+		ProcessID:      "main",
+		ServerImageTag: imageTag,
+		ServerPort:     port,
+		DB:             porterDB,
+		Env:            env,
+	}
+
+	_, _, err := docker.StartPorter(startOpts)
+
+	if err != nil {
+		return err
+	}
+
+	// fmt.Println("Spinning up the server...")
+	// time.Sleep(7 * time.Second)
+	// openBrowser(fmt.Sprintf("http://localhost:%d/login?email=%s", port, username))
+	fmt.Printf("Server ready: listening on localhost:%d\n", port)
+
+	return setHost(fmt.Sprintf("http://localhost:%d", port))
+}
+
+func stop() error {
+	agent, err := docker.NewAgentFromEnv()
+
+	if err != nil {
+		return err
+	}
+
+	err = agent.StopPorterContainersWithProcessID("main", false)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 0 - 221
cli/cmd/start.go

@@ -1,221 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"time"
-
-	"github.com/porter-dev/porter/cli/cmd/docker"
-	"k8s.io/client-go/util/homedir"
-
-	"github.com/porter-dev/porter/cli/cmd/credstore"
-
-	"github.com/spf13/cobra"
-)
-
-type startOps struct {
-	insecure       *bool
-	skipKubeconfig *bool
-	kubeconfigPath string
-	contexts       *[]string
-	imageTag       string `form:"required"`
-	db             string `form:"oneof=sqlite postgres"`
-}
-
-var opts = &startOps{}
-
-// startCmd represents the start command
-var startCmd = &cobra.Command{
-	Args: func(cmd *cobra.Command, args []string) error {
-		return nil
-	},
-	Use:   "start",
-	Short: "Starts a Porter instance using the Docker engine.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := start(
-			opts.imageTag,
-			opts.kubeconfigPath,
-			opts.db,
-			*opts.contexts,
-			*opts.insecure,
-			*opts.skipKubeconfig,
-		)
-
-		if err != nil {
-			fmt.Println("Error running start:", err.Error())
-			fmt.Println("Shutting down...")
-
-			err = stop()
-
-			if err != nil {
-				fmt.Println("Shutdown unsuccessful:", err.Error())
-			}
-
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(startCmd)
-
-	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, one of sqlite or postgres",
-	)
-
-	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.StopPorterContainersWithProcessID("main", false)
-
-	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")
-
-	// if not insecure, or username/pw set incorrectly, prompt for new username/pw
-	if username, pw, err = credstore.Get(); !insecure && err != nil {
-		fmt.Println("Please register your admin account with an email and password:")
-
-		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
-		}
-	}
-
-	env := make([]string, 0)
-
-	env = append(env, []string{
-		"ADMIN_INIT=true",
-		"ADMIN_EMAIL=" + username,
-		"ADMIN_PASSWORD=" + pw,
-	}...)
-
-	var porterDB docker.PorterDB
-
-	switch db {
-	case "postgres":
-		porterDB = docker.Postgres
-	case "sqlite":
-		porterDB = docker.SQLite
-	}
-
-	port := 8080
-
-	startOpts := &docker.PorterStartOpts{
-		ProcessID:      "main",
-		ServerImageTag: imageTag,
-		ServerPort:     port,
-		DB:             porterDB,
-		KubeconfigPath: kubeconfigPath,
-		SkipKubeconfig: skipKubeconfig,
-		Env:            env,
-	}
-
-	_, _, err = docker.StartPorter(startOpts)
-
-	fmt.Println("Spinning up the server...")
-	time.Sleep(7 * time.Second)
-	openBrowser(fmt.Sprintf("http://localhost:%d/login?email=%s", port, username))
-	fmt.Printf("Server ready: listening on localhost:%d\n", port)
-
-	return nil
-}
-
-// openBrowser opens the specified URL in the default browser of the user.
-func openBrowser(url string) error {
-	var cmd string
-	var args []string
-
-	switch runtime.GOOS {
-	case "windows":
-		cmd = "cmd"
-		args = []string{"/c", "start"}
-	case "darwin":
-		cmd = "open"
-	default: // "linux", "freebsd", "openbsd", "netbsd"
-		cmd = "xdg-open"
-	}
-	args = append(args, url)
-	return exec.Command(cmd, args...).Start()
-}

+ 24 - 0
cli/cmd/utils/browser.go

@@ -0,0 +1,24 @@
+package utils
+
+import (
+	"os/exec"
+	"runtime"
+)
+
+// OpenBrowser opens the specified URL in the default browser of the user.
+func OpenBrowser(url string) error {
+	var cmd string
+	var args []string
+
+	switch runtime.GOOS {
+	case "windows":
+		cmd = "cmd"
+		args = []string{"/c", "start"}
+	case "darwin":
+		cmd = "open"
+	default: // "linux", "freebsd", "openbsd", "netbsd"
+		cmd = "xdg-open"
+	}
+	args = append(args, url)
+	return exec.Command(cmd, args...).Start()
+}

+ 25 - 0
cli/cmd/utils/close.go

@@ -0,0 +1,25 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+func closeHandler(closer func() error) {
+	sig := make(chan os.Signal)
+	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-sig
+		err := closer()
+
+		if err == nil {
+			fmt.Println("shutdown successful")
+			os.Exit(0)
+		}
+
+		fmt.Printf("shutdown unsuccessful: %s\n", err.Error())
+		os.Exit(1)
+	}()
+}

+ 57 - 0
cli/cmd/utils/prompt.go

@@ -0,0 +1,57 @@
+package utils
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	"golang.org/x/crypto/ssh/terminal"
+)
+
+// PromptPlaintext prompts a user to input plain text
+func PromptPlaintext(prompt string) (string, error) {
+	reader := bufio.NewReader(os.Stdin)
+
+	fmt.Print(prompt)
+	text, err := reader.ReadString('\n')
+
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(text), nil
+}
+
+// PromptPassword prompts a user to input a hidden field
+func PromptPassword(prompt string) (string, error) {
+	fmt.Print(prompt)
+	pw, err := terminal.ReadPassword(0)
+	fmt.Print("\r")
+
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(string(pw)), nil
+}
+
+// PromptPasswordWithConfirmation is a helper function to prompt
+// for a password twice
+func PromptPasswordWithConfirmation() (string, error) {
+	pw, err := PromptPassword("Password: ")
+	if err != nil {
+		return "", err
+	}
+	confirmPw, err := PromptPassword("Confirm password: ")
+	if err != nil {
+		return "", err
+	}
+
+	if pw != confirmPw {
+		return "", errors.New("Passwords do not match")
+	}
+
+	return pw, nil
+}

+ 0 - 0
internal/utils/random_string.go → cli/cmd/utils/random_string.go


+ 2 - 10
cmd/app/main.go

@@ -38,15 +38,6 @@ func main() {
 
 	repo := gorm.NewRepository(db, &key)
 
-	// upsert admin if config requires
-	// if appConf.Db.AdminInit {
-	// 	err := upsertAdmin(repo.User, appConf.Db.AdminEmail, appConf.Db.AdminPassword)
-
-	// 	if err != nil {
-	// 		fmt.Println("Error while upserting admin: " + err.Error())
-	// 	}
-	// }
-
 	// declare as Store interface (methods Get, New, Save)
 	var store sessions.Store
 	store, _ = sessionstore.NewStore(repo, appConf.Server)
@@ -55,6 +46,7 @@ func main() {
 
 	a := api.New(
 		logger,
+		nil,
 		repo,
 		validator,
 		store,
@@ -83,7 +75,7 @@ func main() {
 	}
 
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
-		log.Fatal("Server startup failed")
+		log.Fatal("Server startup failed", err)
 	}
 }
 

+ 25 - 0
cmd/ready/main.go

@@ -0,0 +1,25 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+
+	"github.com/porter-dev/porter/internal/config"
+)
+
+func main() {
+	appConf := config.FromEnv()
+
+	resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/livez", appConf.Server.Port))
+
+	if err != nil || resp.StatusCode >= http.StatusBadRequest {
+		os.Exit(1)
+	}
+
+	resp, err = http.Get(fmt.Sprintf("http://localhost:%d/api/readyz", appConf.Server.Port))
+
+	if err != nil || resp.StatusCode >= http.StatusBadRequest {
+		os.Exit(1)
+	}
+}

+ 3 - 1
docker/Dockerfile

@@ -22,7 +22,8 @@ FROM base AS build-go
 RUN --mount=type=cache,target=/root/.cache/go-build \
     --mount=type=cache,target=$GOPATH/pkg/mod \
     go build -ldflags '-w -s' -a -o ./bin/app ./cmd/app && \
-    go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate
+    go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate && \
+    go build -ldflags '-w -s' -a -o ./bin/ready ./cmd/ready
 
 # Go test environment
 # -------------------
@@ -52,6 +53,7 @@ RUN apk update
 
 COPY --from=build-go /porter/bin/app /porter/
 COPY --from=build-go /porter/bin/migrate /porter/
+COPY --from=build-go /porter/bin/ready /porter/
 COPY --from=build-webpack /webpack/build /porter/static
 
 ENV DEBUG=false

+ 4 - 0
server/api/api.go

@@ -6,6 +6,7 @@ import (
 	"github.com/go-playground/validator/v10"
 	"github.com/porter-dev/porter/internal/oauth"
 	"golang.org/x/oauth2"
+	"gorm.io/gorm"
 
 	"github.com/gorilla/sessions"
 	"github.com/porter-dev/porter/internal/helm"
@@ -25,6 +26,7 @@ type TestAgents struct {
 // App represents an API instance with handler methods attached, a DB connection
 // and a logger instance
 type App struct {
+	db           *gorm.DB
 	logger       *lr.Logger
 	repo         *repository.Repository
 	validator    *validator.Validate
@@ -39,6 +41,7 @@ type App struct {
 // New returns a new App instance
 func New(
 	logger *lr.Logger,
+	db *gorm.DB,
 	repo *repository.Repository,
 	validator *validator.Validate,
 	store sessions.Store,
@@ -71,6 +74,7 @@ func New(
 	}
 
 	return &App{
+		db:           db,
 		logger:       logger,
 		repo:         repo,
 		validator:    validator,

+ 44 - 0
server/api/health_handler.go

@@ -0,0 +1,44 @@
+package api
+
+import (
+	"net/http"
+)
+
+// HandleLive responds immediately with an HTTP 200 status.
+func (app *App) HandleLive(w http.ResponseWriter, r *http.Request) {
+	writeHealthy(w)
+}
+
+// HandleReady responds with HTTP 200 if healthy, 500 otherwise
+func (app *App) HandleReady(w http.ResponseWriter, r *http.Request) {
+	if app.db == nil {
+		writeHealthy(w)
+		return
+	}
+
+	db, err := app.db.DB()
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	if err := db.Ping(); err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	writeHealthy(w)
+}
+
+func writeHealthy(w http.ResponseWriter) {
+	w.Header().Set("Content-Type", "text/plain")
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte("."))
+}
+
+func writeUnhealthy(w http.ResponseWriter) {
+	w.Header().Set("Content-Type", "text/plain")
+	w.WriteHeader(http.StatusInternalServerError)
+	w.Write([]byte("."))
+}

+ 1 - 1
server/api/helpers_test.go

@@ -79,7 +79,7 @@ func newTester(canQuery bool) *tester {
 	repo := test.NewRepository(canQuery)
 
 	store, _ := sessionstore.NewStore(repo, appConf.Server)
-	app := api.New(logger, repo, validator, store, appConf.Server.CookieName, true, nil)
+	app := api.New(logger, nil, repo, validator, store, appConf.Server.CookieName, true, nil)
 	r := router.New(app, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 
 	return &tester{

+ 4 - 0
server/router/router.go

@@ -27,6 +27,10 @@ func New(
 	r.Route("/api", func(r chi.Router) {
 		r.Use(mw.ContentTypeJSON)
 
+		// health checks
+		r.Method("GET", "/livez", http.HandlerFunc(a.HandleLive))
+		r.Method("GET", "/readyz", http.HandlerFunc(a.HandleReady))
+
 		// /api/users routes
 		r.Method("GET", "/users/{user_id}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleReadUser, l), mw.URLParam))
 		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))