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

[YUNIKORN-1977] E2E test for verifying user info with non kube-admin user #915

Closed
wants to merge 14 commits into from

Conversation

rrajesh-cloudera
Copy link
Contributor

@rrajesh-cloudera rrajesh-cloudera commented Sep 14, 2024

This PR adds an end-to-end (E2E) test that verifies pod deployment and user info using a non-kube-admin user. The test ensures that RBAC roles and permissions for non-admin users are correctly configured. Specifically, it verifies that user information can be retrieved through pod annotations.

What type of PR is it?

  • - Bug Fix
  • - Improvement
  • - Feature
  • - Documentation
  • - Hot Fix
  • - Refactoring

Todos

  • - Task

What is the Jira issue?

https://issues.apache.org/jira/browse/YUNIKORN-1977

How should this be tested?

Screenshots (if appropriate)

Questions:

  • - The licenses files need update.
  • - There is breaking changes for older versions.
  • - It needs documentation.

@rrajesh-cloudera rrajesh-cloudera changed the title [YUNIKORN-1977] Added E2E test for deploying pod and verifying user i… [YUNIKORN-1977] E2E test for verifying user info with non kube-admin user Sep 14, 2024
Copy link

codecov bot commented Sep 16, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 68.03%. Comparing base (f1e7920) to head (2e25c0e).
Report is 12 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #915      +/-   ##
==========================================
- Coverage   68.18%   68.03%   -0.15%     
==========================================
  Files          70       70              
  Lines        7621     9195    +1574     
==========================================
+ Hits         5196     6256    +1060     
- Misses       2216     2732     +516     
+ Partials      209      207       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pbacsko pbacsko assigned pbacsko and rrajesh-cloudera and unassigned pbacsko Sep 16, 2024
Copy link
Contributor

@ryankert01 ryankert01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, It looks great! having a simple question.

test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
@pbacsko pbacsko requested review from pbacsko and removed request for ryankert01 September 16, 2024 19:59
Copy link
Contributor

@pbacsko pbacsko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 I'm not a fan of the kubectl approach, it's completely unnecessary. It makes the test more complicated. It's possible to use the API.

Comment on lines 1025 to 1033
_, err = clientset.CoreV1().Secrets(namespace).Create(context.TODO(), &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Annotations: map[string]string{
"kubernetes.io/service-account.name": serviceAccountName,
},
},
Type: v1.SecretTypeServiceAccountToken,
}, metav1.CreateOptions{})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use KubeCtl.CreateSecret() if possible. If the annotation is necessary, create a new method with the name CreateSecretWithAnnotation(secret *v1.Secret, namespace string, annotations map[string]string) to avoid calling this directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code with requested changes.

Comment on lines 1253 to 1283
func createKubeconfig(path, currentContext, clusterCA, clusterServer, userTokenValue string) error {
kubeconfigTemplate := `
apiVersion: v1
kind: Config
current-context: ${CURRENT_CONTEXT}
contexts:
- name: ${CURRENT_CONTEXT}
context:
cluster: ${CURRENT_CONTEXT}
user: test-user
clusters:
- name: ${CURRENT_CONTEXT}
cluster:
certificate-authority-data: ${CLUSTER_CA}
server: ${CLUSTER_SERVER}
users:
- name: test-user
user:
token: ${USER_TOKEN_VALUE}
`
// Replace placeholders in the template
kubeconfigContent := strings.ReplaceAll(kubeconfigTemplate, "${CURRENT_CONTEXT}", currentContext)
kubeconfigContent = strings.ReplaceAll(kubeconfigContent, "${CLUSTER_CA}", clusterCA)
kubeconfigContent = strings.ReplaceAll(kubeconfigContent, "${CLUSTER_SERVER}", clusterServer)
kubeconfigContent = strings.ReplaceAll(kubeconfigContent, "${USER_TOKEN_VALUE}", userTokenValue)

// Write the kubeconfig YAML to the file
err := os.WriteFile(path, []byte(kubeconfigContent), 0600)
gomega.Ω(err).NotTo(gomega.HaveOccurred())
return nil
}
Copy link
Contributor

@pbacsko pbacsko Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this should NOT be necessary. Way too complicated to mess around with separate kubectl calls.

You can do this:

