mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feature(kubeconfig): access to all kube environment contexts from within the Portainer UI [EE-1727] (#5966)
This commit is contained in:
parent
c0a4727114
commit
6be1ff4d9c
22 changed files with 404 additions and 434 deletions
|
@ -1,66 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
)
|
||||
|
||||
// GetKubeConfig returns kubeconfig for the current user based on:
|
||||
// - portainer server url
|
||||
// - portainer user bearer token
|
||||
// - portainer token data - which maps to k8s service account
|
||||
func (kcl *KubeClient) GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *portainer.TokenData) (*clientV1.Config, error) {
|
||||
serviceAccount, err := kcl.GetServiceAccount(tokenData)
|
||||
if err != nil {
|
||||
errText := fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username)
|
||||
return nil, fmt.Errorf("%s; err=%w", errText, err)
|
||||
}
|
||||
|
||||
kubeconfig := generateKubeconfig(apiServerURL, bearerToken, serviceAccount.Name)
|
||||
|
||||
return kubeconfig, nil
|
||||
}
|
||||
|
||||
// generateKubeconfig will generate and return kubeconfig resource - usable by `kubectl` cli
|
||||
// which will allow the client to connect directly to k8s server environment(endpoint) via portainer (proxy)
|
||||
func generateKubeconfig(apiServerURL, bearerToken, serviceAccountName string) *clientV1.Config {
|
||||
const (
|
||||
KubeConfigPortainerContext = "portainer-ctx"
|
||||
KubeConfigPortainerCluster = "portainer-cluster"
|
||||
)
|
||||
|
||||
return &clientV1.Config{
|
||||
APIVersion: "v1",
|
||||
Kind: "Config",
|
||||
CurrentContext: KubeConfigPortainerContext,
|
||||
Contexts: []clientV1.NamedContext{
|
||||
{
|
||||
Name: KubeConfigPortainerContext,
|
||||
Context: clientV1.Context{
|
||||
AuthInfo: serviceAccountName,
|
||||
Cluster: KubeConfigPortainerCluster,
|
||||
},
|
||||
},
|
||||
},
|
||||
Clusters: []clientV1.NamedCluster{
|
||||
{
|
||||
Name: KubeConfigPortainerCluster,
|
||||
Cluster: clientV1.Cluster{
|
||||
Server: apiServerURL,
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthInfos: []clientV1.NamedAuthInfo{
|
||||
{
|
||||
Name: serviceAccountName,
|
||||
AuthInfo: clientV1.AuthInfo{
|
||||
Token: bearerToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kfake "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func Test_GetKubeConfig(t *testing.T) {
|
||||
|
||||
t.Run("returns error if SA non-existent", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: 1,
|
||||
Role: portainer.AdministratorRole,
|
||||
Username: portainerClusterAdminServiceAccountName,
|
||||
}
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err == nil {
|
||||
t.Error("GetKubeConfig should fail as service account does not exist")
|
||||
}
|
||||
if k8sErr := errors.Unwrap(err); !k8serrors.IsNotFound(k8sErr) {
|
||||
t.Error("GetKubeConfig should fail with service account not found k8s error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successfully obtains kubeconfig for cluster admin", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
Username: portainerClusterAdminServiceAccountName,
|
||||
}
|
||||
serviceAccount := &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: tokenData.Username},
|
||||
}
|
||||
|
||||
k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetKubeConfig should succeed; err=%s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successfully obtains kubeconfig for standard user", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: 1,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
nonAdminUserName := userServiceAccountName(int(tokenData.ID), k.instanceID)
|
||||
serviceAccount := &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: nonAdminUserName},
|
||||
}
|
||||
|
||||
k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetKubeConfig should succeed; err=%s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_generateKubeconfig(t *testing.T) {
|
||||
apiServerURL, bearerToken, serviceAccountName := "localhost", "test-token", "test-user"
|
||||
|
||||
t.Run("generates Config resource kind", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "Config"
|
||||
if config.Kind != want {
|
||||
t.Errorf("generateKubeconfig resource kind should be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates v1 version", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "v1"
|
||||
if config.APIVersion != want {
|
||||
t.Errorf("generateKubeconfig api version should be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates single entry context cluster and authinfo", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if len(config.Contexts) != 1 {
|
||||
t.Error("generateKubeconfig should generate single context configuration")
|
||||
}
|
||||
if len(config.Clusters) != 1 {
|
||||
t.Error("generateKubeconfig should generate single cluster configuration")
|
||||
}
|
||||
if len(config.AuthInfos) != 1 {
|
||||
t.Error("generateKubeconfig should generate single user configuration")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets default context appropriately", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "portainer-ctx"
|
||||
if config.CurrentContext != want {
|
||||
t.Errorf("generateKubeconfig set cluster to be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates cluster with InsecureSkipTLSVerify to be set to true", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if config.Clusters[0].Cluster.InsecureSkipTLSVerify != true {
|
||||
t.Error("generateKubeconfig default cluster InsecureSkipTLSVerify should be true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should contain passed in value", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if config.Clusters[0].Cluster.Server != apiServerURL {
|
||||
t.Errorf("generateKubeconfig default cluster server url should be %s", apiServerURL)
|
||||
}
|
||||
|
||||
if config.AuthInfos[0].Name != serviceAccountName {
|
||||
t.Errorf("generateKubeconfig default authinfo name should be %s", serviceAccountName)
|
||||
}
|
||||
|
||||
if config.AuthInfos[0].AuthInfo.Token != bearerToken {
|
||||
t.Errorf("generateKubeconfig default authinfo user token should be %s", bearerToken)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue