add test_k8s_dashboard_access TC steps 2

Added the remaining test steps for the newe automated TC:
Step 5: Navigate to K8s dashboard login page
            - Get the k8s dashboard URL.
            - Open the k8s dashboard login page.
            - Login to the dashboard using the token.
Step 6 : Logout from the dashboard
            - Logout from the dashboard
Step 7 : Login to the dashboard using kubeconfig file
            - Update the token in the kubeconfig file
            - Open the k8s dashboard login page.
            - Login to the dashboard using the kubeconfig file.

Change-Id: Id3d68c4d3ec41c8ea108a7744494f81fad1e0691
Signed-off-by: Gabriel Calixto <Gabriel.CalixtodePaula@windriver.com>
This commit is contained in:
Gabriel Calixto
2025-03-18 14:34:51 -03:00
committed by Gabriel Calixto de Paula
parent d1bed50b29
commit 987c07f253
12 changed files with 289 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
{
// KUBECONFIG environment variable on the cloud_platform system.
"kubeconfig": "/etc/kubernetes/admin.conf",
"dashboard_port": "8443"
"dashboard_port": "30000"
}

View File

@@ -2,6 +2,8 @@ import time
from typing import List
import selenium
from selenium import webdriver
from config.configuration_manager import ConfigurationManager
from framework.logging.automation_logger import get_logger
from framework.web.action.web_action_click import WebActionClick
@@ -11,7 +13,6 @@ from framework.web.condition.web_condition import WebCondition
from framework.web.condition.web_condition_text_equals import WebConditionTextEquals
from framework.web.web_action_executor import WebActionExecutor
from framework.web.web_locator import WebLocator
from selenium import webdriver
class WebDriverCore:
@@ -57,6 +58,7 @@ class WebDriverCore:
is_navigation_success = True
timeout = time.time() + 30
reload_attempt_timeout = 2
while not is_navigation_success and time.time() < timeout:
for condition in conditions:
@@ -66,9 +68,10 @@ class WebDriverCore:
if not is_navigation_success:
get_logger().log_debug(f"Failed to load page with URL: {url}")
get_logger().log_debug("Sleep for 2 seconds and try reloading the page.")
time.sleep(2)
get_logger().log_debug(f"Reload page and sleep for {reload_attempt_timeout} seconds ")
self.driver.get(url)
time.sleep(reload_attempt_timeout)
reload_attempt_timeout += 2
if is_navigation_success:
get_logger().log_debug(f"Navigation to {url} successful.")

View File

