Sfoglia il codice sorgente

Merge branch 'kubeconfig' of https://github.com/porter-dev/porter into frontend-boilerplate

jusrhee 5 anni fa
parent
commit
dd3d4243f5

+ 18 - 4
go.mod

@@ -3,9 +3,12 @@ module github.com/porter-dev/porter
 go 1.14
 
 require (
+	github.com/Azure/go-autorest/autorest v0.11.1 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
+	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 // indirect
+	github.com/evanphx/json-patch v4.9.0+incompatible // indirect
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/go-chi/cors v1.1.1
@@ -13,28 +16,39 @@ require (
 	github.com/go-playground/universal-translator v0.17.0
 	github.com/go-playground/validator/v10 v10.3.0
 	github.com/go-test/deep v1.0.7
+	github.com/google/go-cmp v0.5.1
 	github.com/gorilla/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/jinzhu/gorm v1.9.16
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
+	github.com/json-iterator/go v1.1.10 // indirect
+	github.com/kr/pretty v0.2.0 // indirect
 	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.7 // indirect
 	github.com/pelletier/go-toml v1.8.1 // indirect
 	github.com/pkg/errors v0.9.1
 	github.com/rs/zerolog v1.20.0
-	github.com/stretchr/testify v1.5.1
+	github.com/sirupsen/logrus v1.6.0
+	github.com/stretchr/testify v1.6.1
 	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
 	golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
-	golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect
+	golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect
 	golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/go-playground/validator.v9 v9.31.0
 	gopkg.in/yaml.v2 v2.3.0
 	gorm.io/driver/postgres v1.0.2
 	gorm.io/gorm v1.20.2
-	k8s.io/apimachinery v0.19.2
-	k8s.io/client-go v0.0.0-20200917000235-cba7285b7f29
+	helm.sh/helm/v3 v3.3.4
+	k8s.io/api v0.18.8
+	k8s.io/apimachinery v0.18.8
+	k8s.io/cli-runtime v0.18.8
+	k8s.io/client-go v0.18.8
+	k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac // indirect
 	k8s.io/klog v1.0.0 // indirect
+	k8s.io/klog/v2 v2.2.0 // indirect
 	k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.0.1 // indirect
 )

File diff suppressed because it is too large
+ 593 - 0
go.sum


+ 2 - 2
internal/forms/user.go

@@ -61,8 +61,8 @@ func (luf *LoginUserForm) ToUser() (*models.User, error) {
 type UpdateUserForm struct {
 	WriteUserForm
 	ID              uint     `form:"required"`
-	RawKubeConfig   string   `json:"rawKubeConfig" form:"required"`
-	AllowedClusters []string `json:"allowedClusters" form:"required"`
+	RawKubeConfig   string   `json:"rawKubeConfig"`
+	AllowedClusters []string `json:"allowedClusters"`
 }
 
 // ToUser converts an UpdateUserForm to models.User by parsing the kubeconfig

+ 50 - 0
internal/helm/action_config.go

@@ -0,0 +1,50 @@
+package helm
+
+import (
+	"github.com/porter-dev/porter/internal/logger"
+
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/kube"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+)
+
+// NewActionConfig creates an action.Configuration, which can then be used to create Helm 3 actions.
+// Among other things, the action.Configuration controls which namespace the command is run against.
+func NewActionConfig(
+	l *logger.Logger,
+	newStorageDriver NewStorageDriver,
+	config *rest.Config,
+	clientset *kubernetes.Clientset,
+	namespace string,
+) (*action.Configuration, error) {
+	actionConfig := &action.Configuration{}
+	store := newStorageDriver(l, namespace, clientset)
+	restClientGetter := NewConfigFlagsFromCluster(namespace, config)
+	actionConfig.RESTClientGetter = restClientGetter
+	actionConfig.KubeClient = kube.New(restClientGetter)
+	actionConfig.Releases = store
+	actionConfig.Log = l.Printf
+	return actionConfig, nil
+}
+
+// NewConfigFlagsFromCluster returns ConfigFlags with default values set from within cluster.
+func NewConfigFlagsFromCluster(namespace string, clusterConfig *rest.Config) genericclioptions.RESTClientGetter {
+	impersonateGroup := []string{}
+
+	// CertFile and KeyFile must be nil for the BearerToken to be used for authentication and authorization instead of the pod's service account.
+	return &genericclioptions.ConfigFlags{
+		Insecure:         &clusterConfig.TLSClientConfig.Insecure,
+		Timeout:          stringptr("0"),
+		Namespace:        stringptr(namespace),
+		APIServer:        stringptr(clusterConfig.Host),
+		CAFile:           stringptr(clusterConfig.CAFile),
+		BearerToken:      stringptr(clusterConfig.BearerToken),
+		ImpersonateGroup: &impersonateGroup,
+	}
+}
+
+func stringptr(val string) *string {
+	return &val
+}

+ 30 - 0
internal/helm/agent.go

@@ -0,0 +1,30 @@
+package helm
+
+import (
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+// ListReleases lists releases based on a ListFilter
+func ListReleases(
+	actionConfig *action.Configuration,
+	namespace string,
+	filter *ListFilter,
+) ([]*release.Release, error) {
+	cmd := action.NewList(actionConfig)
+
+	filter.apply(cmd)
+
+	return cmd.Run()
+}
+
+// GetRelease returns the info of a release.
+func GetRelease(
+	actionConfig *action.Configuration,
+	name string,
+) (*release.Release, error) {
+	// Namespace is already known by the RESTClientGetter.
+	cmd := action.NewGet(actionConfig)
+
+	return cmd.Run(name)
+}

+ 173 - 0
internal/helm/agent_test.go

@@ -0,0 +1,173 @@
+package helm_test
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/logger"
+
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chartutil"
+	kubefake "helm.sh/helm/v3/pkg/kube/fake"
+	"helm.sh/helm/v3/pkg/release"
+	"helm.sh/helm/v3/pkg/storage"
+	"helm.sh/helm/v3/pkg/storage/driver"
+)
+
+func newActionConfigFixture(t *testing.T) *action.Configuration {
+	t.Helper()
+
+	l := logger.NewConsole(true)
+
+	return &action.Configuration{
+		Releases: storage.Init(driver.NewMemory()),
+		KubeClient: &kubefake.FailingKubeClient{
+			PrintingKubeClient: kubefake.PrintingKubeClient{
+				Out: ioutil.Discard,
+			},
+		},
+		Capabilities: chartutil.DefaultCapabilities,
+		Log:          l.Printf,
+	}
+}
+
+type releaseStub struct {
+	name         string
+	namespace    string
+	version      int
+	chartVersion string
+	status       release.Status
+}
+
+// makeReleases adds a slice of releases to the configured storage.
+func makeReleases(t *testing.T, actionConfig *action.Configuration, rels []releaseStub) {
+	t.Helper()
+	storage := actionConfig.Releases
+	for _, r := range rels {
+		rel := &release.Release{
+			Name:      r.name,
+			Namespace: r.namespace,
+			Version:   r.version,
+			Info: &release.Info{
+				Status: r.status,
+			},
+			Chart: &chart.Chart{
+				Metadata: &chart.Metadata{
+					Version: r.chartVersion,
+					Icon:    "https://example.com/icon.png",
+				},
+			},
+		}
+		err := storage.Create(rel)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+}
+
+func compareReleaseToStubs(t *testing.T, releases []*release.Release, stubs []releaseStub) {
+	t.Helper()
+
+	if len(releases) != len(stubs) {
+		t.Fatalf("length of release %v doesn't match length of stub %v\n",
+			len(releases), len(stubs))
+	}
+
+	for i, r := range releases {
+		if r.Name != stubs[i].name {
+			t.Errorf("Release name %v doesn't match stub name %v\n",
+				r.Name, stubs[i].name)
+		}
+
+		if r.Namespace != stubs[i].namespace {
+			t.Errorf("Release namespace %v doesn't match stub namespace %v\n",
+				r.Namespace, stubs[i].namespace)
+		}
+
+		if r.Info.Status != stubs[i].status {
+			t.Errorf("Release namespace %v doesn't match stub namespace %v\n",
+				r.Info.Status, stubs[i].status)
+		}
+
+		if r.Version != stubs[i].version {
+			t.Errorf("Release version %v doesn't match stub version %v\n",
+				r.Version, stubs[i].version)
+		}
+
+		if r.Chart.Metadata.Version != stubs[i].chartVersion {
+			t.Errorf("Release metadata version %v doesn't match stub chart version %v\n",
+				r.Chart.Metadata.Version, stubs[i].chartVersion)
+		}
+	}
+
+	return
+}
+
+type listReleaseTest struct {
+	name      string
+	namespace string
+	filter    *helm.ListFilter
+	releases  []releaseStub
+	expRes    []releaseStub
+}
+
+var listReleaseTests = []listReleaseTest{
+	listReleaseTest{
+		name:      "simple test across namespaces, should sort by name",
+		namespace: "",
+		filter: &helm.ListFilter{
+			Namespace:    "",
+			Limit:        20,
+			Skip:         0,
+			ByDate:       false,
+			StatusFilter: []string{"deployed"},
+		},
+		releases: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusDeployed},
+			releaseStub{"not-in-default-namespace", "other", 1, "1.0.2", release.StatusDeployed},
+		},
+		expRes: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"not-in-default-namespace", "other", 1, "1.0.2", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusDeployed},
+		},
+	},
+	listReleaseTest{
+		name:      "simple test limit",
+		namespace: "",
+		filter: &helm.ListFilter{
+			Namespace:    "",
+			Limit:        2,
+			Skip:         0,
+			ByDate:       false,
+			StatusFilter: []string{"deployed"},
+		},
+		releases: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"not-in-default-namespace", "other", 1, "1.0.1", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.2", release.StatusDeployed},
+		},
+		expRes: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"not-in-default-namespace", "other", 1, "1.0.1", release.StatusDeployed},
+		},
+	},
+}
+
+func TestListReleases(t *testing.T) {
+	for _, tc := range listReleaseTests {
+		actionConfig := newActionConfigFixture(t)
+		makeReleases(t, actionConfig, tc.releases)
+		actionConfig.Releases.Driver.(*driver.Memory).SetNamespace(tc.namespace)
+
+		releases, err := helm.ListReleases(actionConfig, tc.namespace, tc.filter)
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		compareReleaseToStubs(t, releases, tc.expRes)
+	}
+}

+ 55 - 0
internal/helm/driver.go

@@ -0,0 +1,55 @@
+package helm
+
+// Helm contains support for several different storage drivers.
+//
+// This includes (as of October 2020):
+// - configmap
+// - secret
+// - memory
+// - postgres
+//
+// This file implements first-class support for each driver type, and integrates with the
+// logger.
+
+import (
+	"github.com/porter-dev/porter/internal/logger"
+
+	"helm.sh/helm/v3/pkg/storage"
+	"helm.sh/helm/v3/pkg/storage/driver"
+	"k8s.io/client-go/kubernetes"
+)
+
+// NewStorageDriver is a function type for returning a new storage driver
+type NewStorageDriver func(l *logger.Logger, namespace string, clientset *kubernetes.Clientset) *storage.Storage
+
+// NewSecretStorageDriver returns a storage using the Secret driver.
+func NewSecretStorageDriver(
+	l *logger.Logger,
+	namespace string,
+	clientset *kubernetes.Clientset,
+) *storage.Storage {
+	d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
+	d.Log = l.Printf
+	return storage.Init(d)
+}
+
+// NewConfigMapsStorageDriver returns a storage using the ConfigMap driver.
+func NewConfigMapsStorageDriver(
+	l *logger.Logger,
+	namespace string,
+	clientset *kubernetes.Clientset,
+) *storage.Storage {
+	d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
+	d.Log = l.Printf
+	return storage.Init(d)
+}
+
+// NewMemoryStorageDriver returns a storage using the In-Memory driver.
+func NewMemoryStorageDriver(
+	l *logger.Logger,
+	namespace string,
+	clientset *kubernetes.Clientset,
+) *storage.Storage {
+	d := driver.NewMemory()
+	return storage.Init(d)
+}

