Parcourir la source

Merge pull request #109 from porter-dev/beta.2.hotfix-107

Beta.2.hotfix 107
abelanger5 il y a 5 ans
Parent
commit
c54916e5b8

+ 13 - 1
.darwin.goreleaser.yml

@@ -15,7 +15,19 @@ builds:
     flags:
       - -tags=cli
     hooks:
-      post: gon gon.hcl
+      post: gon gon.cli.hcl
+  - id: "porter-server"
+    binary: portersvr
+    env:
+      - CGO_ENABLED=1
+    dir: cmd/app
+    main: ./main.go
+    goos:
+      - darwin
+    goarch:
+      - amd64
+    hooks:
+      post: gon gon.server.hcl
 archives:
   - format: binary
     replacements:

+ 1 - 1
.gitignore

@@ -5,4 +5,4 @@ app
 *.db
 test.yaml
 dist
-gon.hcl
+gon*.hcl

+ 11 - 0
.goreleaser.yml

@@ -15,6 +15,17 @@ builds:
       - amd64
     flags:
       - -tags=cli
+  - id: "porter-server"
+    binary: portersvr
+    env:
+      - CGO_ENABLED=1
+    dir: cmd/app
+    main: ./main.go
+    goos:
+      - linux
+      - windows
+    goarch:
+      - amd64
 archives:
   - format: zip
     replacements:

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

@@ -153,6 +153,7 @@ func (c *Client) CreateProject(
 // which can be resolved to create a service account
 type CreateProjectCandidatesRequest struct {
 	Kubeconfig string `json:"kubeconfig"`
+	IsLocal    bool   `json:"is_local"`
 }
 
 // CreateProjectCandidatesResponse is the list of candidates returned after

+ 12 - 12
cli/cmd/api/project_test.go

@@ -186,8 +186,8 @@ func TestGetProjectServiceAccount(t *testing.T) {
 		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "cluster-test", resp.Clusters[0].Name)
 	}
 
-	if resp.Clusters[0].Server != "https://localhost" {
-		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://localhost", resp.Clusters[0].Server)
+	if resp.Clusters[0].Server != "https://10.10.10.10" {
+		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://10.10.10.10", resp.Clusters[0].Server)
 	}
 }
 
@@ -231,8 +231,8 @@ func TestCreateProjectCandidates(t *testing.T) {
 		t.Errorf("cluster name incorrect: expected %s, got %s\n", "cluster-test", resp[0].ClusterName)
 	}
 
-	if resp[0].ClusterEndpoint != "https://localhost" {
-		t.Errorf("cluster endpoint incorrect: expected %s, got %s\n", "https://localhost", resp[0].ClusterEndpoint)
+	if resp[0].ClusterEndpoint != "https://10.10.10.10" {
+		t.Errorf("cluster endpoint incorrect: expected %s, got %s\n", "https://10.10.10.10", resp[0].ClusterEndpoint)
 	}
 
 	// make sure correct actions need to be performed
@@ -284,8 +284,8 @@ func TestGetProjectCandidates(t *testing.T) {
 		t.Errorf("cluster name incorrect: expected %s, got %s\n", "cluster-test", resp[0].ClusterName)
 	}
 
-	if resp[0].ClusterEndpoint != "https://localhost" {
-		t.Errorf("cluster endpoint incorrect: expected %s, got %s\n", "https://localhost", resp[0].ClusterEndpoint)
+	if resp[0].ClusterEndpoint != "https://10.10.10.10" {
+		t.Errorf("cluster endpoint incorrect: expected %s, got %s\n", "https://10.10.10.10", resp[0].ClusterEndpoint)
 	}
 
 	// make sure correct actions need to be performed
@@ -355,8 +355,8 @@ func TestCreateProjectServiceAccount(t *testing.T) {
 		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "cluster-test", resp.Clusters[0].Name)
 	}
 
-	if resp.Clusters[0].Server != "https://localhost" {
-		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://localhost", resp.Clusters[0].Server)
+	if resp.Clusters[0].Server != "https://10.10.10.10" {
+		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://10.10.10.10", resp.Clusters[0].Server)
 	}
 }
 
@@ -394,8 +394,8 @@ func TestListProjectClusters(t *testing.T) {
 		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "cluster-test", resp[0].Name)
 	}
 
-	if resp[0].Server != "https://localhost" {
-		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://localhost", resp[0].Server)
+	if resp[0].Server != "https://10.10.10.10" {
+		t.Errorf("cluster's name is incorrect: expected %s, got %s\n", "https://10.10.10.10", resp[0].Server)
 	}
 }
 
@@ -444,7 +444,7 @@ const OIDCAuthWithoutData string = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -462,7 +462,7 @@ users:
       config:
         client-id: porter-api
         id-token: token
-        idp-issuer-url: https://localhost
+        idp-issuer-url: https://10.10.10.10
         idp-certificate-authority: /fake/path/to/ca.pem
       name: oidc
 `

+ 16 - 0
cli/cmd/config.go

@@ -11,6 +11,7 @@ import (
 
 // a set of shared flags
 var (
+	driver    string
 	host      string
 	projectID uint
 	clusterID uint
@@ -85,6 +86,21 @@ func init() {
 	configCmd.AddCommand(setHostCmd)
 }
 
+func setDriver(driver string) error {
+	viper.Set("driver", driver)
+	err := viper.WriteConfig()
+	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
+	return err
+}
+
+func getDriver() string {
+	if driver != "" {
+		return driver
+	}
+
+	return viper.GetString("driver")
+}
+
 func setProject(id uint) error {
 	viper.Set("project", id)
 	color.New(color.FgGreen).Printf("Set the current project id as %d\n", id)

+ 7 - 0
cli/cmd/connect.go

@@ -66,10 +66,17 @@ func init() {
 }
 
 func runConnect(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	isLocal := false
+
+	if getDriver() == "local" {
+		isLocal = true
+	}
+
 	return connect.Kubeconfig(
 		client,
 		kubeconfigPath,
 		*contexts,
 		getProjectID(),
+		isLocal,
 	)
 }

+ 17 - 0
cli/cmd/connect/kubeconfig.go

@@ -26,6 +26,7 @@ func Kubeconfig(
 	kubeconfigPath string,
 	contexts []string,
 	projectID uint,
+	isLocal bool,
 ) error {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	if projectID == 0 {
@@ -45,6 +46,7 @@ func Kubeconfig(
 		projectID,
 		&api.CreateProjectCandidatesRequest{
 			Kubeconfig: string(rawBytes),
+			IsLocal:    isLocal,
 		},
 	)
 
@@ -83,6 +85,14 @@ func Kubeconfig(
 						return err
 					}
 
+					resolvers = append(resolvers, resolveAction)
+				case models.ClusterLocalhostAction:
+					resolveAction, err := resolveLocalhostAction()
+
+					if err != nil {
+						return err
+					}
+
 					resolvers = append(resolvers, resolveAction)
 				case models.ClientCertDataAction:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
@@ -256,6 +266,13 @@ func resolveClusterCAAction(
 	}, nil
 }
 
+func resolveLocalhostAction() (*models.ServiceAccountAllActions, error) {
+	return &models.ServiceAccountAllActions{
+		Name:            models.ClusterLocalhostAction,
+		ClusterHostname: "host.docker.internal",
+	}, nil
+}
+
 // resolves a client cert data action
 func resolveClientCertAction(
 	filename string,

+ 150 - 0
cli/cmd/github/release.go

@@ -0,0 +1,150 @@
+package github
+
+import (
+	"archive/zip"
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+
+	"github.com/google/go-github/github"
+)
+
+func getLatestReleaseDownloadURL() (string, error) {
+	client := github.NewClient(nil)
+
+	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), "porter-dev", "porter")
+
+	if err != nil {
+		return "", err
+	}
+
+	var re *regexp.Regexp
+
+	switch os := runtime.GOOS; os {
+	case "darwin":
+		re = regexp.MustCompile(`portersvr_.*_Darwin_x86_64\.zip`)
+	case "linux":
+		re = regexp.MustCompile(`portersvr_.*_Linux_x86_64\.zip`)
+	default:
+		fmt.Printf("%s.\n", os)
+	}
+
+	releaseURL := ""
+
+	// iterate through the assets
+	for _, asset := range rel.Assets {
+		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
+			releaseURL = downloadURL
+			break
+		}
+	}
+
+	return releaseURL, nil
+}
+
+// DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
+// it, and adds the binary to the porter directory
+func DownloadLatestServerRelease(porterDir string) error {
+	releaseURL, err := getLatestReleaseDownloadURL()
+	fmt.Println(releaseURL)
+
+	if err != nil {
+		return err
+	}
+
+	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
+
+	err = downloadToFile(releaseURL, zipFile)
+
+	if err != nil {
+		return err
+	}
+
+	err = unzipToDir(zipFile, porterDir)
+
+	return err
+}
+
+func downloadToFile(url string, filepath string) error {
+	// Get the data
+	resp, err := http.Get(url)
+
+	if err != nil {
+		return err
+	}
+
+	defer resp.Body.Close()
+
+	// Create the file
+	out, err := os.Create(filepath)
+
+	if err != nil {
+		return err
+	}
+
+	defer out.Close()
+
+	// Write the body to file
+	_, err = io.Copy(out, resp.Body)
+
+	return err
+}
+
+func unzipToDir(zipfile string, dir string) error {
+	r, err := zip.OpenReader(zipfile)
+
+	if err != nil {
+		return err
+	}
+
+	defer r.Close()
+
+	for _, f := range r.File {
+		// Store filename/path for returning and using later on
+		fpath := filepath.Join(dir, f.Name)
+
+		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
+		if !strings.HasPrefix(fpath, filepath.Clean(dir)+string(os.PathSeparator)) {
+			return fmt.Errorf("%s: illegal file path", fpath)
+		}
+
+		if f.FileInfo().IsDir() {
+			// Make Folder
+			os.MkdirAll(fpath, os.ModePerm)
+			continue
+		}
+
+		// Make File
+		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
+			return err
+		}
+
+		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
+		if err != nil {
+			return err
+		}
+
+		rc, err := f.Open()
+		if err != nil {
+			return err
+		}
+
+		_, err = io.Copy(outFile, rc)
+
+		// Close the file without defer to close before next iteration of loop
+		outFile.Close()
+		rc.Close()
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 11 - 1
cli/cmd/root.go

@@ -23,9 +23,19 @@ var home = homedir.HomeDir()
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
+	// 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(filepath.Join(home, ".porter"))
+	viper.AddConfigPath(porterDir)
 
 	err := viper.ReadInConfig()
 

+ 76 - 19
cli/cmd/server.go

@@ -3,6 +3,8 @@ package cmd
 import (
 	"fmt"
 	"os"
+	"os/exec"
+	"path/filepath"
 
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/docker"
@@ -13,6 +15,7 @@ import (
 type startOps struct {
 	imageTag string `form:"required"`
 	db       string `form:"oneof=sqlite postgres"`
+	driver   string `form:"required"`
 	port     *int   `form:"required"`
 }
 
@@ -23,29 +26,72 @@ var serverCmd = &cobra.Command{
 	Short: "Commands to control a local Porter server",
 }
 
+var testCmd = &cobra.Command{
+	Use:   "test",
+	Short: "Testing",
+	Run: func(cmd *cobra.Command, args []string) {
+		setDriver("local")
+
+		// TODO -- DOWNLOAD THE LATEST RELEASE, IF NOT EXIST
+		// porterDir := filepath.Join(home, ".porter")
+
+		// err := github.DownloadLatestServerRelease(porterDir)
+
+		// if err != nil {
+		// 	color.New(color.FgRed).Println("Failed:", err.Error())
+		// 	os.Exit(1)
+		// }
+
+		cmdPath := filepath.Join(home, ".porter", "portersvr")
+		sqlLitePath := filepath.Join(home, ".porter", "porter.db")
+		staticFilePath := filepath.Join(home, ".porter", "static")
+
+		cmdPorter := exec.Command(cmdPath)
+		cmdPorter.Env = os.Environ()
+		cmdPorter.Env = append(cmdPorter.Env, []string{
+			"IS_LOCAL=true",
+			"SQL_LITE=true",
+			"SQL_LITE_PATH=" + sqlLitePath,
+			"STATIC_FILE_PATH=" + staticFilePath,
+		}...)
+
+		cmdPorter.Stdout = os.Stdout
+		cmdPorter.Stderr = os.Stderr
+
+		err := cmdPorter.Run()
+
+		if err != nil {
+			color.New(color.FgRed).Println("Failed:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
 // 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 getDriver() == "docker" {
+			err := startDocker(
+				opts.imageTag,
+				opts.db,
+				*opts.port,
+			)
 
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error running start:", err.Error())
-			red.Println("Shutting down...")
+			if err != nil {
+				red := color.New(color.FgRed)
+				red.Println("Error running start:", err.Error())
+				red.Println("Shutting down...")
 
-			err = stop()
+				err = stopDocker()
 
-			if err != nil {
-				red.Println("Shutdown unsuccessful:", err.Error())
-			}
+				if err != nil {
+					red.Println("Shutdown unsuccessful:", err.Error())
+				}
 
-			os.Exit(1)
+				os.Exit(1)
+			}
 		}
 	},
 }
@@ -54,14 +100,18 @@ 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 {
-			color.New(color.FgRed).Println("Shutdown unsuccessful:", err.Error())
-			os.Exit(1)
+		if getDriver() == "docker" {
+			if err := stopDocker(); err != nil {
+				color.New(color.FgRed).Println("Shutdown unsuccessful:", err.Error())
+				os.Exit(1)
+			}
 		}
 	},
 }
 
 func init() {
+	rootCmd.AddCommand(testCmd)
+
 	rootCmd.AddCommand(serverCmd)
 
 	serverCmd.AddCommand(startCmd)
@@ -74,6 +124,13 @@ func init() {
 		"the db to use, one of sqlite or postgres",
 	)
 
+	startCmd.PersistentFlags().StringVar(
+		&opts.driver,
+		"driver",
+		"local",
+		"the db to use, one of local or docker",
+	)
+
 	startCmd.PersistentFlags().StringVar(
 		&opts.imageTag,
 		"image-tag",
@@ -89,7 +146,7 @@ func init() {
 	)
 }
 
-func start(
+func startDocker(
 	imageTag string,
 	db string,
 	port int,
@@ -129,7 +186,7 @@ func start(
 	return setHost(fmt.Sprintf("http://localhost:%d", port))
 }
 
-func stop() error {
+func stopDocker() error {
 	agent, err := docker.NewAgentFromEnv()
 
 	if err != nil {

+ 20 - 0
cmd/app/main.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 
@@ -30,6 +31,24 @@ func main() {
 		return
 	}
 
+	err = db.AutoMigrate(
+		&models.Project{},
+		&models.Role{},
+		&models.ServiceAccount{},
+		&models.ServiceAccountAction{},
+		&models.ServiceAccountCandidate{},
+		&models.Cluster{},
+		&models.TokenCache{},
+		&models.User{},
+		&models.Session{},
+		&models.RepoClient{},
+	)
+
+	if err != nil {
+		logger.Fatal().Err(err).Msg("")
+		return
+	}
+
 	var key [32]byte
 
 	for i, b := range []byte(appConf.Db.EncryptionKey) {
@@ -52,6 +71,7 @@ func main() {
 		store,
 		appConf.Server.CookieName,
 		false,
+		appConf.Server.IsLocal,
 		&oauth.Config{
 			ClientID:     appConf.Server.GithubClientID,
 			ClientSecret: appConf.Server.GithubClientSecret,

+ 8 - 0
go.sum

@@ -58,20 +58,24 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX
 github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
 github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
 github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
 github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest v0.11.1 h1:eVvIXUKiTgv++6YnWb42DUA1YL7qDugnKP0HljexdnQ=
 github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
 github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
 github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
 github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
 github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
+github.com/Azure/go-autorest/autorest/adal v0.9.0 h1:SigMbuFNuKgc1xcGhaeapbh+8fgsu+GxgDRFyg7f5lM=
 github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
 github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
 github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
 github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
 github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
 github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
 github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
 github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
@@ -80,8 +84,10 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935
 github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
 github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE=
 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -306,6 +312,7 @@ github.com/denverdino/aliyungo v0.0.0-20180316152028-2581e433b270/go.mod h1:dV8l
 github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
 github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/digitalocean/godo v1.19.0/go.mod h1:AAPQ+tiM4st79QHlEBTg8LM7JQNre4SAQCbn56wEyKY=
@@ -631,6 +638,7 @@ github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTV
 github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
 github.com/gophercloud/gophercloud v0.0.0-20180807015416-4ea085781bae/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
 github.com/gophercloud/gophercloud v0.0.0-20190216224116-dcc6e84aef1b/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
+github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/goreleaser/goreleaser v0.136.0/go.mod h1:wiKrPUeSNh6Wu8nUHxZydSOVQ/OZvOaO7DTtFqie904=

+ 2 - 0
internal/config/config.go

@@ -26,6 +26,8 @@ type ServerConf struct {
 	TimeoutWrite   time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
 	TimeoutIdle    time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
 
+	IsLocal bool `env:"IS_LOCAL,default=false"`
+
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
 }

+ 49 - 0
internal/forms/action.go

@@ -2,6 +2,7 @@ package forms
 
 import (
 	"encoding/base64"
+	"net/url"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
@@ -91,6 +92,13 @@ func (sar *ServiceAccountActionResolver) PopulateServiceAccount(
 		}
 	}
 
+	// if auth mechanism is local, just write the kubeconfig and return: rest of config is
+	// unnecessary
+	if sar.SACandidate.AuthMechanism == models.Local && len(sar.SACandidate.Kubeconfig) > 0 {
+		sar.SA.Kubeconfig = sar.SACandidate.Kubeconfig
+		return nil
+	}
+
 	if len(authInfo.ClientCertificateData) > 0 {
 		sar.SA.ClientCertificateData = authInfo.ClientCertificateData
 	}
@@ -179,6 +187,47 @@ func (cda *ClusterCADataAction) PopulateServiceAccount(
 	return nil
 }
 
+// ClusterLocalhostAction contains the non-localhost server
+type ClusterLocalhostAction struct {
+	*ServiceAccountActionResolver
+	ClusterHostname string `json:"cluster_hostname" form:"required"`
+}
+
+// PopulateServiceAccount will add cluster ca data to a cluster in the ServiceAccount's
+// list of clusters
+func (cla *ClusterLocalhostAction) PopulateServiceAccount(
+	repo repository.ServiceAccountRepository,
+) error {
+	err := cla.ServiceAccountActionResolver.PopulateServiceAccount(repo)
+
+	if err != nil {
+		return err
+	}
+
+	saCandidate := cla.ServiceAccountActionResolver.SACandidate
+
+	for i, cluster := range cla.ServiceAccountActionResolver.SA.Clusters {
+		if cluster.Name == saCandidate.ClusterName && cluster.Server == saCandidate.ClusterEndpoint {
+			serverURL, err := url.Parse(cluster.Server)
+
+			if err != nil {
+				continue
+			}
+
+			if serverURL.Port() == "" {
+				serverURL.Host = cla.ClusterHostname
+			} else {
+				serverURL.Host = cla.ClusterHostname + ":" + serverURL.Port()
+			}
+
+			(&cluster).Server = serverURL.String()
+			cla.ServiceAccountActionResolver.SA.Clusters[i] = cluster
+		}
+	}
+
+	return nil
+}
+
 // ClientCertDataAction contains the base64 encoded cluster cert data
 type ClientCertDataAction struct {
 	*ServiceAccountActionResolver

+ 93 - 8
internal/forms/action_test.go

@@ -20,7 +20,7 @@ func TestPopulateServiceAccountBasic(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClusterCAWithData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClusterCAWithData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -82,7 +82,7 @@ func TestPopulateServiceAccountClusterDataAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClusterCAWithoutData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClusterCAWithoutData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -137,6 +137,71 @@ func TestPopulateServiceAccountClusterDataAction(t *testing.T) {
 	}
 }
 
+func TestPopulateServiceAccountClusterLocalhostAction(t *testing.T) {
+	// create the in-memory repository
+	repo := test.NewRepository(true)
+
+	// create a new project
+	repo.Project.CreateProject(&models.Project{
+		Name: "test-project",
+	})
+
+	// create a ServiceAccountCandidate from a kubeconfig
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClusterLocalhost), false)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	for _, saCandidate := range saCandidates {
+		repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+	}
+
+	// create a new form
+	form := forms.ClusterLocalhostAction{
+		ServiceAccountActionResolver: &forms.ServiceAccountActionResolver{
+			ServiceAccountCandidateID: 1,
+		},
+		ClusterHostname: "host.docker.internal",
+	}
+
+	err = form.PopulateServiceAccount(repo.ServiceAccount)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	sa, err := repo.ServiceAccount.CreateServiceAccount(form.ServiceAccountActionResolver.SA)
+	decodedStr, _ := base64.StdEncoding.DecodeString("LS0tLS1CRUdJTiBDRVJ=")
+
+	if len(sa.Clusters) != 1 {
+		t.Fatalf("cluster not written\n")
+	}
+
+	if sa.Clusters[0].ServiceAccountID != 1 {
+		t.Errorf("service account ID of joined cluster is not 1")
+	}
+
+	if sa.Clusters[0].Server != "https://host.docker.internal:30000" {
+		t.Errorf("service account cluster server is incorrect: expected %s, got %s\n",
+			"https://host.docker.internal:30000", sa.Clusters[0].Server)
+	}
+
+	if sa.AuthMechanism != "x509" {
+		t.Errorf("service account auth mechanism is not x509")
+	}
+
+	if string(sa.ClientCertificateData) != string(decodedStr) {
+		t.Errorf("service account cert data and input do not match: expected %s, got %s\n",
+			string(sa.ClientCertificateData), string(decodedStr))
+	}
+
+	if string(sa.ClientKeyData) != string(decodedStr) {
+		t.Errorf("service account key data and input do not match: expected %s, got %s\n",
+			string(sa.ClientKeyData), string(decodedStr))
+	}
+}
+
 func TestPopulateServiceAccountClientCertAction(t *testing.T) {
 	// create the in-memory repository
 	repo := test.NewRepository(true)
@@ -147,7 +212,7 @@ func TestPopulateServiceAccountClientCertAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClientWithoutCertData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClientWithoutCertData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -212,7 +277,7 @@ func TestPopulateServiceAccountClientCertAndKeyActions(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClientWithoutCertAndKeyData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(ClientWithoutCertAndKeyData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -289,7 +354,7 @@ func TestPopulateServiceAccountTokenDataAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(BearerTokenWithoutData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(BearerTokenWithoutData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -344,7 +409,7 @@ func TestPopulateServiceAccountGCPKeyDataAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(GCPPlugin))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(GCPPlugin), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -398,7 +463,7 @@ func TestPopulateServiceAccountAWSKeyDataAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(AWSEKSGetTokenExec))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(AWSEKSGetTokenExec), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -464,7 +529,7 @@ func TestPopulateServiceAccountOIDCAction(t *testing.T) {
 	})
 
 	// create a ServiceAccountCandidate from a kubeconfig
-	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(OIDCAuthWithoutData))
+	saCandidates, err := kubernetes.GetServiceAccountCandidates([]byte(OIDCAuthWithoutData), false)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -550,6 +615,26 @@ users:
 current-context: context-test
 `
 