@@ -13,12 +13,12 @@ class OpenStackEndpointListKeywords(BaseKeyword):
def __init__(self, ssh_connection: SSHConnection):
self.ssh_connection = ssh_connection
def endpoint_list(self):
def endpoint_list(self) -> OpenStackEndpointListOutput:
"""
Keyword for openstack endpoint list
Executes the 'openstack endpoint list' command and parses the output.
Returns:
OpenStackEndpointListOutput object
OpenStackEndpointListOutput: Parsed output of the 'openstack endpoint list' command.
"""
output = self.ssh_connection.send(source_openrc("openstack endpoint list"))
self.validate_success_return_code(self.ssh_connection)
@@ -32,5 +32,5 @@ class OpenStackEndpointListKeywords(BaseKeyword):
"""
endpoint_output = self.endpoint_list()
url = endpoint_output.get_endpoint("keystone", "public").get_url().rsplit(":", 1)[0]
end_point = f"{url}:{ConfigurationManager.get_k8s_config().get_dashboard_port()}"
end_point = f"{url}:{ConfigurationManager.get_k8s_config().get_dashboard_port()}/"
return end_point

View File

@@ -1,10 +1,11 @@
import os
import yaml
from jinja2 import Environment, FileSystemLoader, Template
from config.configuration_manager import ConfigurationManager
from framework.logging.automation_logger import get_logger
from framework.ssh.ssh_connection import SSHConnection
from jinja2 import Environment, FileSystemLoader, Template
from keywords.base_keyword import BaseKeyword
from keywords.files.file_keywords import FileKeywords
@@ -22,25 +23,26 @@ class YamlKeywords(BaseKeyword):
"""
self.ssh_connection = ssh_connection
def generate_yaml_file_from_template(self, template_file: str, replacement_dictionary: str, target_file_name: str, target_remote_location: str) -> str:
def generate_yaml_file_from_template(self, template_file: str, replacement_dictionary: str, target_file_name: str, target_remote_location: str, copy_to_remote: bool = True) -> str:
"""
This function will generate a YAML file from the specified template. The parameters in the file will get substituted by
using the key-value pairs from the replacement_dictionary. A copy of the file will be stored in the logs folder as 'target_file_name'.
It will then be SCPed over to 'target_remote_location' on the machine to which this SSH connection is connected.
It will then be SCPed over to 'target_remote_location' on the machine to which this SSH connection is connected
Args:
template_file: Path in the repo to the Template YAML file. e.g. 'resources/cloud_platform/folder/file_name'
replacement_dictionary: A dictionary containing all the variables to replace in the template and the values that you want.
e.g. { pod_name: 'awesome_pod_name', memory: '2Gb'}
target_file_name: The name of the 'new' file that will get generated from this function.
target_remote_location: The folder location on the 'sshed-server' where we want to place the new file.
Returns: The file name and path of the target_remote_file.
template_file (str): Path to the template YAML file.
Example: 'resources/cloud_platform/folder/file_name'.
replacement_dictionary (dict): Dictionary containing placeholder keys and their replacement values.
Example: { 'pod_name': 'awesome_pod_name', 'memory': '2Gb' }.
target_file_name (str): Name of the generated YAML file.
target_remote_location (str): Remote directory path where the file will be uploaded if `copy_to_remote` is True.
copy_to_remote (bool, optional): Flag indicating whether to upload the file to a remote location. Defaults to True.
Returns:
str: Path to the generated YAML file, either local or remote depending on `copy_to_remote`.
"""
# Load the Template YAML file.
with open(template_file, 'r') as template_file:
with open(template_file, "r") as template_file:
yaml_template = template_file.read()
# Render the YAML file by replacing the tokens.
@@ -52,11 +54,12 @@ class YamlKeywords(BaseKeyword):
# Create the new file in the log folder.
log_folder = ConfigurationManager.get_logger_config().get_test_case_resources_log_location()
rendered_yaml_file_location = os.path.join(log_folder, target_file_name)
with open(rendered_yaml_file_location, 'w') as f:
with open(rendered_yaml_file_location, "w") as f:
f.write(rendered_yaml)
get_logger().log_info(f"Generated YAML file from template: {rendered_yaml_file_location}")
# Upload the file to the remote location
target_remote_file = f"{target_remote_location}/{target_file_name}"
FileKeywords(self.ssh_connection).upload_file(rendered_yaml_file_location, target_remote_file)
return target_remote_file
if copy_to_remote:
target_remote_file = f"{target_remote_location}/{target_file_name}"
FileKeywords(self.ssh_connection).upload_file(rendered_yaml_file_location, target_remote_file)
return target_remote_file
return rendered_yaml_file_location

View File

@@ -18,40 +18,35 @@ class KubectlDeleteServiceAccountKeywords(BaseKeyword):
"""
self.ssh_connection = ssh_connection
def delete_serviceaccount(self, serviceaccount_name: str, nspace: str = None) -> str:
def delete_serviceaccount(self, serviceaccount_name: str, namespace: str = None):
"""
Deletes the specified Kubernetes service account.
Args:
serviceaccount_name (str): The name of the service account to delete.
nspace (str, optional): The namespace of the service account. Defaults to None.
Returns:
str: The output of the kubectl delete command.
namespace (str, optional): The namespace of the service account. Defaults to None.
"""
args = ""
if nspace:
args += f" -n {nspace} "
if namespace:
args += f" -n {namespace} "
args += f"{serviceaccount_name}"
output = self.ssh_connection.send(export_k8s_config(f"kubectl delete serviceaccount {args}"))
self.ssh_connection.send(export_k8s_config(f"kubectl delete serviceaccount {args}"))
self.validate_success_return_code(self.ssh_connection)
return output
def cleanup_serviceaccount(self, serviceaccount_name: str, nspace: str = None) -> str:
def cleanup_serviceaccount(self, serviceaccount_name: str, namespace: str = None) -> int:
"""
Deletes a Kubernetes ServiceAccount,method is used for cleanup purposes.
Deletes a Kubernetes ServiceAccount. This method is used for cleanup purposes.
Args:
serviceaccount_name (str): The name of the ServiceAccount to delete.
nspace (str, optional): The namespace of the ServiceAccount. Defaults to None.
namespace (str, optional): The namespace of the ServiceAccount. Defaults to None.
Returns:
str: The output of the command.
int: The return code of the kubectl delete command.
"""
args = ""
if nspace:
args += f" -n {nspace} "
if namespace:
args += f" -n {namespace} "
args += f"{serviceaccount_name}"
self.ssh_connection.send(export_k8s_config(f"kubectl delete serviceaccount {args}"))
rc = self.ssh_connection.get_return_code()

View File

@@ -1,3 +1,5 @@
from pytest import fail
from framework.ssh.ssh_connection import SSHConnection
from keywords.base_keyword import BaseKeyword
from keywords.k8s.k8s_command_wrapper import export_k8s_config
@@ -17,19 +19,23 @@ class KubectlCreateTokenKeywords(BaseKeyword):
"""
self.ssh_connection = ssh_connection
def create_token(self, nspace: str, user: str) -> str:
def create_token(self, namespace: str, user: str) -> list:
"""
Creates a Kubernetes token for a specified user in a given namespace.
Create a Kubernetes token for a specified user in a given namespace.
Args:
nspace (str): The Kubernetes namespace where the token will be created.
user (str): The user for whom the token will be created.
namespace (str): The Kubernetes namespace where the token will be created.
user (str): The name of the Kubernetes service account for which the token will be created.
Returns:
str: The output from the command execution.
list: The output from the kubectl command execution.
"""
args = f"{user} -n {nspace}"
args = f"{user} -n {namespace}"
output = self.ssh_connection.send(export_k8s_config(f"kubectl create token {args}"))
if output and len(output) == 1:
output = output[0]
else:
fail("Token creation failed.")
self.validate_success_return_code(self.ssh_connection)
return output

View File

@@ -12,4 +12,4 @@ users:
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
token: {}
token: "{{token_value}}"

View File

@@ -1,5 +1,4 @@
import os
import time
from pytest import fixture, mark
@@ -9,9 +8,11 @@ from framework.logging.automation_logger import get_logger
from framework.resources.resource_finder import get_stx_resource_path
from framework.rest.rest_client import RestClient
from framework.ssh.ssh_connection import SSHConnection
from framework.web.webdriver_core import WebDriverCore
from keywords.cloud_platform.openstack.endpoint.openstack_endpoint_list_keywords import OpenStackEndpointListKeywords
from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords
from keywords.files.file_keywords import FileKeywords
from keywords.files.yaml_keywords import YamlKeywords
from keywords.k8s.files.kubectl_file_apply_keywords import KubectlFileApplyKeywords
from keywords.k8s.files.kubectl_file_delete_keywords import KubectlFileDeleteKeywords
from keywords.k8s.namespace.kubectl_create_namespace_keywords import KubectlCreateNamespacesKeywords
@@ -23,6 +24,7 @@ from keywords.k8s.secret.kubectl_delete_secret_keywords import KubectlDeleteSecr
from keywords.k8s.serviceaccount.kubectl_delete_serviceaccount_keywords import KubectlDeleteServiceAccountKeywords
from keywords.k8s.token.kubectl_create_token_keywords import KubectlCreateTokenKeywords
from keywords.openssl.openssl_keywords import OpenSSLKeywords
from web_pages.k8s_dashboard.login.k8s_login_page import K8sLoginPage
def check_url_access(url: str) -> tuple:
@@ -49,7 +51,7 @@ def copy_k8s_files(request: fixture, ssh_connection: SSHConnection):
ssh_connection (SSHConnection): ssh connection object
"""
k8s_dashboard_dir = "k8s_dashboard"
dashboard_file_names = ["admin-user.yaml", "kubeconfig.yaml", "k8s_dashboard.yaml"]
dashboard_file_names = ["admin-user.yaml", "k8s_dashboard.yaml"]
get_logger().log_info("Creating k8s_dashboard directory")
ssh_connection.send("mkdir -p {}".format(k8s_dashboard_dir))
for dashboard_file_name in dashboard_file_names:
@@ -112,9 +114,6 @@ def create_k8s_dashboard(request: fixture, namespace: str, con_ssh: SSHConnectio
request.addfinalizer(teardown)
KubectlApplyPatchKeywords(ssh_connection=con_ssh).apply_patch_service(svc_name=name, namespace=namespace, args_port=arg_port)
get_logger().log_info("Waiting 30s for the service to be up")
time.sleep(30)
get_logger().log_info(f"Verify that {name} is working")
end_point = OpenStackEndpointListKeywords(ssh_connection=con_ssh).get_k8s_dashboard_url()
status_code, _ = check_url_access(end_point)
@@ -148,11 +147,11 @@ def get_k8s_token(request: fixture, con_ssh: SSHConnection) -> str:
KubectlFileApplyKeywords(ssh_connection=con_ssh).apply_resource_from_yaml(admin_user_file_path)
get_logger().log_info("Creating the token for admin-user")
token = KubectlCreateTokenKeywords(ssh_connection=con_ssh).create_token(nspace="kube-system", user=serviceaccount)
token = KubectlCreateTokenKeywords(ssh_connection=con_ssh).create_token("kube-system", serviceaccount)
def teardown():
get_logger().log_info(f"Removing serviceaccount {serviceaccount} in kube-system")
KubectlDeleteServiceAccountKeywords(ssh_connection=con_ssh).cleanup_serviceaccount(serviceaccount_name=serviceaccount, nspace="kube-system")
KubectlDeleteServiceAccountKeywords(ssh_connection=con_ssh).cleanup_serviceaccount(serviceaccount, "kube-system")
request.addfinalizer(teardown)
@@ -160,6 +159,32 @@ def get_k8s_token(request: fixture, con_ssh: SSHConnection) -> str:
return token
def get_local_kubeconfig_path() -> str:
"""
Get the local path to the kubeconfig file.
Returns:
str: The local path to the kubeconfig.yaml file.
"""
kubeconfig_file = "kubeconfig.yaml"
local_path = get_stx_resource_path(f"resources/cloud_platform/containers/k8s_dashboard/{kubeconfig_file}")
return local_path
def update_token_in_local_kubeconfig(token: str) -> str:
"""
Update the token in the local kubeconfig file and save it to a temporary location.
Args:
token (str): The token to be updated in the kubeconfig file.
Returns:
str: The path to the updated temporary kubeconfig file.
"""
tmp_kubeconfig_path = YamlKeywords(ssh_connection=None).generate_yaml_file_from_template(template_file=get_local_kubeconfig_path(), target_file_name="kubeconfig_tmp.yaml", replacement_dictionary={"token_value": token}, target_remote_location=None, copy_to_remote=False)
return tmp_kubeconfig_path
@mark.p0
def test_k8s_dashboard_access(request):
"""
@@ -173,6 +198,26 @@ def test_k8s_dashboard_access(request):
- Check the copies on the SystemController.
Step 2: Create namespace kubernetes-dashboard
- Check that the dashboard is correctly created
Step 3: Create the necessary k8s dashboard resources
- Create SSL certificate for the dashboard.
- Create the necessary secrets.
- Apply the k8s dashboard yaml file.
- Expose the dashboard service on port 30000.
- Verify that the dashboard is accessible.
Step 4: Create the token for the dashboard
- Create the admin-user service-account.
- Bind the cluster-admin ClusterRoleBinding to the admin-user.
- Create a token for the admin-user.
Step 5: Navigate to K8s dashboard login page
- Get the k8s dashboard URL.
- Open the k8s dashboard login page.
- Login to the dashboard using the token.
Step 6 : Logout from the dashboard
- Logout from the dashboard
Step 7 : Login to the dashboard using kubeconfig file
- Update the token in the kubeconfig file
- Open the k8s dashboard login page.
- Login to the dashboard using the kubeconfig file.
Teardown:
- Delete the kubernetes-dashboard namespace
@@ -185,7 +230,7 @@ def test_k8s_dashboard_access(request):
# Opens an SSH session to active controller.
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
copy_k8s_files(request, ssh_connection)
# Create Dashboard namespace
# Step 2: Create Dashboard namespace
namespace_name = "kubernetes-dashboard"
kubectl_create_ns_keyword = KubectlCreateNamespacesKeywords(ssh_connection)
kubectl_create_ns_keyword.create_namespaces(namespace_name)
@@ -201,9 +246,26 @@ def test_k8s_dashboard_access(request):
request.addfinalizer(teardown)
# Step 2: Create the necessary k8s dashboard resources
# Step 3: Create the necessary k8s dashboard resources
test_namespace = "kubernetes-dashboard"
create_k8s_dashboard(request, namespace=test_namespace, con_ssh=ssh_connection)
# Step 3: Create the token for the dashboard
get_k8s_token(request=request, con_ssh=ssh_connection)
# Step 4: Create the token for the dashboard
token = get_k8s_token(request=request, con_ssh=ssh_connection)
# Step 5: Navigate to K8s dashboard login page
k8s_dashboard_url = OpenStackEndpointListKeywords(ssh_connection=ssh_connection).get_k8s_dashboard_url()
driver = WebDriverCore()
request.addfinalizer(lambda: driver.close())
login_page = K8sLoginPage(driver)
login_page.navigate_to_login_page(k8s_dashboard_url)
# Login to the dashboard using the token.
login_page.login_with_token(token)
# Step 6: Logout from dashboard
login_page.logout()
# Step 7: Login to the dashboard using kubeconfig file
kubeconfig_tmp_path = update_token_in_local_kubeconfig(token=token)
login_page.login_with_kubeconfig(kubeconfig_tmp_path)

View File

View File

@@ -0,0 +1,95 @@
from framework.web.condition.web_condition_element_visible import WebConditionElementVisible
from framework.web.webdriver_core import WebDriverCore
from web_pages.base_page import BasePage
from web_pages.k8s_dashboard.login.k8s_login_page_locators import K8sLoginPageLocators
class K8sLoginPage(BasePage):
"""
Page class that contains operations for the Login Page.
"""
def __init__(self, driver: WebDriverCore):
self.locators = K8sLoginPageLocators()
self.driver = driver
def navigate_to_login_page(self, dashboard_url: str) -> None:
"""
Navigates to the Kubernetes Dashboard Login Page.
Args:
dashboard_url (str): The URL of the Kubernetes Dashboard.
"""
signin_btn = self.locators.get_locator_signin_button()
condition = WebConditionElementVisible(signin_btn)
self.driver.navigate_to_url(dashboard_url, [condition])
def login_with_token(self, token: str) -> None:
"""
Logs in to the Kubernetes Dashboard using the provided token.
Args:
token (str): The authentication token to use for login.
"""
self.set_token(token)
self.click_signin()
def login_with_kubeconfig(self, kubeconfig_path: str) -> None:
"""
Logs in to the Kubernetes Dashboard using the provided kubeconfig file.
Args:
kubeconfig_path (str): The file path to the kubeconfig file.
"""
condition = WebConditionElementVisible(self.locators.get_locator_input_kubeconfig_file())
kubeconfig_option = self.locators.get_locator_kubeconfig_option()
self.driver.click(kubeconfig_option, conditions=[condition])
# this actually needs to be changed to send_keys
kubeconfig_input = self.locators.get_locator_input_kubeconfig_file()
self.driver.set_text(kubeconfig_input, kubeconfig_path)
self.click_signin()
def click_user_button(self) -> None:
"""
This function will click on the User button.
"""
condition = WebConditionElementVisible(self.locators.get_locator_sign_out_button())
self.driver.click(locator=self.locators.get_locator_user_button(), conditions=[condition])
def logout(self) -> None:
"""
This function will logout from the k8s dashboard.
"""
# click at user button first
self.click_user_button()
# click at logout button
self.click_signout()
def set_token(self, token: str) -> None:
"""
Sets the provided authentication token in the token input field.
Args:
token (str): The authentication token to be entered in the input field.
"""
token_input = self.locators.get_locator_token_input()
self.driver.set_text(token_input, token)
def click_signin(self):
"""
This function will click on the Signin button and check if the dashboard appears
"""
condition = WebConditionElementVisible(self.locators.get_locator_overview_dashboard())
signin_button = self.locators.get_locator_signin_button()
self.driver.click(signin_button, conditions=[condition])
def click_signout(self):
"""
This function will click on the Signout button and check if the login page appears
"""
condition = WebConditionElementVisible(self.locators.get_locator_signin_button())
signin_button = self.locators.get_locator_sign_out_button()
self.driver.click(signin_button, conditions=[condition])