+ 48 - 0
internal/helm/filter.go

@@ -0,0 +1,48 @@
+package helm
+
+import (
+	"helm.sh/helm/v3/pkg/action"
+)
+
+// ListFilter is a struct that represents the various filter options used for
+// retrieving the releases
+type ListFilter struct {
+	Namespace    string   `json:"namespace"`
+	Limit        int      `json:"limit"`
+	Skip         int      `json:"skip"`
+	ByDate       bool     `json:"byDate"`
+	StatusFilter []string `json:"statusFilter"`
+}
+
+// listStatesFromNames accepts the following list of names:
+//
+// "deployed", "uninstalled", "uninstalling", "pending", "pending_upgrade",
+// "pending_rollback", "superseded", "failed"
+//
+// It returns an action.ListStates to be used in an action.List as filters for
+// releases in a certain state.
+func (h *ListFilter) listStatesFromNames() action.ListStates {
+	var res action.ListStates = 0
+
+	for _, name := range h.StatusFilter {
+		res = res | res.FromName(name)
+	}
+
+	return res
+}
+
+// apply sets the ListFilter options for an action.List
+func (h *ListFilter) apply(list *action.List) {
+	if h.Namespace == "" {
+		list.AllNamespaces = true
+	}
+
+	list.Limit = h.Limit
+	list.Offset = h.Skip
+
+	list.StateMask = h.listStatesFromNames()
+
+	if h.ByDate {
+		list.ByDate = true
+	}
+}

+ 19 - 0
internal/kubernetes/agent.go

@@ -0,0 +1,19 @@
+package kubernetes
+
+import (
+	"context"
+
+	v1 "k8s.io/api/core/v1"
+	v1Machinery "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+// ListNamespaces simply lists namespaces
+func ListNamespaces(clientset *kubernetes.Clientset) *v1.NamespaceList {
+	namespaces, _ := clientset.CoreV1().Namespaces().List(
+		context.TODO(),
+		v1Machinery.ListOptions{},
+	)
+
+	return namespaces
+}

+ 18 - 0
internal/kubernetes/client.go

@@ -0,0 +1,18 @@
+package kubernetes
+
+import (
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/tools/clientcmd"
+)
+
+// GetClientsetFromConfig is a simple wrapper that returns a *kubernetes.Clientset based on
+// a clientcmd.ClientConfig
+func GetClientsetFromConfig(conf clientcmd.ClientConfig) (*kubernetes.Clientset, error) {
+	clientConf, err := conf.ClientConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return kubernetes.NewForConfig(clientConf)
+}

+ 102 - 93
internal/kubernetes/kubeconfig.go

@@ -2,65 +2,117 @@ package kubernetes
 
 import (
 	"github.com/porter-dev/porter/internal/models"
-	"gopkg.in/yaml.v2"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
 )
 
-// KubeConfigCluster represents the cluster field in a kubeconfig
-type KubeConfigCluster struct {
-	Cluster struct {
-		Server string `yaml:"server"`
-	} `yaml:"cluster"`
-	Name string `yaml:"name"`
-}
+// GetRestrictedClientConfigFromBytes returns a clientcmd.ClientConfig from a raw kubeconfig,
+// a context name, and the set of allowed clusters.
+func GetRestrictedClientConfigFromBytes(
+	bytes []byte,
+	contextName string,
+	allowedClusters []string,
+) (clientcmd.ClientConfig, error) {
+	config, err := clientcmd.NewClientConfigFromBytes(bytes)
 
-// KubeConfigContext represents the context field in a kubeconfig
-type KubeConfigContext struct {
-	Context struct {
-		Cluster string `yaml:"cluster"`
-		User    string `yaml:"user"`
-	} `yaml:"context"`
-	Name string `yaml:"name"`
-}
+	if err != nil {
+		return nil, err
+	}
+
+	rawConf, err := config.RawConfig()
+
+	if err != nil {
+		return nil, err
+	}
 
-// KubeConfigUser represents the user field in a kubeconfig
-type KubeConfigUser struct {
-	Name string `yaml:"name"`
+	// grab a copy to get the pointer and set clusters, authinfos, and contexts to empty
+	copyConf := rawConf.DeepCopy()
+
+	copyConf.Clusters = make(map[string]*api.Cluster)
+	copyConf.AuthInfos = make(map[string]*api.AuthInfo)
+	copyConf.Contexts = make(map[string]*api.Context)
+	copyConf.CurrentContext = contextName
+
+	// put allowed clusters in a map
+	aClusterMap := createAllowedClusterMap(allowedClusters)
+
+	// discover all allowed clusters
+	for contextName, context := range rawConf.Contexts {
+		userName := context.AuthInfo
+		clusterName := context.Cluster
+		authInfo, userFound := rawConf.AuthInfos[userName]
+		cluster, clusterFound := rawConf.Clusters[clusterName]
+
+		// make sure the cluster is "allowed"
+		_, aClusterFound := aClusterMap[clusterName]
+
+		if userFound && clusterFound && aClusterFound {
+			copyConf.Clusters[clusterName] = cluster
+			copyConf.AuthInfos[userName] = authInfo
+			copyConf.Contexts[contextName] = context
+		}
+	}
+
+	// validate the copyConf and create a ClientConfig
+	err = clientcmd.Validate(*copyConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	clientConf := clientcmd.NewDefaultClientConfig(*copyConf, &clientcmd.ConfigOverrides{})
+
+	return clientConf, nil
 }
 
-// KubeConfig represents an unmarshaled kubeconfig
-type KubeConfig struct {
-	CurrentContext string              `yaml:"current-context"`
-	Clusters       []KubeConfigCluster `yaml:"clusters"`
-	Contexts       []KubeConfigContext `yaml:"contexts"`
-	Users          []KubeConfigUser    `yaml:"users"`
+// GetAllClusterConfigsFromBytes converts a raw string to a set of ClusterConfigs
+// by unmarshaling and calling (*KubeConfig).ToAllClusterConfigs
+func GetAllClusterConfigsFromBytes(bytes []byte) ([]models.ClusterConfig, error) {
+	config, err := clientcmd.NewClientConfigFromBytes(bytes)
+
+	if err != nil {
+		return nil, err
+	}
+
+	rawConf, err := config.RawConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	err = clientcmd.Validate(rawConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	clusters := toAllClusterConfigs(&rawConf)
+
+	return clusters, nil
 }
 
 // GetAllowedClusterConfigsFromBytes converts a raw string to a set of ClusterConfigs
 // by unmarshaling and calling (*KubeConfig).ToAllowedClusterConfigs
 func GetAllowedClusterConfigsFromBytes(bytes []byte, allowedClusters []string) ([]models.ClusterConfig, error) {
-	conf := KubeConfig{}
-	err := yaml.Unmarshal(bytes, &conf)
+	config, err := clientcmd.NewClientConfigFromBytes(bytes)
 
 	if err != nil {
 		return nil, err
 	}
 
-	clusters := conf.toAllowedClusterConfigs(allowedClusters)
+	rawConf, err := config.RawConfig()
 
-	return clusters, nil
-}
+	if err != nil {
+		return nil, err
+	}
 
-// GetAllClusterConfigsFromBytes converts a raw string to a set of ClusterConfigs
-// by unmarshaling and calling (*KubeConfig).ToAllClusterConfigs
-func GetAllClusterConfigsFromBytes(bytes []byte) ([]models.ClusterConfig, error) {
-	conf := KubeConfig{}
-	err := yaml.Unmarshal(bytes, &conf)
+	err = clientcmd.Validate(rawConf)
 
 	if err != nil {
 		return nil, err
 	}
 
-	clusters := conf.toAllClusterConfigs()
+	clusters := toAllowedClusterConfigs(&rawConf, allowedClusters)
 
 	return clusters, nil
 }
@@ -69,23 +121,18 @@ func GetAllClusterConfigsFromBytes(bytes []byte) ([]models.ClusterConfig, error)
 // joining users and clusters on the context.
 //
 // It accepts a list of cluster names that the user wishes to connect to
-func (k *KubeConfig) toAllowedClusterConfigs(allowedClusters []string) []models.ClusterConfig {
+func toAllowedClusterConfigs(rawConf *api.Config, allowedClusters []string) []models.ClusterConfig {
 	clusters := make([]models.ClusterConfig, 0)
 
-	// convert clusters, contexts, and users to maps for fast lookup
-	clusterMap := k.createClusterMap()
-	contextMap := k.createContextMap()
-	userMap := k.createUserMap()
-
 	// put allowed clusters in map
 	aClusterMap := createAllowedClusterMap(allowedClusters)
 
 	// iterate through context maps and link to a user-cluster pair
-	for contextName, context := range contextMap {
-		userName := context.Context.User
-		clusterName := context.Context.Cluster
-		_, userFound := userMap[userName]
-		cluster, clusterFound := clusterMap[clusterName]
+	for contextName, context := range rawConf.Contexts {
+		userName := context.AuthInfo
+		clusterName := context.Cluster
+		_, userFound := rawConf.AuthInfos[userName]
+		cluster, clusterFound := rawConf.Clusters[clusterName]
 
 		// make sure the cluster is "allowed"
 		_, aClusterFound := aClusterMap[clusterName]
@@ -93,7 +140,7 @@ func (k *KubeConfig) toAllowedClusterConfigs(allowedClusters []string) []models.
 		if userFound && clusterFound && aClusterFound {
 			clusters = append(clusters, models.ClusterConfig{
 				Name:    clusterName,
-				Server:  cluster.Cluster.Server,
+				Server:  cluster.Server,
 				Context: contextName,
 				User:    userName,
 			})
@@ -105,25 +152,20 @@ func (k *KubeConfig) toAllowedClusterConfigs(allowedClusters []string) []models.
 
 // toAllClusterConfigs converts a KubeConfig to a set of ClusterConfigs by
 // joining users and clusters on the context.
-func (k *KubeConfig) toAllClusterConfigs() []models.ClusterConfig {
+func toAllClusterConfigs(rawConf *api.Config) []models.ClusterConfig {
 	clusters := make([]models.ClusterConfig, 0)
 
-	// convert clusters, contexts, and users to maps for fast lookup
-	clusterMap := k.createClusterMap()
-	contextMap := k.createContextMap()
-	userMap := k.createUserMap()
-
 	// iterate through context maps and link to a user-cluster pair
-	for contextName, context := range contextMap {
-		userName := context.Context.User
-		clusterName := context.Context.Cluster
-		_, userFound := userMap[userName]
-		cluster, clusterFound := clusterMap[clusterName]
+	for contextName, context := range rawConf.Contexts {
+		userName := context.AuthInfo
+		clusterName := context.Cluster
+		_, userFound := rawConf.AuthInfos[userName]
+		cluster, clusterFound := rawConf.Clusters[clusterName]
 
 		if userFound && clusterFound {
 			clusters = append(clusters, models.ClusterConfig{
 				Name:    clusterName,
-				Server:  cluster.Cluster.Server,
+				Server:  cluster.Server,
 				Context: contextName,
 				User:    userName,
 			})
@@ -143,36 +185,3 @@ func createAllowedClusterMap(clusters []string) map[string]string {
 
 	return aClusterMap
 }
-
-// createClusterMap creates a map from a cluster name to a KubeConfigCluster object
-func (k *KubeConfig) createClusterMap() map[string]KubeConfigCluster {
-	clusterMap := make(map[string]KubeConfigCluster)
-
-	for _, cluster := range k.Clusters {
-		clusterMap[cluster.Name] = cluster
-	}
-
-	return clusterMap
-}
-
-// createContextMap creates a map from a context name to a KubeConfigContext object
-func (k *KubeConfig) createContextMap() map[string]KubeConfigContext {
-	contextMap := make(map[string]KubeConfigContext)
-
-	for _, context := range k.Contexts {
-		contextMap[context.Name] = context
-	}
-
-	return contextMap
-}
-
-// createUserMap creates a map from a user name to a KubeConfigUser object
-func (k *KubeConfig) createUserMap() map[string]KubeConfigUser {
-	userMap := make(map[string]KubeConfigUser)
-
-	for _, user := range k.Users {
-		userMap[user.Name] = user
-	}
-
-	return userMap
-}

+ 76 - 47
internal/kubernetes/kubeconfig_test.go

@@ -2,76 +2,76 @@ package kubernetes_test
 
 import (
 	"reflect"
+	"strings"
 	"testing"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 )
 
-type KubeConfigTest struct {
+type kubeConfigTest struct {
 	msg             string
 	raw             []byte
 	allowedClusters []string
 	expected        []models.ClusterConfig
 }
 
-var MissingFieldsTest = []KubeConfigTest{
-	KubeConfigTest{
-		msg:             "no fields at all",
+type kubeConfigTestValidateError struct {
+	msg             string
+	raw             []byte
+	allowedClusters []string
+	contextName     string
+	errorContains   string // a string that the error message should contain
+}
+
+var ValidateErrorTests = []kubeConfigTestValidateError{
+	kubeConfigTestValidateError{
+		msg:             "No configuration",
 		raw:             []byte(""),
 		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
+		contextName:     "",
+		errorContains:   "invalid configuration: no configuration has been provided",
 	},
-	KubeConfigTest{
-		msg:             "no contexts to join",
+	kubeConfigTestValidateError{
+		msg:             "Context name does not exist",
 		raw:             []byte(noContexts),
-		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
+		allowedClusters: []string{"porter-test-1"},
+		contextName:     "context-test",
+		errorContains:   "invalid configuration: context was not found for specified context: context-test",
 	},
-	KubeConfigTest{
-		msg:             "no clusters to join",
+	kubeConfigTestValidateError{
+		msg:             "Cluster to join does not exist",
 		raw:             []byte(noClusters),
-		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
+		allowedClusters: []string{"porter-test-1"},
+		contextName:     "context-test",
+		errorContains:   "invalid configuration: context was not found for specified context: context-test",
 	},
-	KubeConfigTest{
-		msg:             "no users to join",
+	kubeConfigTestValidateError{
+		msg:             "User to join does not exist",
 		raw:             []byte(noUsers),
-		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
-	},
-	KubeConfigTest{
-		msg:             "no cluster contexts to join",
-		raw:             []byte(noContextClusters),
-		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
-	},
-	KubeConfigTest{
-		msg:             "no cluster users to join",
-		raw:             []byte(noContextUsers),
-		allowedClusters: []string{},
-		expected:        []models.ClusterConfig{},
+		allowedClusters: []string{"porter-test-1"},
+		contextName:     "context-test",
+		errorContains:   "invalid configuration: context was not found for specified context: context-test",
 	},
 }
 
-func TestMissingFields(t *testing.T) {
-	for _, c := range MissingFieldsTest {
-		res, err := kubernetes.GetAllowedClusterConfigsFromBytes(c.raw, c.allowedClusters)
+func TestValidateErrors(t *testing.T) {
+	for _, c := range ValidateErrorTests {
 
-		if err != nil {
-			t.Fatalf("Testing %s returned an error: %v\n", c.msg, err.Error())
-		}
+		_, err := kubernetes.GetRestrictedClientConfigFromBytes(c.raw, c.contextName, c.allowedClusters)
 
-		isEqual := reflect.DeepEqual(c.expected, res)
+		if err == nil {
+			t.Fatalf("Testing %s did not return an error\n", c.msg)
+		}
 
-		if !isEqual {
-			t.Errorf("Testing: %s, Expected: %v, Got: %v\n", c.msg, c.expected, res)
+		if !strings.Contains(err.Error(), c.errorContains) {
+			t.Errorf("Testing %s -- Error was:\n \"%s\" \n It did not contain string \"%s\"\n", c.msg, err.Error(), c.errorContains)
 		}
 	}
 }
 
-var NoAllowedClustersTests = []KubeConfigTest{
-	KubeConfigTest{
+var NoAllowedClustersTests = []kubeConfigTest{
+	kubeConfigTest{
 		msg:             "basic test",
 		raw:             []byte(basic),
 		allowedClusters: []string{},
@@ -95,8 +95,8 @@ func TestNoAllowedClusters(t *testing.T) {
 	}
 }
 
-var BasicClustersAllowedTests = []KubeConfigTest{
-	KubeConfigTest{
+var BasicClustersAllowedTests = []kubeConfigTest{
+	kubeConfigTest{
 		msg:             "basic test",
 		raw:             []byte(basic),
 		allowedClusters: []string{"cluster-test"},
@@ -127,8 +127,8 @@ func TestBasicAllowed(t *testing.T) {
 	}
 }
 
-var BasicClustersAllTests = []KubeConfigTest{
-	KubeConfigTest{
+var BasicClustersAllTests = []kubeConfigTest{
+	kubeConfigTest{
 		msg:             "basic test",
 		raw:             []byte(basic),
 		allowedClusters: []string{"cluster-test"},
@@ -159,6 +159,35 @@ func TestBasicAll(t *testing.T) {
 	}
 }
 
+func TestGetRestrictedClientConfig(t *testing.T) {
+	clusters := []string{"cluster-test"}
+	contextName := "context-test"
+
+	clientConf, err := kubernetes.GetRestrictedClientConfigFromBytes([]byte(basic), contextName, clusters)
+
+	if err != nil {
+		t.Fatalf("Fatal error: %s\n", err.Error())
+	}
+
+	rawConf, err := clientConf.RawConfig()
+
+	if err != nil {
+		t.Fatalf("Fatal error: %s\n", err.Error())
+	}
+
+	if cluster, clusterFound := rawConf.Clusters["cluster-test"]; !clusterFound || cluster.Server != "https://localhost" {
+		t.Errorf("invalid cluster returned")
+	}
+
+	if _, contextFound := rawConf.Contexts["context-test"]; !contextFound {
+		t.Errorf("invalid context returned")
+	}
+
+	if _, authInfoFound := rawConf.AuthInfos["test-admin"]; !authInfoFound {
+		t.Errorf("invalid auth info returned")
+	}
+}
+
 const noContexts string = `
 apiVersion: v1
 kind: Config
@@ -167,7 +196,7 @@ clusters:
 - cluster:
     server: https://localhost
   name: porter-test-1
-current-context: default
+current-context: context-test
 users:
 - name: test-admin
   user:
@@ -177,7 +206,7 @@ const noClusters string = `
 apiVersion: v1
 kind: Config
 preferences: {}
-current-context: default
+current-context: context-test
 contexts:
 - context:
     cluster: porter-test-1
@@ -246,7 +275,7 @@ const basic string = `
 apiVersion: v1
 kind: Config
 preferences: {}
-current-context: default
+current-context: context-test
 clusters:
 - cluster:
     server: https://localhost

+ 1 - 1
internal/models/cluster_configs_test.go

@@ -3,8 +3,8 @@ package models_test
 import (
 	"testing"
 
-	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 
 func TestClusterConfigExternalize(t *testing.T) {

+ 1 - 1
internal/models/user_test.go

@@ -3,8 +3,8 @@ package models_test
 import (
 	"testing"
 
-	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 
 func TestUserExternalize(t *testing.T) {

+ 12 - 0
server/api/chart_handler.go

@@ -0,0 +1,12 @@
+package api
+
+import "net/http"
+
+// TODO -- IMPLEMENT
+func (app *App) HandleListCharts(w http.ResponseWriter, r *http.Request) {
+	// get the user id
+
+	// create a client config using the app's helm/kubernetes agents
+
+	// call the list charts method
+}

+ 5 - 5
server/api/user_handler_test.go

@@ -86,7 +86,7 @@ func initUserWithClusters(tester *tester) {
 			User:    "test-admin",
 		},
 	}
-	user.RawKubeConfig = []byte("apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin")
+	user.RawKubeConfig = []byte("apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin")
 
 	tester.repo.User.UpdateUser(user)
 }
@@ -394,7 +394,7 @@ var readUserTests = []*userTest{
 		endpoint:  "/api/users/1",
 		body:      "",
 		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"email":"belanger@getporter.dev","clusters":[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`,
+		expBody:   `{"id":1,"email":"belanger@getporter.dev","clusters":[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`,
 		useCookie: true,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			UserModelBodyValidator,
@@ -497,7 +497,7 @@ var updateUserTests = []*userTest{
 		msg:       "Update user successful",
 		method:    "PUT",
 		endpoint:  "/api/users/1",
-		body:      `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
+		body:      `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
 		expStatus: http.StatusNoContent,
 		expBody:   "",
 		useCookie: true,
@@ -522,7 +522,7 @@ var updateUserTests = []*userTest{
 				expBody := &models.UserExternal{}
 
 				json.Unmarshal(rr2.Body.Bytes(), gotBody)
-				json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`), expBody)
+				json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`), expBody)
 
 				if !reflect.DeepEqual(gotBody, expBody) {
 					t.Errorf("%s, handler returned wrong body: got %v want %v",
@@ -538,7 +538,7 @@ var updateUserTests = []*userTest{
 		msg:       "Update user invalid id",
 		method:    "PUT",
 		endpoint:  "/api/users/alsdfjk",
-		body:      `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
+		body:      `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
 		expStatus: http.StatusForbidden,
 		expBody:   http.StatusText(http.StatusForbidden) + "\n",
 		validators: []func(c *userTest, tester *tester, t *testing.T){

Some files were not shown because too many files changed in this diff