Skip to content

Commit

Permalink
Add tests for default storage class properties (#360)
Browse files Browse the repository at this point in the history
* Inital test script for standard "scs-0211-v1-kaas-default-storage-class.md"

The script added in this commit holds the basic functionalities and tests used for checking this standard.

Signed-off-by: Toni Finger <toni.finger@cloudandheat.com>

* Apply k8s-default-storage-class as sonobuoy plugin

Signed-off-by: Toni Finger <toni.finger@cloudandheat.com>

* Update Tests/kaas/k8s-default-storage-class/k8s-default-storage-class-check.py

Co-authored-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com>
Signed-off-by: Kurt Garloff <kurt@garloff.de>

* Move supporting functions to helper.py

  Update Tests/kaas/k8s-default-storage-class/k8s-default-storage-class-check.py
  Update Tests/kaas/k8s-default-storage-class/helper.py

Signed-off-by: Toni Finger <toni.finger@cloudandheat.com>

---------

Signed-off-by: Toni Finger <toni.finger@cloudandheat.com>
Signed-off-by: Kurt Garloff <kurt@garloff.de>
Co-authored-by: Kurt Garloff <kurt@garloff.de>
Co-authored-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 16, 2023
1 parent a28b58c commit 4fd661c
Show file tree
Hide file tree
Showing 6 changed files with 433 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.idea
.DS_Store
node_modules
Tests/kaas/results/
*.tar.gz
21 changes: 21 additions & 0 deletions Tests/kaas/k8s-default-storage-class/Dockerfile_sonobuoy_plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ubuntu

# Install kubectl
# Note: Latest version may be found on:
# https://aur.archlinux.org/packages/kubectl-bin/
ADD https://storage.googleapis.com/kubernetes-release/release/v1.14.1/bin/linux/amd64/kubectl /usr/local/bin/kubectl

ENV HOME=/config

# Basic check it works.
RUN apt-get update && \
apt-get -y install net-tools && \
apt-get -y install curl && \
chmod +x /usr/local/bin/kubectl && \
kubectl version --client


COPY ./ ./
RUN chmod +x ./run_checks.sh

ENTRYPOINT ["./run_checks.sh"]
74 changes: 74 additions & 0 deletions Tests/kaas/k8s-default-storage-class/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import yaml
import sys
import logging
from kubernetes import client, config

manual_result_file_template = {
"name": None,
"status": None,
"details": {"messages": None},
}

logger = logging.getLogger("helper")


def initialize_logging():
logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)


def print_usage(file=sys.stderr):
"""Help output"""
print(
"""Usage: k8s_storageclass_check.py [options]
This tool checks the requested k8s default storage class according to the SCS Standard 0211 "kaas-default-storage-class".
Options:
[-k/--kubeconfig PATH_TO_KUBECONFIG] sets kubeconfig file to access kubernetes api
[-d/--debug] enables DEBUG logging channel
""",
end="",
file=file,
)


class SCSTestException(Exception):
"""Raised when an Specific test did not pass"""

def __init__(self, *args, return_code: int):
self.return_code = return_code


def setup_k8s_client(kubeconfigfile=None):

if kubeconfigfile:
logger.debug(f"using kubeconfig file '{kubeconfigfile}'")
config.load_kube_config(kubeconfigfile)
else:
logger.debug(" using system kubeconfig")
config.load_kube_config()

k8s_api_client = client.CoreV1Api()
k8s_storage_client = client.StorageV1Api()

return (
k8s_api_client,
k8s_storage_client,
)


def gen_sonobuoy_result_file(error_n: int, error_msg: str, test_file_name: str):

test_name = test_file_name.replace(".py", "")

test_status = "passed"

if error_n != 0:
test_status = test_name + "_" + str(error_n)

result_file = manual_result_file_template

result_file["name"] = test_name
result_file["status"] = test_status
result_file["details"]["messages"] = error_msg

with open(f"./{test_name}.result.yaml", "w") as file:
yaml.dump(result_file, file)
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#!/usr/bin/env python3

"""PersistentVolumeClaims checker
Return codes:
0: Default StorageClass is available, and setup to SCS standard
1: Not able to connect to k8s api
31: Default storage class has no provisioner
32: None or more then one default Storage Class is defined
41: Not able to bind PersitantVolume to PersitantVolumeClaim
42: ReadWriteOnce is not a supported access mode
All return codes between (and including) 1-19 as well as all return codes ending on 9
can be seen as failures.
Check given cloud for conformance with SCS standard regarding
Default StorageClass and PersistentVolumeClaims, to be found under /Standards/scs-0211-v1-kaas-default-storage-class.md
"""

import getopt
import sys
import time
import json
import logging

from kubernetes import client
from helper import gen_sonobuoy_result_file
from helper import SCSTestException
from helper import initialize_logging
from helper import print_usage
from helper import setup_k8s_client

import logging.config

logger = logging.getLogger("k8s-default-storage-class-check")


def check_default_storageclass(k8s_client_storage):

api_response = k8s_client_storage.list_storage_class(_preload_content=False)
storageclasses = api_response.read().decode("utf-8")
storageclasses_dict = json.loads(storageclasses)

ndefault_class = 0

for item in storageclasses_dict["items"]:
storage_class_name = item["metadata"]["name"]
annotations = item["metadata"]["annotations"]

if annotations["storageclass.kubernetes.io/is-default-class"] == "true":
ndefault_class += 1
default_storage_class = storage_class_name
provisioner = item["provisioner"]

if provisioner == "kubernetes.io/no-provisioner":
raise SCSTestException(
f"Provisioner is set to: {provisioner}.",
"This means the default storage class has no provisioner.",
return_code=31,
)

if ndefault_class != 1:
raise SCSTestException(
"More then one or none default StorageClass is defined! ",
f"Number of defined default StorageClasses = {ndefault_class} ",
return_code=32,
)

logger.info(f"One default Storage Class found:'{default_storage_class}'")
return default_storage_class


def check_default_persistentvolumeclaim_readwriteonce(k8s_api_instance, storage_class):
"""
1. Create PersistantVolumeClaim
2. Create pod which uses the PersitantVolumeClaim
3. Check if PV got succesfully created using ReadWriteOnce
4. Delete resources used for testing
"""

namespace = "default"
pvc_name = "test-pvc"
pv_name = "test-pv"
pod_name = "test-pod"

# 1. Create PersistantVolumeClaim
logger.debug(f"create pvc: {pvc_name}")

pvc_meta = client.V1ObjectMeta(name=pvc_name)
pvc_resources = client.V1ResourceRequirements(
requests={"storage": "1Gi"},
)
pvc_spec = client.V1PersistentVolumeClaimSpec(
access_modes=["ReadWriteOnce"],
storage_class_name=storage_class,
resources=pvc_resources,
)
body_pvc = client.V1PersistentVolumeClaim(
api_version="v1", kind="PersistentVolumeClaim", metadata=pvc_meta, spec=pvc_spec
)

api_response = k8s_api_instance.create_namespaced_persistent_volume_claim(
namespace, body_pvc
)

# 2. Create a pod which makes use of the PersitantVolumeClaim
logger.debug(f"create pod: {pod_name}")

pod_vol = client.V1Volume(
name=pv_name,
persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(pvc_name),
)
pod_con = client.V1Container(
name="nginx",
image="nginx",
ports=[client.V1ContainerPort(container_port=80)],
volume_mounts=[
client.V1VolumeMount(name=pv_name, mount_path="/usr/share/nginx/html")
],
)
pod_spec = client.V1PodSpec(volumes=[pod_vol], containers=[pod_con])
pod_body = client.V1Pod(
api_version="v1",
kind="Pod",
metadata=client.V1ObjectMeta(name=pod_name),
spec=pod_spec,
)

api_response = k8s_api_instance.create_namespaced_pod(
namespace, pod_body, _preload_content=False
)
pod_info = json.loads(api_response.read().decode("utf-8"))
pod_status = pod_info["status"]["phase"]

# Check if pod is up and running:
retries = 0
while pod_status != "Running" and retries <= 30:

api_response = k8s_api_instance.read_namespaced_pod(
pod_name, namespace, _preload_content=False
)
pod_info = json.loads(api_response.read().decode("utf-8"))
pod_status = pod_info["status"]["phase"]
logger.debug(f"retries:{retries} status:{pod_status}")
time.sleep(1)
retries += 1

# assert pod_status == "Running"
if pod_status != "Running":
raise SCSTestException(
"pod is not Running not able to setup test Enviornment",
return_code=13,
)

# 3. Check if PV got succesfully created using ReadWriteOnce
logger.debug("check if the created PV supports ReadWriteOnce")

api_response = k8s_api_instance.list_persistent_volume(_preload_content=False)

pv_info = json.loads(api_response.read().decode("utf-8"))
pv_list = pv_info["items"]

logger.debug("searching for corresponding pv")
for pv in pv_list:
logger.debug(f"parsing pv: {pv['metadata']['name']}")
if pv["spec"]["claimRef"]["name"] == pvc_name:
logger.debug(f"found pv to pvc: {pvc_name}")

if pv["status"]["phase"] != "Bound":
raise SCSTestException(
"Not able to bind pv to pvc",
return_code=41,
)

if "ReadWriteOnce" not in pv["spec"]["accessModes"]:
raise SCSTestException(
"access mode 'ReadWriteOnce' is not supported",
return_code=42,
)

# 4. Delete resources used for testing
logger.debug(f"delete pod:{pod_name}")
api_response = k8s_api_instance.delete_namespaced_pod(pod_name, namespace)
logger.debug(f"delete pvc:{pvc_name}")
api_response = k8s_api_instance.delete_namespaced_persistent_volume_claim(
pvc_name, namespace
)

return 0


def main(argv):

initialize_logging()
return_code = 0
return_message = "return_message: FAILED"

try:
opts, args = getopt.gnu_getopt(argv, "k:h", ["kubeconfig=", "help"])
except getopt.GetoptError as exc:
logger.debug(f"{exc}", file=sys.stderr)
print_usage()
return 1

kubeconfig = None

for opt in opts:
if opt[0] == "-h" or opt[0] == "--help":
print_usage()
return 0
if opt[0] == "-k" or opt[0] == "--kubeconfig":
kubeconfig = opt[1]
else:
print_usage(kubeconfig)
return 2

# Setup kubernetes client
try:
logger.debug("setup_k8s_client(kubeconfig)")
k8s_core_api, k8s_storage_api = setup_k8s_client(kubeconfig)
except Exception as exception_message:
logger.info(f"{exception_message}")
return_message = f"{exception_message}"
return_code = 1

# Check if default storage class is defined (MENTETORY)
try:
logger.info("check_default_storageclass()")
default_class_name = check_default_storageclass(k8s_storage_api)
except SCSTestException as test_exception:
logger.info(f"{test_exception}")
return_message = f"{test_exception}"
return_code = test_exception.return_code
except Exception as exception_message:
logger.info(f"{exception_message}")
return_message = f"{exception_message}"
return_code = 1

# Check if default_persistent volume has ReadWriteOnce defined (MENTETORY)
try:
logger.info("check_default_persistentvolume_readwriteonce()")
return_code = check_default_persistentvolumeclaim_readwriteonce(
k8s_core_api, default_class_name
)
except SCSTestException as test_exception:
logger.info(f"{test_exception}")
return_message = f"{test_exception}"
return_code = test_exception.return_code
except Exception as exception_message:
logger.info(f"{exception_message}")
return_message = f"{exception_message}"
return_code = 1

logger.debug(f"return_code:{return_code}")

if return_code == 0:
return_message = "all tests passed"

gen_sonobuoy_result_file(return_code, return_message, __file__)

return return_code


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
sonobuoy-config:
driver: Job
plugin-name: k8s-default-storage-class
result-format: manual
resutl-file: k8s-default-storage-class-check.result.yaml

spec:
args:
- k8s-default-storage-class-check
command:
- ./run_checks.sh
image: ghcr.io/sovereigncloudstack/standards/k8s-default-storage-class:latest
name: k8s-default-storage-class
Loading

0 comments on commit 4fd661c

Please sign in to comment.