Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests to access command #2203

Merged
merged 4 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 7 additions & 178 deletions internal/cmd/alpha/access/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@ package access

import (
"fmt"
"strconv"
"strings"

"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmdcommon"
"github.com/kyma-project/cli.v3/internal/kube"
"github.com/kyma-project/cli.v3/internal/kube/resources"
"github.com/kyma-project/cli.v3/internal/kubeconfig"
"github.com/spf13/cobra"
authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)

type accessConfig struct {
Expand Down Expand Up @@ -71,7 +65,7 @@ func runAccess(cfg *accessConfig) clierror.Error {
}

// Fill kubeconfig
generatedKubeconfig, clierr := prepareKubeconfig(cfg)
generatedKubeconfig, clierr := kubeconfig.Prepare(cfg.Ctx, cfg.KubeClient, cfg.name, cfg.namespace, cfg.time, cfg.output, cfg.permanent)
if clierr != nil {
return clierr
}
Expand All @@ -94,186 +88,21 @@ func runAccess(cfg *accessConfig) clierror.Error {

func createObjects(cfg *accessConfig) clierror.Error {
// Create Service Account
err := createServiceAccount(cfg)
err := resources.CreateServiceAccount(cfg.Ctx, cfg.KubeClient, cfg.name, cfg.namespace)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to create Service Account"))
}
// Create Role Binding for the Service Account
err = createClusterRoleBinding(cfg)
err = resources.CreateClusterRoleBinding(cfg.Ctx, cfg.KubeClient, cfg.name, cfg.namespace, cfg.clusterrole)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to create Cluster Role Binding"))
}
// Create a service-account-token type secret
if cfg.permanent {
secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: cfg.name,
Namespace: cfg.namespace,
Annotations: map[string]string{
"kubernetes.io/service-account.name": cfg.name,
},
},
Type: v1.SecretTypeServiceAccountToken,
}

_, err := cfg.KubeClient.Static().CoreV1().Secrets(cfg.namespace).Create(cfg.Ctx, &secret, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return clierror.Wrap(err, clierror.New("failed to create secret"))
}
}
return nil
}

func prepareKubeconfig(cfg *accessConfig) (*api.Config, clierror.Error) {
currentCtx := cfg.KubeClient.APIConfig().CurrentContext
clusterName := cfg.KubeClient.APIConfig().Contexts[currentCtx].Cluster
var tokenData authv1.TokenRequestStatus
var certData []byte
var err clierror.Error

// Prepare the token and certificate data
if cfg.permanent {
var secret *v1.Secret
for ok := true; ok; ok = string(secret.Data["token"]) == "" {
var loopErr error
secret, loopErr = cfg.KubeClient.Static().CoreV1().Secrets(cfg.namespace).Get(cfg.Ctx, cfg.name, metav1.GetOptions{})
if loopErr != nil {
return nil, clierror.Wrap(loopErr, clierror.New("failed to get secret"))
}
}

tokenData.Token = string(secret.Data["token"])
certData = secret.Data["ca.crt"]
if cfg.output != "" {
fmt.Println("Token is valid permanently")
}
} else {
certData = cfg.KubeClient.APIConfig().Clusters[clusterName].CertificateAuthorityData
tokenData, err = getServiceAccountToken(cfg)
err = resources.CreateServiceAccountToken(cfg.Ctx, cfg.KubeClient, cfg.name, cfg.namespace)
if err != nil {
return nil, err
}
if cfg.output != "" {
fmt.Println("Token will expire: " + tokenData.ExpirationTimestamp.String())
}
}

// Create a new kubeconfig
kubeconfig := &api.Config{
Kind: "Config",
APIVersion: "v1",
Clusters: map[string]*api.Cluster{
clusterName: {
Server: cfg.KubeClient.APIConfig().Clusters[clusterName].Server,
CertificateAuthorityData: certData,
},
},
AuthInfos: map[string]*api.AuthInfo{
cfg.name: {
Token: tokenData.Token,
},
},
Contexts: map[string]*api.Context{
currentCtx: {
Cluster: clusterName,
Namespace: cfg.namespace,
AuthInfo: cfg.name,
},
},
CurrentContext: currentCtx,
Extensions: nil,
}

return kubeconfig, nil
}

func createServiceAccount(cfg *accessConfig) error {
sa := v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: cfg.name,
Namespace: cfg.namespace,
},
}
_, err := cfg.KubeClient.Static().CoreV1().ServiceAccounts(cfg.namespace).Create(cfg.Ctx, &sa, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}