config, _ := kClient.GetKubeConfig() // handle error in real code

newConf := config.DeepCopy()  // copy existing config
newConf.TLSClientConfig.CertFile = ""  // remove cert file
newConf.TLSClientConfig.KeyFile = ""  // remove key file
newConf.BearerToken = "<base64Token>"  // set token that is retrieved in the test
_ = kClient.SetClientFromConfig(newConf)

After this point, kClient will use the token for authentication there's no need to delete/restore anything.

New method in KubeCtl:

func (k *KubeCtl) SetClientFromConfig(conf *rest.Config) error {
    k.kubeConfig = conf.DeepCopy()
    k.clientSet, err = kubernetes.NewForConfig(k.kubeConfig)   // creates new clientset 
    return err
} 

Also, try to retrieve the secret token using KubeCtl. We might need to create a new method for it, but again, it shouldn't involve running the kubectl command:

func (k *KubeCtl) GetSecret(namespace string) (*v1.Secret, error) {
    return k.clientSet.CoreV1().Secrets(namespace).Get(context.TODO(), namespace, metav1.GetOptions{})
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure @pbacsko will accommodate the requested changes.

@pbacsko
Copy link
Contributor

pbacsko commented Oct 3, 2024

@rrajesh-cloudera please fix the merge conflicts in user_group_limit_test.go

@rrajesh-cloudera
Copy link
Contributor Author

@rrajesh-cloudera please fix the merge conflicts in user_group_limit_test.go

Hi @pbacsko , resolved the Merge Conflict.

Copy link
Contributor

@ryankert01 ryankert01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your patch, some questions and nits left.

time.Sleep(10 * time.Second)
_, err = kClient.CreateSecret(secret, namespace)
gomega.Ω(err).NotTo(HaveOccurred())
time.Sleep(10 * time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use Eventually in this case? time.Sleep can lead to flaky tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or could we further optimize it using TokenRequest API instead of manually create secret and wait until it pupulate.

Copy link
Contributor

@pbacsko pbacsko Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 fixed sleeps are dangerous and can be time consuming. Avoid them if possible. Add helper poll method which repeatedly checks if the secret exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code with requested changes.

Comment on lines 1043 to 1048
kubeconfigPath := filepath.Join(os.TempDir(), "test-user-config")
err = k8s.WriteConfigToFile(newConf, kubeconfigPath)
gomega.Ω(err).NotTo(gomega.HaveOccurred())
config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
gomega.Ω(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we simplify this six line to:

clientset, err = kubernetes.NewForConfig(newConf)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
kubeconfigPath := filepath.Join(os.TempDir(), "test-user-config")
err = k8s.WriteConfigToFile(newConf, kubeconfigPath)
gomega.Ω(err).NotTo(gomega.HaveOccurred())
config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
gomega.Ω(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(config)
clientset, err = kubernetes.NewForConfig(newConf)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried the suggested solution but facing issue , It's not working with this test case.

test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
Copy link
Contributor

@pbacsko pbacsko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments from me as well.

Comment on lines 1113 to 1150
func WriteConfigToFile(config *rest.Config, kubeconfigPath string) error {
// Build the kubeconfig API object from the rest.Config
kubeConfig := &clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{
"default-cluster": {
Server: config.Host,
CertificateAuthorityData: config.CAData,
InsecureSkipTLSVerify: config.Insecure,
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"default-auth": {
Token: config.BearerToken,
},
},
Contexts: map[string]*clientcmdapi.Context{
"default-context": {
Cluster: "default-cluster",
AuthInfo: "default-auth",
},
},
CurrentContext: "default-context",
}

// Ensure the directory where the file is being written exists
err := os.MkdirAll(filepath.Dir(kubeconfigPath), os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create directory for kubeconfig file: %v", err)
}

// Write the kubeconfig to the specified file
err = clientcmd.WriteToFile(*kubeConfig, kubeconfigPath)
if err != nil {
return fmt.Errorf("failed to write kubeconfig to file: %v", err)
}

return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? I think we can get away with writing stuff to files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a temp file to write the kubeconfig and reload the kubeconfig of the temp file to apply the changes to Kubernetes clientset.
Tried without writing the file and copying the existing kubeconfig to newConf with bearertoken value set but that approach is not working well so that's why we needed to add the write config file to temp store the kubeconfig and reload for the k8s clientset.

}

// Wait before retrying
time.Sleep(2 * time.Second)
Copy link
Contributor

@pbacsko pbacsko Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This retry loop & sleep should definitely not be here. Normally, a getter function is only about retrieving something, whenever possible. If we have to wait for a value to show up, let's create a separate helper method for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code with requested changes.

Copy link
Contributor

@pbacsko pbacsko Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to code the retry. We can take advantage of existing k8s functions.

To be consistent with existing code, let's use the following:

func (k *KubeCtl) GetSecretValue(namespace, secretName, key string) (string, error) {
	secret, err := k.GetSecret(namespace, secretName)
	if err != nil {
		return "", err
	}
	// Check if the key exists in the secret
	value, ok := secret.Data[key]
	if !ok {
		return "", fmt.Errorf("key %s not found in secret %s", key, secretName)
	}
	return string(value), nil
}

func (k *KubeCtl) GetSecret(namespace, secretName string) (*v1.Secret, error) {
	secret, err := k.clientSet.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	return secret, nil
}

func (k *KubeCtl) WaitForSecret(namespace, secretName string, timeout time.Duration) error {
	var cond wait.ConditionFunc
	cond = func() (done bool, err error) {
		secret, err := k.GetSecret(namespace, secretName)
		if err != nil {
			return false, err
		}
		if secret != nil {
			return false, nil
		}
		return true, nil
	}
	return wait.PollUntilContextTimeout(context.TODO(), time.Second, timeout, false, cond.WithContext())
}

First, you call kClient.WaitForSecret() to indicate that you're waiting for a secret to appear over the API. Then call kClient.GetSecretValue(). By having these 3 methods, all of them are usable indepdently of one another.

test/e2e/framework/helpers/k8s/k8s_utils.go Outdated Show resolved Hide resolved
time.Sleep(10 * time.Second)
_, err = kClient.CreateSecret(secret, namespace)
gomega.Ω(err).NotTo(HaveOccurred())
time.Sleep(10 * time.Second)
Copy link
Contributor

@pbacsko pbacsko Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 fixed sleeps are dangerous and can be time consuming. Avoid them if possible. Add helper poll method which repeatedly checks if the secret exists.

test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
if !ok {
return nil, fmt.Errorf("user info not found in pod annotation")
}
var userInfoObj interface{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know the type of this object. It's si.UserGroupInformation, let's use that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code with requested changes.

Copy link
Contributor

@pbacsko pbacsko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments

test/e2e/user_group_limit/user_group_limit_test.go Outdated Show resolved Hide resolved
}

// Wait before retrying
time.Sleep(2 * time.Second)
Copy link
Contributor

@pbacsko pbacsko Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to code the retry. We can take advantage of existing k8s functions.

To be consistent with existing code, let's use the following:

func (k *KubeCtl) GetSecretValue(namespace, secretName, key string) (string, error) {
	secret, err := k.GetSecret(namespace, secretName)
	if err != nil {
		return "", err
	}
	// Check if the key exists in the secret
	value, ok := secret.Data[key]
	if !ok {
		return "", fmt.Errorf("key %s not found in secret %s", key, secretName)
	}
	return string(value), nil
}

func (k *KubeCtl) GetSecret(namespace, secretName string) (*v1.Secret, error) {
	secret, err := k.clientSet.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	return secret, nil
}

func (k *KubeCtl) WaitForSecret(namespace, secretName string, timeout time.Duration) error {
	var cond wait.ConditionFunc
	cond = func() (done bool, err error) {
		secret, err := k.GetSecret(namespace, secretName)
		if err != nil {
			return false, err
		}
		if secret != nil {
			return false, nil
		}
		return true, nil
	}
	return wait.PollUntilContextTimeout(context.TODO(), time.Second, timeout, false, cond.WithContext())
}

First, you call kClient.WaitForSecret() to indicate that you're waiting for a secret to appear over the API. Then call kClient.GetSecretValue(). By having these 3 methods, all of them are usable indepdently of one another.

test/e2e/framework/helpers/k8s/k8s_utils.go Show resolved Hide resolved
Copy link
Contributor

@pbacsko pbacsko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@pbacsko pbacsko closed this in 352e1b7 Oct 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants