ソースを参照

download server release from github

Alexander Belanger 5 年 前
コミット
a4877bc35d

+ 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
 `

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

@@ -83,6 +83,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 +264,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()
 

+ 20 - 0
cli/cmd/server.go

@@ -3,6 +3,9 @@ package cmd
 import (
 	"fmt"
 	"os"
+	"path/filepath"
+
+	"github.com/porter-dev/porter/cli/cmd/github"
 
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/docker"
@@ -23,6 +26,21 @@ 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) {
+		porterDir := filepath.Join(home, ".porter")
+
+		err := github.DownloadLatestServerRelease(porterDir)
+
+		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",
@@ -62,6 +80,8 @@ var stopCmd = &cobra.Command{
 }
 
 func init() {
+	rootCmd.AddCommand(testCmd)
+
 	rootCmd.AddCommand(serverCmd)
 
 	serverCmd.AddCommand(startCmd)

+ 19 - 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) {

+ 42 - 0
internal/forms/action.go

@@ -2,6 +2,7 @@ package forms
 
 import (
 	"encoding/base64"
+	"net/url"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
@@ -179,6 +180,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

+ 85 - 0
internal/forms/action_test.go

@@ -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))
+
+	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)
@@ -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

+ 15 - 5
internal/kubernetes/kubeconfig.go

@@ -3,6 +3,7 @@ package kubernetes
 import (
 	"context"
 	"errors"
+	"net/url"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
@@ -196,12 +197,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,
-			},
+			})
 		}
 	}
 

+ 75 - 36
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),
 			},
@@ -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",

+ 7 - 0
server/api/project_handler.go

@@ -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{

+ 12 - 12
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,
@@ -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
 `