+const ClusterLocalhost string = `
+apiVersion: v1
+kind: Config
+clusters:
+- name: cluster-test
+  cluster:
+    server: https://localhost:30000
+contexts:
+- context:
+    cluster: cluster-test
+    user: test-admin
+  name: context-test
+users:
+- name: test-admin
+  user:
+    client-certificate-data: LS0tLS1CRUdJTiBDRVJ=
+    client-key-data: LS0tLS1CRUdJTiBDRVJ=
+current-context: context-test
+`
+
 const ClientWithoutCertData string = `
 apiVersion: v1
 kind: Config

+ 10 - 2
internal/forms/candidate.go

@@ -10,12 +10,20 @@ import (
 type CreateServiceAccountCandidatesForm struct {
 	ProjectID  uint   `json:"project_id"`
 	Kubeconfig string `json:"kubeconfig"`
+
+	// Represents whether the auth mechanism should be designated as
+	// "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
+	// kubeconfig.
+	IsLocal bool `json:"is_local"`
 }
 
 // ToServiceAccountCandidates creates a ServiceAccountCandidate from the kubeconfig and
 // project id
-func (csa *CreateServiceAccountCandidatesForm) ToServiceAccountCandidates() ([]*models.ServiceAccountCandidate, error) {
-	candidates, err := kubernetes.GetServiceAccountCandidates([]byte(csa.Kubeconfig))
+func (csa *CreateServiceAccountCandidatesForm) ToServiceAccountCandidates(
+	isServerLocal bool,
+) ([]*models.ServiceAccountCandidate, error) {
+	// can only use "local" auth mechanism if the server is running locally
+	candidates, err := kubernetes.GetServiceAccountCandidates([]byte(csa.Kubeconfig), isServerLocal && csa.IsLocal)
 
 	if err != nil {
 		return nil, err

+ 1 - 1
internal/kubernetes/config.go

@@ -20,7 +20,7 @@ import (
 	"k8s.io/client-go/util/homedir"
 
 	// add oidc provider here
-	_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
 )
 
 // GetAgentOutOfClusterConfig creates a new Agent using the OutOfClusterConfig

+ 42 - 18
internal/kubernetes/kubeconfig.go

@@ -3,6 +3,7 @@ package kubernetes
 import (
 	"context"
 	"errors"
+	"net/url"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
@@ -19,7 +20,11 @@ import (
 
 // GetServiceAccountCandidates parses a kubeconfig for a list of service account
 // candidates.
-func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCandidate, error) {
+//
+// The local boolean represents whether the auth mechanism should be designated as
+// "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
+// kubeconfig.
+func GetServiceAccountCandidates(kubeconfig []byte, local bool) ([]*models.ServiceAccountCandidate, error) {
 	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
 
 	if err != nil {
@@ -39,19 +44,25 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 		awsClusterID := ""
 		authInfoName := context.AuthInfo
 
-		// get the auth mechanism and actions
-		authMechanism, authInfoActions := parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
-		clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
-
-		actions := append(authInfoActions, clusterActions...)
+		actions := make([]models.ServiceAccountAction, 0)
+		var authMechanism string
 
-		// if auth mechanism is unsupported, we'll skip it
-		if authMechanism == models.NotAvailable {
-			continue
-		} else if authMechanism == models.AWS {
-			// if the auth mechanism is AWS, we need to parse more explicitly
-			// for the cluster id
-			awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
+		if local {
+			authMechanism = models.Local
+		} else {
+			// get the auth mechanism and actions
+			authMechanism, actions = parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
+			clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
+			actions = append(actions, clusterActions...)
+
+			// if auth mechanism is unsupported, we'll skip it
+			if authMechanism == models.NotAvailable {
+				continue
+			} else if authMechanism == models.AWS {
+				// if the auth mechanism is AWS, we need to parse more explicitly
+				// for the cluster id
+				awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
+			}
 		}
 
 		// construct the raw kubeconfig that's relevant for that context
@@ -196,12 +207,21 @@ func parseClusterForActions(cluster *api.Cluster) (actions []models.ServiceAccou
 	actions = make([]models.ServiceAccountAction, 0)
 
 	if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
-		return []models.ServiceAccountAction{
-			models.ServiceAccountAction{
-				Name:     models.ClusterCADataAction,
+		actions = append(actions, models.ServiceAccountAction{
+			Name:     models.ClusterCADataAction,
+			Resolved: false,
+			Filename: cluster.CertificateAuthority,
+		})
+	}
+
+	serverURL, err := url.Parse(cluster.Server)
+
+	if err == nil {
+		if hostname := serverURL.Hostname(); hostname == "127.0.0.1" || hostname == "localhost" {
+			actions = append(actions, models.ServiceAccountAction{
+				Name:     models.ClusterLocalhostAction,
 				Resolved: false,
-				Filename: cluster.CertificateAuthority,
-			},
+			})
 		}
 	}
 
@@ -272,6 +292,10 @@ func GetClientConfigFromServiceAccount(
 	clusterID uint,
 	updateTokenCache UpdateTokenCacheFunc,
 ) (clientcmd.ClientConfig, error) {
+	if sa.AuthMechanism == models.Local {
+		return clientcmd.NewClientConfigFromBytes(sa.Kubeconfig)
+	}
+
 	apiConfig, err := createRawConfigFromServiceAccount(sa, clusterID, updateTokenCache)
 
 	if err != nil {

+ 79 - 40
internal/kubernetes/kubeconfig_test.go

@@ -79,7 +79,7 @@ var BasicContextAllowedTests = []kubeConfigTest{
 		expected: []models.Context{
 			models.Context{
 				Name:     "context-test",
-				Server:   "https://localhost",
+				Server:   "https://10.10.10.10",
 				Cluster:  "cluster-test",
 				User:     "test-admin",
 				Selected: true,
@@ -112,7 +112,7 @@ var BasicContextAllTests = []kubeConfigTest{
 		expected: []models.Context{
 			models.Context{
 				Name:     "context-test",
-				Server:   "https://localhost",
+				Server:   "https://10.10.10.10",
 				Cluster:  "cluster-test",
 				User:     "test-admin",
 				Selected: false,
@@ -153,7 +153,7 @@ func TestGetRestrictedClientConfig(t *testing.T) {
 		t.Fatalf("Fatal error: %s\n", err.Error())
 	}
 
-	if cluster, clusterFound := rawConf.Clusters["cluster-test"]; !clusterFound || cluster.Server != "https://localhost" {
+	if cluster, clusterFound := rawConf.Clusters["cluster-test"]; !clusterFound || cluster.Server != "https://10.10.10.10" {
 		t.Errorf("invalid cluster returned")
 	}
 
@@ -187,12 +187,31 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.X509,
 				Kubeconfig:      []byte(ClusterCAWithoutData),
 			},
 		},
 	},
+	saCandidatesTest{
+		name: "test cluster localhost",
+		raw:  []byte(ClusterLocalhost),
+		expected: []*models.ServiceAccountCandidate{
+			&models.ServiceAccountCandidate{
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "fix-cluster-localhost",
+						Resolved: false,
+					},
+				},
+				Kind:            "connector",
+				ClusterName:     "cluster-test",
+				ClusterEndpoint: "https://localhost",
+				AuthMechanism:   models.X509,
+				Kubeconfig:      []byte(ClusterLocalhost),
+			},
+		},
+	},
 	saCandidatesTest{
 		name: "x509 test with cert and key data",
 		raw:  []byte(x509WithData),
@@ -201,7 +220,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.X509,
 				Kubeconfig:      []byte(x509WithData),
 			},
@@ -221,7 +240,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.X509,
 				Kubeconfig:      []byte(x509WithoutCertData),
 			},
@@ -241,7 +260,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.X509,
 				Kubeconfig:      []byte(x509WithoutKeyData),
 			},
@@ -266,7 +285,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.X509,
 				Kubeconfig:      []byte(x509WithoutCertAndKeyData),
 			},
@@ -280,7 +299,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.Bearer,
 				Kubeconfig:      []byte(BearerTokenWithData),
 			},
@@ -300,7 +319,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.Bearer,
 				Kubeconfig:      []byte(BearerTokenWithoutData),
 			},
@@ -319,7 +338,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.GCP,
 				Kubeconfig:      []byte(GCPPlugin),
 			},
@@ -338,7 +357,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.AWS,
 				Kubeconfig:      []byte(AWSIamAuthenticatorExec),
 			},
@@ -357,7 +376,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.AWS,
 				Kubeconfig:      []byte(AWSEKSGetTokenExec),
 			},
@@ -377,7 +396,7 @@ var SACandidatesTests = []saCandidatesTest{
 				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.OIDC,
 				Kubeconfig:      []byte(OIDCAuthWithoutData),
 			},
@@ -391,7 +410,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.OIDC,
 				Kubeconfig:      []byte(OIDCAuthWithData),
 			},
@@ -405,7 +424,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
-				ClusterEndpoint: "https://localhost",
+				ClusterEndpoint: "https://10.10.10.10",
 				AuthMechanism:   models.Basic,
 				Kubeconfig:      []byte(BasicAuth),
 			},
@@ -413,9 +432,9 @@ var SACandidatesTests = []saCandidatesTest{
 	},
 }
 
-func TestGetServiceAccountCandidates(t *testing.T) {
+func TestGetServiceAccountCandidatesNonLocal(t *testing.T) {
 	for _, c := range SACandidatesTests {
-		result, err := kubernetes.GetServiceAccountCandidates(c.raw)
+		result, err := kubernetes.GetServiceAccountCandidates(c.raw, false)
 
 		if err != nil {
 			t.Fatalf("error occurred %v\n", err)
@@ -492,7 +511,7 @@ func TestGetServiceAccountCandidates(t *testing.T) {
 }
 
 func TestAWSClusterIDGuess(t *testing.T) {
-	result, err := kubernetes.GetServiceAccountCandidates([]byte(AWSIamAuthenticatorExec))
+	result, err := kubernetes.GetServiceAccountCandidates([]byte(AWSIamAuthenticatorExec), false)
 
 	if err != nil {
 		t.Fatalf("error occurred %v\n", err)
@@ -506,7 +525,7 @@ func TestAWSClusterIDGuess(t *testing.T) {
 		t.Errorf("Guess AWS cluster id failed: expected %s, got %s\n", "cluster-test-aws-id-guess", result[0].AWSClusterIDGuess)
 	}
 
-	result, err = kubernetes.GetServiceAccountCandidates([]byte(AWSEKSGetTokenExec))
+	result, err = kubernetes.GetServiceAccountCandidates([]byte(AWSEKSGetTokenExec), false)
 
 	if err != nil {
 		t.Fatalf("error occurred %v\n", err)
@@ -527,7 +546,7 @@ kind: Config
 preferences: {}
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: porter-test-1
 current-context: context-test
 users:
@@ -557,7 +576,7 @@ preferences: {}
 current-context: default
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: porter-test-1
 contexts:
 - context:
@@ -573,7 +592,7 @@ preferences: {}
 current-context: default
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: porter-test-1
 contexts:
 - context:
@@ -592,7 +611,7 @@ preferences: {}
 current-context: default
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: porter-test-1
 contexts:
 - context:
@@ -611,7 +630,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -628,7 +647,7 @@ kind: Config
 clusters:
 - name: cluster-test
   cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority: /fake/path/to/ca.pem
 contexts:
 - context:
@@ -643,6 +662,26 @@ users:
 current-context: context-test
 `
 
+const ClusterLocalhost string = `
+apiVersion: v1
+kind: Config
+clusters:
+- name: cluster-test
+  cluster:
+    server: https://localhost
+contexts:
+- context:
+    cluster: cluster-test
+    user: test-admin
+  name: context-test
+users:
+- name: test-admin
+  user:
+    client-certificate-data: LS0tLS1CRUdJTiBDRVJ=
+    client-key-data: LS0tLS1CRUdJTiBDRVJ=
+current-context: context-test
+`
+
 const x509WithData string = `
 apiVersion: v1
 kind: Config
@@ -650,7 +689,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -671,7 +710,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -692,7 +731,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -713,7 +752,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -734,7 +773,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -754,7 +793,7 @@ preferences: {}
 current-context: context-test
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
   name: cluster-test
 contexts:
 - context:
@@ -772,7 +811,7 @@ kind: Config
 clusters:
 - name: cluster-test
   cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
 users:
 - name: test-admin
@@ -791,7 +830,7 @@ const AWSIamAuthenticatorExec = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -818,7 +857,7 @@ const AWSEKSGetTokenExec = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -846,7 +885,7 @@ const OIDCAuthWithoutData = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -864,7 +903,7 @@ users:
       config:
         client-id: porter-api
         id-token: token
-        idp-issuer-url: https://localhost
+        idp-issuer-url: https://10.10.10.10
         idp-certificate-authority: /fake/path/to/ca.pem
       name: oidc
 `