func getServiceAccountToken(cfg *accessConfig) (authv1.TokenRequestStatus, clierror.Error) {
var seconds int64
var tokenData authv1.TokenRequestStatus

// Convert the time passed in argument to seconds
if strings.Contains(cfg.time, "h") {
// remove the "h" from the string
cfg.time = strings.TrimRight(cfg.time, "h")
// convert the string to an int
hours, err := strconv.Atoi(cfg.time)
if err != nil {
return tokenData, clierror.Wrap(err, clierror.New("failed to convert time to seconds", "Make sure to use h for hours and d for days"))
}
// convert the hours to seconds
seconds = int64(hours * 3600)
}

if strings.Contains(cfg.time, "d") {
// remove the "d" from the string
cfg.time = strings.TrimRight(cfg.time, "d")
// convert the string to an int
days, err := strconv.Atoi(cfg.time)
if err != nil {
return tokenData, clierror.Wrap(err, clierror.New("failed to convert time to seconds", "Make sure to use h for hours and d for days"))
return clierror.Wrap(err, clierror.New("failed to create secret"))
}
// convert the days to seconds
seconds = int64(days * 86400)
}

if seconds == 0 {
return tokenData, clierror.New("failed to convert the token duration", "Make sure to use h for hours and d for days")
}

tokenRequest := authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
ExpirationSeconds: &seconds,
},
}

tokenResponse, err := cfg.KubeClient.Static().CoreV1().ServiceAccounts(cfg.namespace).CreateToken(cfg.Ctx, cfg.name, &tokenRequest, metav1.CreateOptions{})
if err != nil {
return tokenData, clierror.Wrap(err, clierror.New("failed to create token"))
}
return tokenResponse.Status, nil
}

func createClusterRoleBinding(cfg *accessConfig) error {
// Check if the cluster role to bind to exists
_, err := cfg.KubeClient.Static().RbacV1().ClusterRoles().Get(cfg.Ctx, cfg.clusterrole, metav1.GetOptions{})
if err != nil {
return err
}
// Create clusterRoleBinding
cRoleBinding := rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: cfg.name + "-binding",
Namespace: cfg.namespace,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: cfg.name,
Namespace: cfg.namespace,
}},

RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: cfg.clusterrole,
},
}
_, err = cfg.KubeClient.Static().RbacV1().ClusterRoleBindings().Create(cfg.Ctx, &cRoleBinding, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}
74 changes: 74 additions & 0 deletions internal/kube/resources/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package resources

import (
"context"

"github.com/kyma-project/cli.v3/internal/kube"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func CreateServiceAccount(ctx context.Context, client kube.Client, name, namespace string) error {
sa := v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
_, err := client.Static().CoreV1().ServiceAccounts(namespace).Create(ctx, &sa, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}

func CreateServiceAccountToken(ctx context.Context, client kube.Client, name, namespace string) error {
secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
"kubernetes.io/service-account.name": name,
},
},
Type: v1.SecretTypeServiceAccountToken,
}

_, err := client.Static().CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}

func CreateClusterRoleBinding(ctx context.Context, client kube.Client, name, namespace, clusterRole string) error {
// Check if the cluster role to bind to exists
_, err := client.Static().RbacV1().ClusterRoles().Get(ctx, clusterRole, metav1.GetOptions{})
if err != nil {
return err
}
// Create clusterRoleBinding
cRoleBinding := rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: name + "-binding",
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: name,
Namespace: namespace,
}},

RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: clusterRole,
},
}
_, err = client.Static().RbacV1().ClusterRoleBindings().Create(ctx, &cRoleBinding, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}
88 changes: 88 additions & 0 deletions internal/kube/resources/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package resources

import (
"context"
"testing"

kube_fake "github.com/kyma-project/cli.v3/internal/kube/fake"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s_fake "k8s.io/client-go/kubernetes/fake"
)

func Test_CreateClusterRoleBinding(t *testing.T) {
t.Parallel()
tests := []struct {
name string
username string
namespace string
clusterRole string
wantErr bool
}{
{
name: "create cluster role binding",
username: "username",
namespace: "default",
clusterRole: "clusterRole",
wantErr: false,
},
{
name: "create existing cluster role binding",
username: "existing",
namespace: "default",
clusterRole: "clusterRole",
wantErr: false,
},
{
name: "non-existent clusterRole",
username: "username",
namespace: "default",
clusterRole: "missing",
wantErr: true,
},
}

ctx := context.Background()
for _, tt := range tests {
username := tt.username
namespace := tt.namespace
clusterRole := tt.clusterRole
wantErr := tt.wantErr

t.Run(tt.name, func(t *testing.T) {
serviceAccount := corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
Namespace: "default",
},
}

ClusterRoleBinding := rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
}
existingClusterRole := rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "clusterRole",
},
}
staticClient := k8s_fake.NewSimpleClientset(
&serviceAccount,
&ClusterRoleBinding,
&existingClusterRole,
)
kubeClient := &kube_fake.FakeKubeClient{
TestKubernetesInterface: staticClient,
}
err := CreateClusterRoleBinding(ctx, kubeClient, username, namespace, clusterRole)
if wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
Loading