View File

@@ -0,0 +1,65 @@
from selenium.webdriver.common.by import By
from framework.web.web_locator import WebLocator
class K8sLoginPageLocators:
"""
Page Elements class that contains elements for the Login Page.
"""
def get_locator_token_input(self) -> WebLocator:
"""
Locator for the Token Input field.
Returns: WebLocator
"""
return WebLocator("token", By.ID)
def get_locator_signin_button(self) -> WebLocator:
"""
Locator for the Login Button
Returns: WebLocator
"""
return WebLocator("//span[contains(text(),'Sign in')]", By.XPATH)
def get_locator_overview_dashboard(self) -> WebLocator:
"""
Locator for the Overview Dashboard element.
Returns: WebLocator
"""
return WebLocator("//div[contains(@class,'kd-toolbar-tools')]", By.XPATH)
def get_locator_kubeconfig_option(self) -> WebLocator:
"""
Locator for the Kubeconfig Option.
Returns: WebLocator
"""
return WebLocator("//input[contains(@value,'kubeconfig')]/..", By.XPATH)
def get_locator_input_kubeconfig_file(self) -> WebLocator:
"""
Locator for the Kubeconfig File Input.
Returns: WebLocator
"""
return WebLocator("""[title="fileInput"]""", By.CSS_SELECTOR)
def get_locator_user_button(self) -> WebLocator:
"""
Locator for the User Button.
Returns: WebLocator
"""
return WebLocator(".kd-user-panel-icon", By.CSS_SELECTOR)
def get_locator_sign_out_button(self) -> WebLocator:
"""
Locator for the Sign Out Button.
Returns: WebLocator
"""
return WebLocator("//button[contains(text(),'Sign out')]", By.XPATH)