@@ -873,7 +912,7 @@ const OIDCAuthWithData = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -891,7 +930,7 @@ users:
       config:
         client-id: porter-api
         id-token: token
-        idp-issuer-url: https://localhost
+        idp-issuer-url: https://10.10.10.10
         idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
       name: oidc
 `
@@ -900,7 +939,7 @@ const BasicAuth = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:

+ 14 - 7
internal/models/action.go

@@ -4,13 +4,14 @@ import "gorm.io/gorm"
 
 // Action names
 const (
-	ClusterCADataAction  string = "upload-cluster-ca-data"
-	ClientCertDataAction        = "upload-client-cert-data"
-	ClientKeyDataAction         = "upload-client-key-data"
-	OIDCIssuerDataAction        = "upload-oidc-idp-issuer-ca-data"
-	TokenDataAction             = "upload-token-data"
-	GCPKeyDataAction            = "upload-gcp-key-data"
-	AWSDataAction               = "upload-aws-data"
+	ClusterCADataAction    string = "upload-cluster-ca-data"
+	ClusterLocalhostAction        = "fix-cluster-localhost"
+	ClientCertDataAction          = "upload-client-cert-data"
+	ClientKeyDataAction           = "upload-client-key-data"
+	OIDCIssuerDataAction          = "upload-oidc-idp-issuer-ca-data"
+	TokenDataAction               = "upload-token-data"
+	GCPKeyDataAction              = "upload-gcp-key-data"
+	AWSDataAction                 = "upload-aws-data"
 )
 
 // ServiceAccountAction is an action that must be resolved to set up
@@ -59,6 +60,7 @@ type ServiceAccountAllActions struct {
 	Name string `json:"name"`
 
 	ClusterCAData      string `json:"cluster_ca_data,omitempty"`
+	ClusterHostname    string `json:"cluster_hostname,omitempty"`
 	ClientCertData     string `json:"client_cert_data,omitempty"`
 	ClientKeyData      string `json:"client_key_data,omitempty"`
 	OIDCIssuerCAData   string `json:"oidc_idp_issuer_ca_data,omitempty"`
@@ -87,6 +89,11 @@ var ServiceAccountActionInfos = map[string]ServiceAccountActionInfo{
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "cluster_ca_data",
 	},
+	"fix-cluster-localhost": ServiceAccountActionInfo{
+		Name:   ClusterLocalhostAction,
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "cluster_hostname",
+	},
 	"upload-client-cert-data": ServiceAccountActionInfo{
 		Name:   ClientCertDataAction,
 		Docs:   "https://github.com/porter-dev/porter",

+ 4 - 0
internal/models/serviceaccount.go

@@ -12,6 +12,7 @@ const (
 	OIDC                = "oidc"
 	GCP                 = "gcp-sa"
 	AWS                 = "aws-sa"
+	Local               = "local"
 	NotAvailable        = "n/a"
 )
 
@@ -139,6 +140,9 @@ type ServiceAccount struct {
 	OIDCCertificateAuthorityData []byte `json:"idp-certificate-authority-data"`
 	OIDCIDToken                  []byte `json:"id-token"`
 	OIDCRefreshToken             []byte `json:"refresh-token"`
+
+	// The raw kubeconfig, used by local auth mechanisms
+	Kubeconfig []byte `json:"kubeconfig"`
 }
 
 // ServiceAccountExternal is an external ServiceAccount to be shared over REST

+ 4 - 0
server/api/api.go

@@ -34,11 +34,13 @@ type App struct {
 	translator   *ut.Translator
 	cookieName   string
 	testing      bool
+	isLocal      bool
 	TestAgents   *TestAgents
 	GithubConfig *oauth2.Config
 }
 
 // New returns a new App instance
+// TODO -- this should accept an app/server config
 func New(
 	logger *lr.Logger,
 	db *gorm.DB,
@@ -47,6 +49,7 @@ func New(
 	store sessions.Store,
 	cookieName string,
 	testing bool,
+	isLocal bool,
 	githubConfig *oauth.Config,
 ) *App {
 	// for now, will just support the english translator from the
@@ -82,6 +85,7 @@ func New(
 		translator:   &trans,
 		cookieName:   cookieName,
 		testing:      testing,
+		isLocal:      isLocal,
 		TestAgents:   testAgents,
 		GithubConfig: oauthGithubConf,
 	}

+ 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, nil, repo, validator, store, appConf.Server.CookieName, true, nil)
+	app := api.New(logger, nil, repo, validator, store, appConf.Server.CookieName, true, false, nil)
 	r := router.New(app, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 
 	return &tester{

+ 8 - 1
server/api/project_handler.go

@@ -210,7 +210,7 @@ func (app *App) HandleCreateProjectSACandidates(w http.ResponseWriter, r *http.R
 	}
 
 	// convert the form to a ServiceAccountCandidate
-	saCandidates, err := form.ToServiceAccountCandidates()
+	saCandidates, err := form.ToServiceAccountCandidates(app.isLocal)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -355,6 +355,13 @@ func (app *App) HandleResolveSACandidateActions(w http.ResponseWriter, r *http.R
 				ClusterCAData:                action.ClusterCAData,
 			}
 
+			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
+		case models.ClusterLocalhostAction:
+			form := &forms.ClusterLocalhostAction{
+				ServiceAccountActionResolver: saResolverBase,
+				ClusterHostname:              action.ClusterHostname,
+			}
+
 			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
 		case models.ClientCertDataAction:
 			form := &forms.ClientCertDataAction{

+ 14 - 14
server/api/project_handler_test.go

@@ -129,7 +129,7 @@ var readProjectSATest = []*projTest{
 		endpoint:  "/api/projects/1/serviceAccounts/1",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}],"auth_mechanism":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"auth_mechanism":"oidc"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSABodyValidator,
@@ -153,7 +153,7 @@ var listProjectClustersTest = []*projTest{
 		endpoint:  "/api/projects/1/clusters",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}]`,
+		expBody:   `[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectClustersValidator,
@@ -176,7 +176,7 @@ var createProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
 		expStatus: http.StatusCreated,
-		expBody:   `[{"id":1,"actions":[],"created_sa_id":1,"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[],"created_sa_id":1,"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -231,7 +231,7 @@ var createProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
 		expStatus: http.StatusCreated,
-		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -255,7 +255,7 @@ var listProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -279,7 +279,7 @@ var resolveProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates/1/resolve",
 		body:      `[{"name": "upload-oidc-idp-issuer-ca-data", "oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}]`,
 		expStatus: http.StatusCreated,
-		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}],"auth_mechanism":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"auth_mechanism":"oidc"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSABodyValidator,
@@ -341,7 +341,7 @@ func initProjectSACandidate(tester *tester) {
 	}
 
 	// convert the form to a ServiceAccountCandidate
-	saCandidates, _ := form.ToServiceAccountCandidates()
+	saCandidates, _ := form.ToServiceAccountCandidates(false)
 
 	for _, saCandidate := range saCandidates {
 		tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
@@ -357,7 +357,7 @@ func initProjectSADefault(tester *tester) {
 	}
 
 	// convert the form to a ServiceAccountCandidate
-	saCandidates, _ := form.ToServiceAccountCandidates()
+	saCandidates, _ := form.ToServiceAccountCandidates(false)
 
 	for _, saCandidate := range saCandidates {
 		tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
@@ -430,15 +430,15 @@ func projectClustersValidator(c *projTest, tester *tester, t *testing.T) {
 	}
 }
 
-const OIDCAuthWithDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://localhost\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://localhost\n        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n      name: oidc`
+const OIDCAuthWithDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n      name: oidc`
 
-const OIDCAuthWithoutDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://localhost\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://localhost\n        idp-certificate-authority: /fake/path/to/ca.pem\n      name: oidc`
+const OIDCAuthWithoutDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority: /fake/path/to/ca.pem\n      name: oidc`
 
 const OIDCAuthWithoutData string = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -456,7 +456,7 @@ users:
       config:
         client-id: porter-api
         id-token: token
-        idp-issuer-url: https://localhost
+        idp-issuer-url: https://10.10.10.10
         idp-certificate-authority: /fake/path/to/ca.pem
       name: oidc
 `
@@ -465,7 +465,7 @@ const OIDCAuthWithData string = `
 apiVersion: v1
 clusters:
 - cluster:
-    server: https://localhost
+    server: https://10.10.10.10
     certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
   name: cluster-test
 contexts:
@@ -483,7 +483,7 @@ users:
       config:
         client-id: porter-api
         id-token: token
-        idp-issuer-url: https://localhost
+        idp-issuer-url: https://10.10.10.10
         idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
       name: oidc
 `