Skip to content

Commit

Permalink
Merge pull request #2193 from FabianKramm/service-account-export
Browse files Browse the repository at this point in the history
feat: add service account to export kube config
  • Loading branch information
FabianKramm authored Oct 1, 2024
2 parents 87d99a6 + 47bc7d1 commit 9c97197
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 191 deletions.
28 changes: 27 additions & 1 deletion chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1513,9 +1513,17 @@
"type": "string",
"description": "Override the default https://localhost:8443 and specify a custom hostname for the generated kubeconfig."
},
"insecure": {
"type": "boolean",
"description": "If tls should get skipped for the server"
},
"serviceAccount": {
"$ref": "#/$defs/ExportKubeConfigServiceAccount",
"description": "ServiceAccount can be used to generate a service account token instead of the default certificates."
},
"secret": {
"$ref": "#/$defs/ExportKubeConfigSecretReference",
"description": "Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig.\nIf this is not defined, vCluster create it with `vc-NAME`. If you specify another name,\nvCluster creates the config in this other secret."
"description": "Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig.\nIf this is not defined, vCluster will create it with `vc-NAME`. If you specify another name,\nvCluster creates the config in this other secret."
}
},
"additionalProperties": false,
Expand All @@ -1537,6 +1545,24 @@
"type": "object",
"description": "Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig."
},
"ExportKubeConfigServiceAccount": {
"properties": {
"name": {
"type": "string",
"description": "Name of the service account to be used to generate a service account token instead of the default certificates."
},
"namespace": {
"type": "string",
"description": "Namespace of the service account to be used to generate a service account token instead of the default certificates.\nIf omitted, will use the kube-system namespace."
},
"clusterRole": {
"type": "string",
"description": "ClusterRole to assign to the service account."
}
},
"additionalProperties": false,
"type": "object"
},
"ExternalConfig": {
"properties": {
"platform": {
Expand Down
15 changes: 14 additions & 1 deletion chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -849,8 +849,21 @@ exportKubeConfig:
# Override the default https://localhost:8443 and specify a custom hostname for the generated kubeconfig.
server: ""

# If tls should get skipped for the server
insecure: false

# ServiceAccount can be used to generate a service account token instead of the default certificates.
serviceAccount:
# Name of the service account to be used to generate a service account token instead of the default certificates.
name: ""
# Namespace of the service account to be used to generate a service account token instead of the default certificates.
# If omitted, will use the kube-system namespace.
namespace: ""
# ClusterRole to assign to the service account.
clusterRole: ""

# Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig.
# If this is not defined, vCluster create it with `vc-NAME`. If you specify another name,
# If this is not defined, vCluster will create it with `vc-NAME`. If you specify another name,
# vCluster creates the config in this other secret.
secret:
# Name is the name of the secret where the kubeconfig should get stored.
Expand Down
24 changes: 21 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,17 +353,35 @@ func UnmarshalYAMLStrict(data []byte, i any) error {
// ExportKubeConfig describes how vCluster should export the vCluster kubeconfig.
type ExportKubeConfig struct {
// Context is the name of the context within the generated kubeconfig to use.
Context string `json:"context"`
Context string `json:"context,omitempty"`

// Override the default https://localhost:8443 and specify a custom hostname for the generated kubeconfig.
Server string `json:"server"`
Server string `json:"server,omitempty"`

// If tls should get skipped for the server
Insecure bool `json:"insecure,omitempty"`

// ServiceAccount can be used to generate a service account token instead of the default certificates.
ServiceAccount ExportKubeConfigServiceAccount `json:"serviceAccount,omitempty"`

// Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig.
// If this is not defined, vCluster create it with `vc-NAME`. If you specify another name,
// If this is not defined, vCluster will create it with `vc-NAME`. If you specify another name,
// vCluster creates the config in this other secret.
Secret ExportKubeConfigSecretReference `json:"secret,omitempty"`
}

type ExportKubeConfigServiceAccount struct {
// Name of the service account to be used to generate a service account token instead of the default certificates.
Name string `json:"name,omitempty"`

// Namespace of the service account to be used to generate a service account token instead of the default certificates.
// If omitted, will use the kube-system namespace.
Namespace string `json:"namespace,omitempty"`

// ClusterRole to assign to the service account.
ClusterRole string `json:"clusterRole,omitempty"`
}

// Declare in which host cluster secret vCluster should store the generated virtual cluster kubeconfig.
// If this is not defined, vCluster create it with `vc-NAME`. If you specify another name,
// vCluster creates the config in this other secret.
Expand Down
5 changes: 5 additions & 0 deletions config/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,11 @@ policies:
exportKubeConfig:
context: ""
server: ""
insecure: false
serviceAccount:
name: ""
namespace: ""
clusterRole: ""
secret:
name: ""
namespace: ""
Expand Down
175 changes: 30 additions & 145 deletions pkg/cli/connect_helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ import (
"github.com/loft-sh/vcluster/pkg/lifecycle"
"github.com/loft-sh/vcluster/pkg/util/clihelper"
"github.com/loft-sh/vcluster/pkg/util/portforward"
"github.com/loft-sh/vcluster/pkg/util/translate"
"github.com/loft-sh/vcluster/pkg/util/serviceaccount"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -402,7 +400,12 @@ func (cmd *connectHelm) getVClusterKubeConfig(ctx context.Context, vcluster *fin

// we want to use a service account token in the kube config
if cmd.ServiceAccount != "" {
token, err := createServiceAccountToken(ctx, *kubeConfig, cmd.ConnectOptions, cmd.Log)
vKubeClient, serviceAccount, serviceAccountNamespace, err := getServiceAccountClientAndName(*kubeConfig, cmd.ConnectOptions)
if err != nil {
return nil, err
}

token, err := serviceaccount.CreateServiceAccountToken(ctx, vKubeClient, serviceAccount, serviceAccountNamespace, cmd.ServiceAccountClusterRole, int64(cmd.ServiceAccountExpiration), cmd.Log)
if err != nil {
return nil, err
}
Expand All @@ -420,6 +423,29 @@ func (cmd *connectHelm) getVClusterKubeConfig(ctx context.Context, vcluster *fin
return kubeConfig, nil
}

func getServiceAccountClientAndName(kubeConfig clientcmdapi.Config, options *ConnectOptions) (kubernetes.Interface, string, string, error) {
vKubeClient, err := getLocalVClusterClient(kubeConfig, options)
if err != nil {
return nil, "", "", err
}

var (
serviceAccount = options.ServiceAccount
serviceAccountNamespace = "kube-system"
)
if strings.Contains(options.ServiceAccount, "/") {
splitted := strings.Split(options.ServiceAccount, "/")
if len(splitted) != 2 {
return nil, "", "", fmt.Errorf("unexpected service account reference, expected ServiceAccountNamespace/ServiceAccountName")
}

serviceAccountNamespace = splitted[0]
serviceAccount = splitted[1]
}

return vKubeClient, serviceAccount, serviceAccountNamespace, nil
}

func (cmd *connectHelm) setServerIfExposed(ctx context.Context, vcluster *find.VCluster, vClusterConfig *clientcmdapi.Config) error {
printedWaiting := false
err := wait.PollUntilContextTimeout(ctx, time.Second*2, time.Minute*5, true, func(ctx context.Context) (done bool, err error) {
Expand Down Expand Up @@ -667,144 +693,3 @@ func (cmd *connectHelm) waitForVCluster(ctx context.Context, vKubeConfig clientc

return nil
}

func createServiceAccountToken(ctx context.Context, vKubeConfig clientcmdapi.Config, options *ConnectOptions, log log.Logger) (string, error) {
vKubeClient, err := getLocalVClusterClient(vKubeConfig, options)
if err != nil {
return "", err
}

var (
serviceAccount = options.ServiceAccount
serviceAccountNamespace = "kube-system"
)
if strings.Contains(options.ServiceAccount, "/") {
splitted := strings.Split(options.ServiceAccount, "/")
if len(splitted) != 2 {
return "", fmt.Errorf("unexpected service account reference, expected ServiceAccountNamespace/ServiceAccountName")
}

serviceAccountNamespace = splitted[0]
serviceAccount = splitted[1]
}

audiences := []string{"https://kubernetes.default.svc.cluster.local", "https://kubernetes.default.svc", "https://kubernetes.default"}
expirationSeconds := int64(10 * 365 * 24 * 60 * 60)
if options.ServiceAccountExpiration > 0 {
expirationSeconds = int64(options.ServiceAccountExpiration)
}
token := ""
log.Infof("Create service account token for %s/%s", serviceAccountNamespace, serviceAccount)
err = wait.PollUntilContextTimeout(ctx, time.Second, time.Minute*3, false, func(ctx context.Context) (bool, error) {
// check if namespace exists
_, err := vKubeClient.CoreV1().Namespaces().Get(ctx, serviceAccountNamespace, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) {
return false, err
}

return false, nil
}

// check if service account exists
_, err = vKubeClient.CoreV1().ServiceAccounts(serviceAccountNamespace).Get(ctx, serviceAccount, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
if serviceAccount == "default" {
return false, nil
}

if options.ServiceAccountClusterRole != "" {
// create service account
_, err = vKubeClient.CoreV1().ServiceAccounts(serviceAccountNamespace).Create(ctx, &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccount,
Namespace: serviceAccountNamespace,
},
}, metav1.CreateOptions{})
if err != nil {
return false, err
}

log.Donef("Created service account %s/%s", serviceAccountNamespace, serviceAccount)
} else {
return false, err
}
} else if kerrors.IsForbidden(err) {
return false, err
} else {
return false, nil
}
}

// create service account cluster role binding
if options.ServiceAccountClusterRole != "" {
clusterRoleBindingName := translate.SafeConcatName("vcluster", "sa", serviceAccount, serviceAccountNamespace)
clusterRoleBinding, err := vKubeClient.RbacV1().ClusterRoleBindings().Get(ctx, clusterRoleBindingName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
// create cluster role binding
_, err = vKubeClient.RbacV1().ClusterRoleBindings().Create(ctx, &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: clusterRoleBindingName,
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.SchemeGroupVersion.Group,
Kind: "ClusterRole",
Name: options.ServiceAccountClusterRole,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: serviceAccount,
Namespace: serviceAccountNamespace,
},
},
}, metav1.CreateOptions{})
if err != nil {
return false, err
}

log.Donef("Created cluster role binding for cluster role %s", options.ServiceAccountClusterRole)
} else if kerrors.IsForbidden(err) {
return false, err
} else {
return false, nil
}
} else {
// if cluster role differs, recreate it
if clusterRoleBinding.RoleRef.Name != options.ServiceAccountClusterRole {
err = vKubeClient.RbacV1().ClusterRoleBindings().Delete(ctx, clusterRoleBindingName, metav1.DeleteOptions{})
if err != nil {
return false, err
}

log.Done("Recreate cluster role binding for service account")
// this will recreate the cluster role binding in the next iteration
return false, nil
}
}
}

// create service account token
result, err := vKubeClient.CoreV1().ServiceAccounts(serviceAccountNamespace).CreateToken(ctx, serviceAccount, &authenticationv1.TokenRequest{Spec: authenticationv1.TokenRequestSpec{
Audiences: audiences,
ExpirationSeconds: &expirationSeconds,
}}, metav1.CreateOptions{})
if err != nil {
if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) {
return false, err
}

return false, nil
}

token = result.Status.Token
return true, nil
})
if err != nil {
return "", fmt.Errorf("create service account token: %w", err)
}

return token, nil
}
9 changes: 8 additions & 1 deletion pkg/cli/connect_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/loft-sh/vcluster/pkg/cli/flags"
"github.com/loft-sh/vcluster/pkg/platform"
"github.com/loft-sh/vcluster/pkg/platform/clihelper"
"github.com/loft-sh/vcluster/pkg/util/serviceaccount"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
Expand Down Expand Up @@ -156,8 +157,14 @@ func (cmd *connectPlatform) getVClusterKubeConfig(ctx context.Context, platformC
return nil, fmt.Errorf("forward token is not enabled on the virtual cluster and hence you cannot authenticate with a service account token")
}

// init client
vKubeClient, serviceAccount, serviceAccountNamespace, err := getServiceAccountClientAndName(*kubeConfig, cmd.ConnectOptions)
if err != nil {
return nil, err
}

// create service account token
token, err := createServiceAccountToken(ctx, *kubeConfig, cmd.ConnectOptions, cmd.log)
token, err := serviceaccount.CreateServiceAccountToken(ctx, vKubeClient, serviceAccount, serviceAccountNamespace, cmd.ServiceAccountClusterRole, int64(cmd.ServiceAccountExpiration), cmd.log)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 9c97197

Please sign in to comment.