diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ea6f46c..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - - -## [1.12.0] - 2021-02-10 -### Fixed -- [Issue-16] HealthCheck customization via environment override - - -[Issue-16]:https://github.com/litmuschaos/litmus-python/issues/16 diff --git a/Dockerfile b/Dockerfile index 87bd61f..b105605 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN ./install.sh WORKDIR /litmus # Copying experiment file -COPY ./bin/experiment/experiment.py ./experiments +COPY ./bin/experiment/experiment.py ./experiment ENV PYTHONPATH /litmus diff --git a/__init__.py b/__init__.py index e69de29..fef66b5 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python \ No newline at end of file diff --git a/bin/experiment/__pycache__/experiment.cpython-38.pyc b/bin/experiment/__pycache__/experiment.cpython-38.pyc deleted file mode 100644 index d36fccc..0000000 Binary files a/bin/experiment/__pycache__/experiment.cpython-38.pyc and /dev/null differ diff --git a/bin/experiment/experiment.py b/bin/experiment/experiment.py index 4576902..786f439 100755 --- a/bin/experiment/experiment.py +++ b/bin/experiment/experiment.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -import experimentList.generic.podDelete.podDelete as podDelete +import experiments.generic.pod_delete.pod_delete as pod_delete import argparse import logging import pkg.utils.client.client as client - logging.basicConfig(format='time=%(asctime)s level=%(levelname)s msg=%(message)s', level=logging.INFO) def main(): @@ -23,10 +22,9 @@ def main(): # invoke the corresponding experiment based on the the (-name) flag if args.name == "pod-delete": - podDelete.PodDelete(clients) + pod_delete.PodDelete(clients) else: logging.error("Unsupported -name %s, please provide the correct value of -name args", args.name) return if __name__ == "__main__": main() - \ No newline at end of file diff --git a/chaosLib/litmus/poddelete/__init__.py b/chaosLib/litmus/pod_delete/__init__.py similarity index 100% rename from chaosLib/litmus/poddelete/__init__.py rename to chaosLib/litmus/pod_delete/__init__.py diff --git a/chaosLib/litmus/poddelete/lib/__init__.py b/chaosLib/litmus/pod_delete/lib/__init__.py similarity index 100% rename from chaosLib/litmus/poddelete/lib/__init__.py rename to chaosLib/litmus/pod_delete/lib/__init__.py diff --git a/chaosLib/litmus/poddelete/lib/podDelete.py b/chaosLib/litmus/pod_delete/lib/pod_delete.py similarity index 100% rename from chaosLib/litmus/poddelete/lib/podDelete.py rename to chaosLib/litmus/pod_delete/lib/pod_delete.py diff --git a/contribute/developer-guide/README.md b/contribute/developer-guide/README.md new file mode 100644 index 0000000..086f365 --- /dev/null +++ b/contribute/developer-guide/README.md @@ -0,0 +1,310 @@ +## Steps to Bootstrap a Chaos Experiment + +The artifacts associated with a chaos-experiment are summarized below: + +- Submitted in the litmuschaos/litmus-python repository, under the experiments/*chaos_category*/*experiment_name* folder + + - Experiment business logic in python. May involve creating new or reusing an existing chaosLib + - Experiment test deployment manifest that is used for verification purposes + - Experiment RBAC (holds experiment-specific ServiceAccount, Role and RoleBinding) + + Example: [pod delete experiment in litmus-python](/experiments/generic/pod_delete) + +- Submitted in litmuschaos/chaos-charts repository, under the *chaos_category* folder + + - Experiment custom resource (CR) (holds experiment-specific chaos parameters & experiment entrypoint) + - Experiment ChartServiceVersion (holds experiment metadata that will be rendered on [charthub](https://hub.litmuschaos.io/)) + - Experiment RBAC (holds experiment-specific ServiceAccount, Role and RoleBinding) + - Experiment Engine (holds experiment-specific chaosengine) + + Example: [pod delete experiment in chaos-charts](https://github.com/litmuschaos/chaos-charts/tree/master/charts/generic/pod-delete) + +The *generate_experiment.py* script is a simple way to bootstrap your experiment, and helps create the aforementioned artifacts in the +appropriate directory (i.e., as per the chaos_category) based on an attributes file provided as input by the chart-developer. The +scaffolded files consist of placeholders which can then be filled as desired. + +### Pre-Requisites + +- *python3* is available (`sudo apt-get install python3`) +- *jinja2* & *pyYaml* python packages are available (`sudo apt-get install python3-pip`, `pip install jinja2`, `pip install pyYaml`) + +### Steps to Generate Experiment Manifests + +- Clone the litmus-python repository & navigate to the `contribute/developer-guide` folder + + ``` + $ git clone https://github.com/litmuschaos/litmus-python.git + $ cd litmus-python/contribute/developer-guide + ``` + +- Populate the `attributes.yaml` with details of the chaos experiment (or chart). Use the [attributes.yaml.sample](/contribute/developer-guide/attributes.yaml.sample) as reference. + + As an example, let us consider an experiment to kill one of the replicas of a nginx deployment. The attributes.yaml can be constructed like this: + + ```yaml + $ cat attributes.yaml + + --- + name: "sample_exec_chaos" + version: "0.1.0" + category: "sample_category" + repository: "https://github.com/litmuschaos/litmus-python/tree/master/sample_category/sample_exec_chaos" + community: "https://kubernetes.slack.com/messages/CNXNB0ZTN" + description: "it execs inside target pods to run the chaos inject commands, waits for the chaos duration and reverts the chaos" + keywords: + - "pods" + - "kubernetes" + - "sample-category" + - "exec" + platforms: + - Minikube + scope: "Namespaced" + auxiliaryappcheck: false + permissions: + - apigroups: + - "" + - "batch" + - "apps" + - "litmuschaos.io" + resources: + - "jobs" + - "pods" + - "pods/log" + - "events" + - "deployments" + - "replicasets" + - "pods/exec" + - "chaosengines" + - "chaosexperiments" + - "chaosresults" + verbs: + - "create" + - "list" + - "get" + - "patch" + - "update" + - "delete" + - "deletecollection" + maturity: "alpha" + maintainers: + - name: "oumkale" + email: "oumkale@chaosnative.com" + provider: + name: "ChaosNative" + minkubernetesversion: "1.12.0" + references: + - name: Documentation + url: "https://docs.litmuschaos.io/docs/getstarted/" + + ``` + +- Run the following command to generate the necessary artefacts for submitting the `sample_category` chaos chart with + `sample_exec_chaos` experiment. + + ``` + $ python3 generate_experiment.py -f=attributes.yaml -g= + ``` + + **Note**: Replace the `-g=` placeholder with the appropriate value based on the usecase: + - `experiment`: Chaos experiment artifacts belonging to an existing OR new experiment. + - `chart`: Just the chaos-chart metadata, i.e., chartserviceversion.yaml + - Provide the type of chart in the `-t=` flag. It supports the following values: + - `category`: It creates the chart metadata for the category i.e chartserviceversion, package manifests + - `experiment`: It creates the chart for the experiment i.e chartserviceversion, engine, rbac, experiment manifests + - `all`: it creates both category and experiment charts (default type) + + - Provide the path of the attribute.yaml manifest in the `-f` flag. + + View the generated files in `/experiments/` folder. + + ``` + $ cd /experiments + + $ ls -ltr + + total 8 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 16:44 __init__.py + drwxrwxr-x 3 oumkale oumkale 4096 Jul 7 16:44 generic/ + drwxrwxr-x 3 oumkale oumkale 4096 Jul 7 16:47 sample_category/ + + $ ls -ltr sample_category/ + + total 4 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 16:50 __init__.py + drwxr-xr-x 5 oumkale oumkale 4096 July 7 16:51 sample_exec_chaos/ + + $ ls -ltr sample_category/sample_exec_chaos/ + + total 12 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 16:47 __init__.py + drwxrwxr-x 2 oumkale oumkale 4096 Jul 7 16:48 experiment/ + drwxrwxr-x 2 oumkale oumkale 4096 Jul 7 16:49 charts/ + drwxrwxr-x 2 oumkale oumkale 4096 Jul 7 16:50 test/ + + $ ls -ltr sample_category/sample_exec_chaos/experiment + + total 8 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 18:43 __init__.py + -rw-rw-r-- 1 oumkale oumkale 6440 Jul 7 18:47 sample_exec_chaos.py + + $ ls -ltr sample_category/charts + + total 24 + -rw-rw-r-- 1 oumkale oumkale 144 Jul 7 18:48 sample_category.package.yaml + -rw-rw-r-- 1 oumkale oumkale 848 Jul 7 18:48 sample_category.category_chartserviceversion.yaml + -rw-rw-r-- 1 oumkale oumkale 989 Jul 7 18:48 sample_exec_chaos.experiment_chartserviceversion.yaml + -rw-rw-r-- 1 oumkale oumkale 1540 Jul 7 18:48 experiment.yaml + -rw-rw-r-- 1 oumkale oumkale 1224 Jul 7 18:48 rbac.yaml + -rw-rw-r-- 1 oumkale oumkale 731 Jul 7 18:48 engine.yaml + + $ ls -ltr sample-category/sample-exec-chaos/test + + total 4 + -rw-r--r-- 1 oumkale oumkale 1039 July 7 18:52 test.yaml + + $ ls -ltr chaosLib + total 4 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 16:44 __init__.py + drwxrwxr-x 4 oumkale oumkale 4096 Jul 7 18:43 litmus + + $ ls -ltr chaosLib/litmus + total 8 + drwxrwxr-x 3 oumkale oumkale 4096 Jul 7 16:44 pod_delete + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 16:44 __init__.py + drwxrwxr-x 2 oumkale oumkale 4096 Jul 7 18:43 sample_exec_chaos + + $ ls -ltr chaosLib/litmus/sample_exec_chaos + total 8 + -rw-rw-r-- 1 oumkale oumkale 0 Jul 7 18:43 __init__.py + -rw-rw-r-- 1 oumkale oumkale 5828 Jul 7 18:47 sample_exec_chaos.py + + ``` + +- Proceed with construction of business logic inside the `sample_exec_chaos.py` file, by making + the appropriate modifications listed below to achieve the desired effect: + + - variables + - entry & exit criteria checks for the experiment + - helper utils in either [pkg](/pkg/) or new [base chaos libraries](/chaosLib) + +- The chaosLib is created at `chaosLib/litmus/sample_exec_chaos/lib/sample_exec_chaos.py` path. It contains some pre-defined steps which runs the `ChaosInject` command (explicitly provided as an ENV var in the experiment CR). Which will induce chaos in the target application. It will wait for the given chaos duration and finally runs the `ChaosKill` command (also provided as an ENV var) for cleanup purposes. Update this chaosLib to achieve the desired effect based on the use-case or reuse the other existing chaosLib. + +- Create an experiment README explaining, briefly, the *what*, *why* & *how* of the experiment to aid users of this experiment. + +### Steps to Test Experiment + +We can use [Okteto](https://github.com/okteto/okteto) to help us in performing the dev-tests for experiment created. +Follow the steps provided below to setup okteto & test the experiment execution. + +- Install the Okteto CLI + + ``` + curl https://get.okteto.com -sSfL | sh + ``` + +- (Optional) Create a sample nginx deployment that can be used as the application under test (AUT). + + ``` + kubectl create deployment nginx --image=nginx + ``` + +- Setup the RBAC necessary for execution of this experiment by applying the generated `rbac.yaml` + + ``` + kubectl apply -f rbac.yaml + ``` + +- Modify the `test/test.yaml` with the desired values (app & chaos info) in the ENV and appropriate `chaosServiceAccount` along + with any other dependencies, if applicable (configmaps, volumes etc.,) & create this deployment + + ``` + kubectl apply -f test/test.yml + ``` + +- Go to the root of this repository (litmuschaos/litmus-python) & launch the Okteto development environment in your workspace. + This should take you to the bash prompt on the dev container into which the content of the litmus-python repo is loaded. + + - Note : + - Avoid use `_` in names ex: sample_category(not sample-category) + - Add packages routes for all the files which are generated from sdk in `setup.py` before creating image. + example : + ``` + 'chaosLib/litmus/sample_exec_chaos', + 'chaosLib/litmus/sample_exec_chaos/lib', + 'pkg/sample_category', + 'pkg/sample_category/environment', + 'pkg/sample_category/types', + 'experiments/sample_category', + 'experiments/sample_category/sample_exec_chaos', + 'experiments/sample_category/sample_exec_chaos/experiment', + ``` + - Add `&` operator in the end of chaos-inject command `CHAOS_INJECT_COMMAND` example: `md5sum /dev/zero &`. As we are running chaos command as a background process in a separate thread. + - Import main file it in bin/experiment/experiment.py and add case. example: line number 3 in experiment.py + - Then go to root(litmus-python) and run `python3 setup.py install` + + ``` + root@test:~/okteto/litmus-python# okteto up + + Deployment litmus-python doesn't exist in namespace litmus. Do you want to create a new one? [y/n]: y + ✓ Development container activated + ✓ Files synchronized + + The value of /proc/sys/fs/inotify/max_user_watches in your cluster nodes is too low. + This can affect file synchronization performance. + Visit https://okteto.com/docs/reference/known-issues/index.html for more information. + Namespace: default + Name: litmus-experiment + Forward: 2345 -> 2345 + 8080 -> 8080 + + Welcome to your development container. Happy coding! + ``` + + This dev container inherits the env, serviceaccount & other properties specified on the test deployment & is now suitable for + running the experiment. + +- Execute the experiment against the sample app chosen & verify the steps via logs printed on the console. + + ``` + python3 bin/experiment/experiment.py -name= + ``` + +- In parallel, observe the experiment execution via the changes to the pod/node state + + ``` + watch -n 1 kubectl get pods,nodes + ``` + +- If there are necessary changes to the code based on the run, make them via your favourite IDE. + These changes are automatically reflected on the dev container. Re-run the experiment to confirm changes. + +- Once the experiment code is validated, stop/remove the development environment + + ``` + root@test:~/okteto/litmus-python# okteto down + ✓ Development container deactivated + i Run 'okteto push' to deploy your code changes to the cluster + ``` + +- (Optional) Once the experiment has been validated using the above step, it can also be tested against the standard Litmus chaos + flow. This involves: + The experiment created using the above steps, can be tested in the following manner: + +- Run the `experiment.yml` with the desired values in the ENV and appropriate `chaosServiceAccount` + using a custom dev image instead of `litmuschaos/litmus-python` (say, oumkale/litmus-python) that packages the + business logic. + - Creating a custom image built with the code validated by the previous steps + - Launching the Chaos-Operator + - Modifying the ChaosExperiment manifest (experiment.yaml) with right defaults (env & other attributes, as applicable) & creating + this CR on the cluster (pointing the `.spec.definition.image` to the custom one just built) + - Modifying the ChaosEngine manifest (engine.yaml) with right app details, run properties & creating this CR to launch the chaos pods + - Verifying the experiment status via ChaosResult + + Refer [litmus docs](https://docs.litmuschaos.io/docs/getstarted/) for more details on performing each step in this procedure. + +### Steps to Include the Chaos Charts/Experiments into the ChartHub + +- Send a PR to the [litmus-python](https://github.com/litmuschaos/litmus-python) repo with the modified experiment files, rbac, + test deployment & README. +- Send a PR to the [chaos-charts](https://github.com/litmuschaos/chaos-charts) repo with the modified experiment CR, + experiment chartserviceversion, rbac, (category-level) chaos chart chartserviceversion & package.yaml (if applicable). diff --git a/contribute/developer-guide/attributes.yaml.sample b/contribute/developer-guide/attributes.yaml.sample new file mode 100644 index 0000000..5fd3251 --- /dev/null +++ b/contribute/developer-guide/attributes.yaml.sample @@ -0,0 +1,51 @@ +--- +name: "sample_exec_chaos" +version: "0.1.0" +category: "sample_category" +repository: "https://github.com/litmuschaos/litmus-python/tree/master/sample_category/sample_exec_chaos" +community: "https://kubernetes.slack.com/messages/CNXNB0ZTN" +description: "it execs inside target pods to run the chaos inject commands, waits for the chaos duration and reverts the chaos" +keywords: + - "pods" + - "kubernetes" + - "sample-category" + - "exec" +platforms: + - Minikube +scope: "Namespaced" +auxiliaryappcheck: false +permissions: + - apigroups: + - "" + - "batch" + - "apps" + - "litmuschaos.io" + resources: + - "jobs" + - "pods" + - "pods/log" + - "events" + - "deployments" + - "replicasets" + - "pods/exec" + - "chaosengines" + - "chaosexperiments" + - "chaosresults" + verbs: + - "create" + - "list" + - "get" + - "patch" + - "update" + - "delete" + - "deletecollection" +maturity: "alpha" +maintainers: + - name: "oumkale" + email: "oumkale@chaosnative.com" +provider: + name: "ChaosNative" +minkubernetesversion: "1.12.0" +references: + - name: Documentation + url: "https://docs.litmuschaos.io/docs/getstarted/" \ No newline at end of file diff --git a/contribute/developer-guide/generate_experiment.py b/contribute/developer-guide/generate_experiment.py new file mode 100644 index 0000000..62bc750 --- /dev/null +++ b/contribute/developer-guide/generate_experiment.py @@ -0,0 +1,278 @@ +from jinja2 import Environment, FileSystemLoader, select_autoescape +import yaml +import os +import sys +import argparse +import glob +import shutil + +# generate_csv creates __init__.py file which helps python to find the corresponding module +def generate_init(init_path): + init_path = init_path + '/' + '__init__.py' + open(init_path, mode='a').close() + +# generate_csv creates the experiment chartserviceversion manifest +def generate_csv(csv_parent_path, csv_name, csv_config, litmus_env): + + csv_filename = csv_parent_path + '/' + csv_name + '.' + 'experiment_chartserviceversion.yaml' + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment_chartserviceversion.tmpl') + output_from_parsed_template = template.render(csv_config) + with open(csv_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_csv creates the category chartserviceversion manifest +def generate_csv_cat(csv_parent_path, csv_name, csv_config, litmus_env): + + csv_filename = csv_parent_path + '/' + csv_name + '.' + 'category_chartserviceversion.yaml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/category_chartserviceversion.tmpl') + output_from_parsed_template = template.render(csv_config) + with open(csv_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_chart creates the experiment custom resource manifest +def generate_chart(chart_parent_path, chart_config, litmus_env): + chart_filename = chart_parent_path + '/' + 'experiment.yaml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment_custom_resource.tmpl') + output_from_parsed_template = template.render(chart_config) + with open(chart_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_rbac creates the rbac for the experiment +def generate_rbac(chart_parent_path, chart_config, litmus_env): + rbac_filename = chart_parent_path + '/' + 'rbac.yaml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment_rbac.tmpl') + output_from_parsed_template = template.render(chart_config) + with open(rbac_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_engine creates the chaos engine for the experiment +def generate_engine(chart_parent_path, chart_config, litmus_env): + engine_filename = chart_parent_path + '/' + 'engine.yaml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment_engine.tmpl') + output_from_parsed_template = template.render(chart_config) + with open(engine_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_chaoslib creates the chaosLib for the experiment +def generate_chaoslib(chaoslib_parent_path, chaoslib_name, chaoslib_config, litmus_env): + chaoslib_filename = chaoslib_parent_path + '/' + chaoslib_name + '.py' + + create_dir(chaoslib_parent_path) + + # Load Jinja2 template + template = litmus_env.get_template('./templates/chaoslib.tmpl') + output_from_parsed_template = template.render(chaoslib_config) + with open(chaoslib_filename, "w") as f: + f.write(output_from_parsed_template) + + # generate __init__.py file + generate_init(chaoslib_parent_path) + +# generate_environment creates the environment for the experiment +def generate_environment(environment_parent_path, environment_config, litmus_env): + environment_filename = environment_parent_path + '/environment.py' + + create_dir(environment_parent_path) + + # Load Jinja2 template + template = litmus_env.get_template('./templates/environment.tmpl') + output_from_parsed_template = template.render(environment_config) + with open(environment_filename, "w") as f: + f.write(output_from_parsed_template) + + # generate __init__.py file + generate_init(environment_parent_path) + +# generate_types creates the types.py for the experiment +def generate_types(types_parent_path, types_config, litmus_env): + types_filename = types_parent_path + '/types.py' + + create_dir(types_parent_path) + + # Load Jinja2 template + template = litmus_env.get_template('./templates/types.tmpl') + output_from_parsed_template = template.render(types_config) + with open(types_filename, "w") as f: + f.write(output_from_parsed_template) + + # generate __init__.py file + generate_init(types_parent_path) + +# generate_k8s_deployment creates the experiment kubernetes deployment manifest +def generate_k8s_deployment(k8s_parent_path, k8s_config, litmus_env): + k8s_filename = k8s_parent_path + '/' + 'test.yml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment_k8s_deployment.tmpl') + output_from_parsed_template = template.render(k8s_config) + with open(k8s_filename, "w") as f: + f.write(output_from_parsed_template) + +# generate_experiment creates the expriment.py file +def generate_experiment(experiment_parent_path, experiment_name, experiment_config, litmus_env): + experiment_filename = experiment_parent_path + '/' + experiment_name + '.py' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/experiment.tmpl') + output_from_parsed_template = template.render(experiment_config) + with open(experiment_filename, "w+") as f: + f.write(output_from_parsed_template) + + # generate __init__.py file + generate_init(experiment_parent_path) + +# generate_package creates the package manifest +def generate_package(package_parent_path, config, package_name, litmus_env): + package_filename = package_parent_path + '/' + package_name + '.package.yaml' + + # Load Jinja2 template + template = litmus_env.get_template('./templates/package.tmpl') + output_package = template.render(config) + with open(package_filename, "w") as f: + f.write(output_package) + +# create_dir create new directory +def create_dir(path): + if os.path.isdir(path) != True: + os.makedirs(path) + +def generate_icon(chart_parent_path, litmus_root, image_name, litmus_env): + src_dir = litmus_root + "/contribute/developer-guide/icons/" + dst_dir = chart_parent_path + '/' + "icons/" + create_dir(dst_dir) + + for jpgfile in glob.iglob(os.path.join(src_dir, "k8s.png")): + shutil.copy(jpgfile, dst_dir) + os.rename(dst_dir + '/' + 'k8s.png', dst_dir + '/' + image_name +'.png') + +def main(): + + parser = argparse.ArgumentParser() + parser.add_argument("-f", "--file", required=True, default="attributes.yaml", + help="metadata to generate experiment_chartserviceversion yaml") + parser.add_argument("-g", "--generate", required=True, default="experiment", + help="scaffold a new chart or experiment into existing chart") + parser.add_argument("-t", "--type", required=False, default="all", + help="type of the chaos chart") + args = parser.parse_args() + + entity_metadata_source = args.file + entity_type = args.generate + chartType = args.type + + # Load data from YAML file into a dictionary + # scalar values to Python the dictionary format + # YAML document to a Python object. + with open(entity_metadata_source) as f: + config = yaml.safe_load(f) + + # get name and category + entity_name = config['name'] + category_name = config['category'] + + env = Environment(loader = FileSystemLoader('./'), trim_blocks=True, lstrip_blocks=True, autoescape=select_autoescape(['yaml'])) + + # store the litmus root from bootstrap folder + litmus_root = os.path.abspath(os.path.join("..", os.pardir)) + + # initilise directories + exp_root_dir = litmus_root + '/experiments/' + '/' + config['category'] + create_dir(exp_root_dir) + experiment_root_dir = exp_root_dir + '/' + config['name'] + create_dir(experiment_root_dir) + generate_init(exp_root_dir) + + # if generate_type is chart, only generate the chart(top)-level CSV & package manifests + if entity_type == 'chart': + + # initilise chart directory + chart_dir = experiment_root_dir + '/charts' + create_dir(chart_dir) + + if chartType == "category" or chartType == "all": + + # generate icon for category + generate_icon(chart_dir, litmus_root, category_name, env) + + # generate category chartserviceversion + generate_csv_cat(chart_dir, category_name, config, env) + + # generate package + generate_package(chart_dir, config, category_name, env) + + if chartType == "experiment" or chartType == "all": + + # generate icon for category + generate_icon(chart_dir, litmus_root, entity_name, env) + + # generate experiment charts + generate_csv(chart_dir, entity_name, config, env) + + # generate experiment-custom-resource + generate_chart(chart_dir, config, env) + + # generate experiment specific rbac + generate_rbac(chart_dir, config, env) + + # generate experiment specific chaos engine + generate_engine(chart_dir, config, env) + + if chartType != "experiment" and chartType != "category" and chartType != "all": + print("Provided --chartType={} flag is invalid".format(chartType)) + return + print("chart created successfully") + # if generate_type is experiment, generate the litmusbook arefacts + elif entity_type == 'experiment': + + # initilise experiment directory + experiment_dir = experiment_root_dir + '/experiment' + create_dir(experiment_dir) + + # initialise test directory + test_dir = experiment_root_dir + '/test' + create_dir(test_dir) + + # generate __init__.py file in root experiment dir + generate_init(experiment_root_dir) + + # initialise chaosLib, environment and types directory + chaoslib_dir = litmus_root + '/chaosLib/litmus/' + config['name'] + '/lib' + environment_dir = litmus_root + '/pkg/' + config['category'] + '/environment' + types_dir = litmus_root + '/pkg/' + config['category'] + '/types' + + # create and generate __init__.py file in chaosLib experiment dir + create_dir(litmus_root + '/chaosLib/litmus/' + config['name']) + generate_init(litmus_root + '/chaosLib/litmus/' + config['name']) + + # generate experiment.py + generate_experiment(experiment_dir, entity_name, config, env) + + # generate chaosLib + generate_chaoslib(chaoslib_dir, entity_name, config, env) + + # generate environment.py + generate_environment(environment_dir, config, env) + + # generate environment.py + generate_types(types_dir, config, env) + + # generate k8s deployment + generate_k8s_deployment(test_dir, config, env) + + generate_init(litmus_root + '/pkg/' + config['category']) + + print("experiment created successfully") + else: + print("Provided --generate={} flag is invalid".format(entity_type)) + +if __name__=="__main__": + main() diff --git a/contribute/developer-guide/icons/k8s.png b/contribute/developer-guide/icons/k8s.png new file mode 100644 index 0000000..5d13f6b Binary files /dev/null and b/contribute/developer-guide/icons/k8s.png differ diff --git a/contribute/developer-guide/templates/category_chartserviceversion.tmpl b/contribute/developer-guide/templates/category_chartserviceversion.tmpl new file mode 100644 index 0000000..a6b25d3 --- /dev/null +++ b/contribute/developer-guide/templates/category_chartserviceversion.tmpl @@ -0,0 +1,34 @@ +apiVersion: litmuchaos.io/v1alpha1 +kind: ChartServiceVersion +metadata: + name: {{ category }} + version: {{ version }} + annotations: + categories: {{ category }} +spec: + displayName: {{ ccategory }} chaos + categoryDescription: > + {{ description }} + experiments: + - {{ name }} + keywords: + {%- for key in keywords %} + - "{{ key }}" + {%- endfor %} + maintainers: + {%- for i in maintainers %} + - name: {{- i.name }} + email: {{- i.email }} + {%- endfor %} + minKubeVersion: {{ minkubernetesversion }} + provider: + name: {{ provider.name }} + links: + {%- for ref in references %} + - name: {{- ref.name }} + url: {{- ref.url }} + {%- endfor %} + icon: + - url: + mediatype: "" + chaosexpcrdlink: https://raw.githubusercontent.com/litmuschaos/chaos-charts/master/charts/{{ category }}/experiments.yaml \ No newline at end of file diff --git a/contribute/developer-guide/templates/chaoslib.tmpl b/contribute/developer-guide/templates/chaoslib.tmpl new file mode 100644 index 0000000..183c166 --- /dev/null +++ b/contribute/developer-guide/templates/chaoslib.tmpl @@ -0,0 +1,133 @@ + +import pkg.types.types as types +import pkg.events.events as events +import logging, threading, signal, sys +import pkg.utils.common.common as common +import pkg.utils.common.pods as pods +import pkg.utils.exec.exec as litmusexec + +# signal object +class Signals(object): + def __init__(self, timeSignal=None, sigSignal=None): + self.timeSignal = timeSignal + self.sigSignal = sigSignal + +# experimentExecution check for the availibilty of the target pods and target containers for the chaos execution +# if the target pod and container found then it will start chaos execution +def experimentExecution(experimentsDetails, clients , resultDetails , eventsDetails , chaosDetails): + + # Get the target pod details for the chaos execution + # if the target pod is not defined it will derive the random target pod list using pod affected percentage + targetPodList, err = pods.Pods().GetPodList(experimentsDetails.TargetPods, experimentsDetails.PodsAffectedPerc, chaosDetails, clients) + if err != None: + return err + + podNames = [] + for pod in targetPodList.items: + podNames.append(pod.metadata.name) + logging.info("[Info]: Target pods list, %s", podNames) + + #Get the target container name of the application pod + if experimentsDetails.TargetContainer == "": + experimentsDetails.TargetContainer, err = common.GetTargetContainer(experimentsDetails.AppNS, targetPodList.items[0].metadata.name, clients) + if err != None: + return ValueError("unable to get the target container name, err: %s", err) + return runChaos(experimentsDetails, targetPodList, clients, resultDetails, eventsDetails, chaosDetails) + +# injectChaos inject the chaos +def injectChaos(experimentsDetails , podName , clients): + # It will contains all the pod & container details required for exec command + execCommandDetails = litmusexec.PodDetails() + command = ['/bin/sh', '-c', experimentsDetails.ChaosInjectCmd] + + litmusexec.SetExecCommandAttributes(execCommandDetails, podName, experimentsDetails.TargetContainer, experimentsDetails.AppNS) + err = litmusexec.Exec(execCommandDetails, clients, command) + if err != None: + return ValueError("unable to run command inside target container, err: {}".format(err)) + + return None + +# runChaos start the chaos injection and kill it after chaos injection completed or any abortion found +def runChaos(experimentsDetails , targetPodList, clients , resultDetails, eventsDetails , chaosDetails): + + sign = Signals() + sign.timeSignal = False + sign.sigSignal = False + signal.alarm(experimentsDetails.ChaosDuration) + + def signal_handler(sig, frame): + if sig == 14: + sign.timeSignal = True + else: + sign.sigSignal = True + + for pod in targetPodList.items : + + if experimentsDetails.EngineName != "" : + msg = "Injecting " + experimentsDetails.ExperimentName + " chaos on application pod" + types.SetEngineEventAttributes(eventsDetails, types.ChaosInject, msg, "Normal", chaosDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosEngine", clients) + + logging.info("[Chaos]: The Target application details container : %s, Pod : %s", experimentsDetails.TargetContainer, pod.metadata.name) + + # sendor thread is used to transmit signal notifications. + threading.Thread(target=injectChaos, args=(experimentsDetails, pod.metadata.name, clients)).start() + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGALRM, signal_handler) + + logging.info("[Chaos]:Waiting for: %s", experimentsDetails.ChaosDuration) + + while True: + if sign.timeSignal: + logging.info("[Chaos]: Time is up for experiment: %s", experimentsDetails.ExperimentName) + c1 = None + break + elif sign.sigSignal: + logging.info("[Chaos]: Revert Started") + err = killChaos(experimentsDetails, pod.metadata.name, clients) + if err != None: + logging.error("unable to kill chaos process after receiving abortion signal") + break + logging.info("[Chaos]: Revert Completed") + sys.exit(0) + + err = killChaos(experimentsDetails, pod.metadata.name, clients) + if err != None: + return err + + return None + +#PrepareChaos contains the prepration steps before chaos injection +def PrepareChaos(experimentsDetails, resultDetails, eventsDetails, chaosDetails, clients): + + #Waiting for the ramp time before chaos injection + if experimentsDetails.RampTime != 0 : + logging.info("[Ramp]: Waiting for the %s ramp time before injecting chaos",experimentsDetails.RampTime) + common.WaitForDuration(experimentsDetails.RampTime) + + #Starting the CPU stress experiment + err = experimentExecution(experimentsDetails, clients, resultDetails, eventsDetails, chaosDetails) + if err != None: + return err + + #Waiting for the ramp time after chaos injection + if experimentsDetails.RampTime != 0 : + logging.info("[Ramp]: Waiting for the %s ramp time after injecting chaos", experimentsDetails.RampTime) + common.WaitForDuration(experimentsDetails.RampTime) + + return None + +# killChaos kill the process +def killChaos(experimentsDetails, podName, clients): + # It will contains all the pod & container details required for exec command + execCommandDetails = litmusexec.PodDetails() + + command = ['/bin/sh', '-c', experimentsDetails.ChaosKillCmd] + + litmusexec.SetExecCommandAttributes(execCommandDetails, podName, experimentsDetails.TargetContainer, experimentsDetails.AppNS) + err = litmusexec.Exec(execCommandDetails, clients, command) + if err != None: + return ValueError("unable to kill the process in {} pod, err: {}".format(podName, err)) + + return diff --git a/contribute/developer-guide/templates/environment.tmpl b/contribute/developer-guide/templates/environment.tmpl new file mode 100644 index 0000000..6c6e7d6 --- /dev/null +++ b/contribute/developer-guide/templates/environment.tmpl @@ -0,0 +1,52 @@ + +import os +import pkg.types.types as types +import pkg.maths.maths as maths + +#GetENV fetches all the env variables from the runner pod +def GetENV(experimentDetails): + experimentDetails.ExperimentName = os.getenv("EXPERIMENT_NAME", "pod-delete") + experimentDetails.ChaosNamespace = os.getenv("CHAOS_NAMESPACE", "litmus") + experimentDetails.EngineName = os.getenv("CHAOSENGINE", "") + experimentDetails.ChaosDuration = maths.atoi(os.getenv("TOTAL_CHAOS_DURATION", "30")) + experimentDetails.ChaosInterval = os.getenv("CHAOS_INTERVAL", "10") + experimentDetails.RampTime = maths.atoi(os.getenv("RAMP_TIME", "0")) + experimentDetails.ChaosLib = os.getenv("LIB", "litmus") + experimentDetails.AppNS = os.getenv("APP_NAMESPACE", "") + experimentDetails.AppLabel = os.getenv("APP_LABEL", "") + experimentDetails.AppKind = os.getenv("APP_KIND", "") + experimentDetails.ChaosUID = os.getenv("CHAOS_UID", "") + experimentDetails.InstanceID = os.getenv("INSTANCE_ID", "") + experimentDetails.ChaosPodName = os.getenv("POD_NAME", "") + experimentDetails.Force = (os.getenv("FORCE", "false") == 'true') + experimentDetails.Delay = maths.atoi(os.getenv("STATUS_CHECK_DELAY", "2")) + experimentDetails.Timeout = maths.atoi(os.getenv("STATUS_CHECK_TIMEOUT", "180")) + experimentDetails.TargetPods = os.getenv("TARGET_PODS", "") + experimentDetails.PodsAffectedPerc = maths.atoi(os.getenv("PODS_AFFECTED_PERC", "0")) + experimentDetails.Sequence = os.getenv("SEQUENCE", "parallel") + experimentDetails.TargetContainer = os.getenv("TARGET_CONTAINER", "") + experimentDetails.ChaosInjectCmd = os.getenv("CHAOS_INJECT_COMMAND", "") + experimentDetails.ChaosKillCmd = os.getenv("CHAOS_KILL_COMMAND", "") + + +#InitialiseChaosVariables initialise all the global variables +def InitialiseChaosVariables(chaosDetails, experimentDetails): + appDetails = types.AppDetails() + appDetails.AnnotationCheck = (os.getenv("ANNOTATION_CHECK", "false") == 'true') + appDetails.AnnotationKey = os.getenv("ANNOTATION_KEY", "litmuschaos.io/chaos") + appDetails.AnnotationValue = "true" + appDetails.Kind = experimentDetails.AppKind + appDetails.Label = experimentDetails.AppLabel + appDetails.Namespace = experimentDetails.AppNS + + chaosDetails.ChaosNamespace = experimentDetails.ChaosNamespace + chaosDetails.ChaosPodName = experimentDetails.ChaosPodName + chaosDetails.ChaosUID = experimentDetails.ChaosUID + chaosDetails.EngineName = experimentDetails.EngineName + chaosDetails.ExperimentName = experimentDetails.ExperimentName + chaosDetails.InstanceID = experimentDetails.InstanceID + chaosDetails.Timeout = experimentDetails.Timeout + chaosDetails.Delay = experimentDetails.Delay + chaosDetails.AppDetail = appDetails + chaosDetails.Randomness = (os.getenv("RANDOMNESS", "false") == 'true') + diff --git a/contribute/developer-guide/templates/experiment.tmpl b/contribute/developer-guide/templates/experiment.tmpl new file mode 100644 index 0000000..399fa0b --- /dev/null +++ b/contribute/developer-guide/templates/experiment.tmpl @@ -0,0 +1,151 @@ +import pkg.types.types as types +import pkg.{{ category }}.types.types as experimentDetails +import pkg.{{ category }}.environment.environment as experimentEnv +import pkg.events.events as events +import logging +import pkg.status.application as application +import chaosLib.litmus.{{ name }}.lib.{{ name }} as litmusLIB +import pkg.result.chaosresult as chaosResults +import pkg.utils.common.common as common + +# Experiment contains steps to inject chaos +def Experiment(clients): + + # Initialising expermentDetails, resultDetails, eventsDetails, chaosDetails, status and result objects + experimentsDetails = experimentDetails.ExperimentDetails() + resultDetails = types.ResultDetails() + eventsDetails = types.EventDetails() + chaosDetails = types.ChaosDetails() + status = application.Application() + result = chaosResults.ChaosResults() + + #Fetching all the ENV passed from the runner pod + experimentEnv.GetENV(experimentsDetails) + + logging.info("[PreReq]: Initialise Chaos Variables for the %s experiment", experimentsDetails.ExperimentName) + # Intialise the chaos attributes + experimentEnv.InitialiseChaosVariables(chaosDetails, experimentsDetails) + + # Intialise Chaos Result Parameters + types.SetResultAttributes(resultDetails, chaosDetails) + + #Updating the chaos result in the beginning of experiment + logging.info("[PreReq]: Updating the chaos result of %s experiment (SOT)",(experimentsDetails.ExperimentName)) + err = result.ChaosResult(chaosDetails, resultDetails, "SOT", clients) + if err != None: + logging.error("Unable to Create the Chaos Result, err: %s",(err)) + failStep = "Updating the chaos result of pod-delete experiment (SOT)" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + + # Set the chaos result uid + result.SetResultUID(resultDetails, chaosDetails, clients) + + # generating the event in chaosresult to marked the verdict as awaited + msg = "Experiment " + experimentsDetails.ExperimentName + ", Result Awaited" + types.SetResultEventAttributes(eventsDetails, types.AwaitedVerdict, msg, "Normal", resultDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosResult", clients) + + #DISPLAY THE APP INFORMATION + logging.info("[Info]: The application information is as follows Namespace=%s, Label=%s, Ramp Time=%s",experimentsDetails.AppNS,experimentsDetails.AppLabel,experimentsDetails.RampTime) + + # Calling AbortWatcher, it will continuously watch for the abort signal and generate the required and result + common.AbortWatcher(experimentsDetails.ExperimentName, resultDetails, chaosDetails, eventsDetails, clients) + + # PRE-CHAOS APPLICATION STATUS CHECK + logging.info("[Status]: Verify that the AUT (Application Under Test) is running (pre-chaos)") + err = status.AUTStatusCheck(experimentsDetails.AppNS, experimentsDetails.AppLabel, experimentsDetails.TargetContainer, experimentsDetails.Timeout, experimentsDetails.Delay, chaosDetails, clients) + if err != None: + logging.error("Application status check failed, err: %s", err) + failStep = "Verify that the AUT (Application Under Test) is running (pre-chaos)" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + + {% if auxiliaryappcheck == true %} + #PRE-CHAOS AUXILIARY APPLICATION STATUS CHECK + if experimentsDetails.AuxiliaryAppInfo != "": + logging.info("[Status]: Verify that the Auxiliary Applications are running (pre-chaos)") + err = status.CheckAuxiliaryApplicationStatus(experimentsDetails.AuxiliaryAppInfo, experimentsDetails.Timeout, experimentsDetails.Delay, clients) + if err != None: + logging.error("Auxiliary Application status check failed, err: %s", err) + failStep = "Verify that the Auxiliary Applications are running (pre-chaos)" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + {% endif %} + + if experimentsDetails.EngineName != "": + # marking AUT as running, as we already checked the status of application under test + msg = "AUT: Running" + # generating the for the pre-chaos check + types.SetEngineEventAttributes(eventsDetails, types.PreChaosCheck, msg, "Normal", chaosDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosEngine", clients) + + # Including the litmus lib for pod-delete + if experimentsDetails.ChaosLib == "litmus" : + err = litmusLIB.PrepareChaos(experimentsDetails, resultDetails, eventsDetails, chaosDetails, clients) + if err != None: + logging.error("Chaos injection failed, err: %s",(err)) + failStep = "failed in chaos injection phase" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + + else: + logging.info("[Invalid]: Please Provide the correct LIB") + failStep = "no match found for specified lib" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + + logging.info("[Confirmation]: %s chaos has been injected successfully", experimentsDetails.ExperimentName) + resultDetails.Verdict = "Pass" + + #POST-CHAOS APPLICATION STATUS CHECK + logging.info("[Status]: Verify that the AUT (Application Under Test) is running (post-chaos)") + err = status.AUTStatusCheck(experimentsDetails.AppNS, experimentsDetails.AppLabel, experimentsDetails.TargetContainer, experimentsDetails.Timeout, experimentsDetails.Delay, chaosDetails, clients) + if err != None: + logging.error("Application status check failed, err: %s", err) + failStep = "Verify that the AUT (Application Under Test) is running (post-chaos)" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + + {% if auxiliaryappcheck == true %} + #POST-CHAOS AUXILIARY APPLICATION STATUS CHECK + if experimentsDetails.AuxiliaryAppInfo != "": + logging.info("[Status]: Verify that the Auxiliary Applications are running (post-chaos)") + err = status.CheckAuxiliaryApplicationStatus(experimentsDetails.AuxiliaryAppInfo, experimentsDetails.Timeout, experimentsDetails.Delay, clients) + if err != None: + logging.error("Auxiliary Application status check failed, err: %s", err) + failStep = "Verify that the Auxiliary Applications are running (post-chaos)" + result.RecordAfterFailure(chaosDetails, resultDetails, failStep, eventsDetails, clients) + return + {% endif %} + + if experimentsDetails.EngineName != "" : + # marking AUT as running, as we already checked the status of application under test + msg = "AUT: Running" + + # generating post chaos event + types.SetEngineEventAttributes(eventsDetails, types.PostChaosCheck, msg, "Normal", chaosDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosEngine", clients) + + + #Updating the chaosResult in the end of experiment + logging.info("[The End]: Updating the chaos result of %s experiment (EOT)", experimentsDetails.ExperimentName) + err = result.ChaosResult(chaosDetails, resultDetails, "EOT", clients) + if err != None: + logging.error("Unable to Update the Chaos Result, err: %s", err) + return + + # generating the event in chaosresult to marked the verdict as pass/fail + msg = "Experiment " + experimentsDetails.ExperimentName + ", Result " + resultDetails.Verdict + reason = types.PassVerdict + eventType = "Normal" + if resultDetails.Verdict != "Pass": + reason = types.FailVerdict + eventType = "Warning" + + types.SetResultEventAttributes(eventsDetails, reason, msg, eventType, resultDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosResult", clients) + if experimentsDetails.EngineName != "": + msg = experimentsDetails.ExperimentName + " experiment has been " + resultDetails.Verdict + "ed" + types.SetEngineEventAttributes(eventsDetails, types.Summary, msg, "Normal", chaosDetails) + events.GenerateEvents(eventsDetails, chaosDetails, "ChaosEngine", clients) diff --git a/contribute/developer-guide/templates/experiment_chartserviceversion.tmpl b/contribute/developer-guide/templates/experiment_chartserviceversion.tmpl new file mode 100644 index 0000000..3216d1e --- /dev/null +++ b/contribute/developer-guide/templates/experiment_chartserviceversion.tmpl @@ -0,0 +1,40 @@ +apiVersion: litmuchaos.io/v1alpha1 +kind: ChartServiceVersion +metadata: + name: {{ name }} + version: {{ version }} + annotations: + categories: {{ category }} +spec: + displayName: {{ name }} + categoryDescription: > + {{ description }} + keywords: + {%- for key in keywords %} + - "{{ key }}" + {%- endfor %} + platforms: + {%- for plat in platforms %} + - "{{ plat }}" + {% endfor %} + maturity: {{ maturity }} + maintainers: + {%- for i in maintainers %} + - name: {{- i.name }} + email: {{- i.email }} + {%- endfor %} + minKubeVersion: {{ minkubernetesversion }} + provider: + name: {{ provider.name }} + labels: + app.kubernetes.io/component: chartserviceversion + app.kubernetes.io/version: latest + links: + {%- for ref in references %} + - name: {{- ref.name }} + url: {{- ref.url }} + {%- endfor %} + icon: + - url: + mediatype: "" + chaosexpcrdlink: https://raw.githubusercontent.com/litmuschaos/chaos-charts/master/charts/{{ category }}/{{ name }}/experiment.yaml \ No newline at end of file diff --git a/contribute/developer-guide/templates/experiment_custom_resource.tmpl b/contribute/developer-guide/templates/experiment_custom_resource.tmpl new file mode 100644 index 0000000..c47a01f --- /dev/null +++ b/contribute/developer-guide/templates/experiment_custom_resource.tmpl @@ -0,0 +1,56 @@ +apiVersion: litmuschaos.io/v1alpha1 +description: + message: | + {{ description }} +kind: ChaosExperiment +metadata: + name: {{ name }} + labels: + name: {{ name }} + app.kubernetes.io/part-of: litmus + app.kubernetes.io/component: chaosexperiment + app.kubernetes.io/version: latest +spec: + definition: + scope: {{ scope }} + permissions: + {% for per in permissions %} + - apiGroups: + {%- for ag in per.apigroups %} + - "{{ ag }}" + {%- endfor %} + resources: + {%- for res in per.resources %} + - "{{ res }}" + {%- endfor %} + verbs: + {%- for vb in per.verbs %} + - "{{ vb }}" + {%- endfor %} + {% endfor %} + image: "litmuschaos/py-runner:latest" + imagePullPolicy: Always + args: + - -c + - python3 -u experiment + command: + - /bin/bash + env: + + - name: TOTAL_CHAOS_DURATION + value: '' + + - name: CHAOS_INTERVAL + value: '' + + - name: LIB + value: '' + + - name: RAMP_TIME + value: '' + + labels: + name: {{ name }} + app.kubernetes.io/part-of: litmus + app.kubernetes.io/component: experiment-job + app.kubernetes.io/version: latest diff --git a/contribute/developer-guide/templates/experiment_engine.tmpl b/contribute/developer-guide/templates/experiment_engine.tmpl new file mode 100644 index 0000000..32a5f8c --- /dev/null +++ b/contribute/developer-guide/templates/experiment_engine.tmpl @@ -0,0 +1,29 @@ +apiVersion: litmuschaos.io/v1alpha1 +kind: ChaosEngine +metadata: + name: nginx-chaos +spec: + appinfo: + appns: 'default' + applabel: 'app=nginx' + appkind: 'deployment' + # It can be active/stop + engineState: 'active' + #ex. values: ns1:name=percona,ns2:run=nginx + auxiliaryAppInfo: '' + chaosServiceAccount: {{ name }}-sa + # It can be delete/retain + jobCleanUpPolicy: 'delete' + experiments: + - name: {{ name }} + spec: + components: + env: + # set chaos duration (in sec) as desired + - name: TOTAL_CHAOS_DURATION + value: '30' + + # set chaos interval (in sec) as desired + - name: CHAOS_INTERVAL + value: '10' + \ No newline at end of file diff --git a/contribute/developer-guide/templates/experiment_k8s_deployment.tmpl b/contribute/developer-guide/templates/experiment_k8s_deployment.tmpl new file mode 100644 index 0000000..6e796f1 --- /dev/null +++ b/contribute/developer-guide/templates/experiment_k8s_deployment.tmpl @@ -0,0 +1,67 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: litmus-experiment +spec: + replicas: 1 + selector: + matchLabels: + app: litmus-experiment + template: + metadata: + labels: + app: litmus-experiment + spec: + serviceAccountName: {{ name }}-sa + containers: + - name: gotest + image: busybox + command: + - sleep + - "3600" + env: + # provide application namespace + - name: APP_NAMESPACE + value: '' + + # provide application labels + - name: APP_LABEL + value: '' + + # provide application kind + - name: APP_KIND + value: '' + + - name: TOTAL_CHAOS_DURATION + value: '' + + # provide auxiliary application details - namespace and labels of the applications + # sample input is - "ns1:app=percona,ns2:name=nginx" + - name: AUXILIARY_APPINFO + value: '' + + ## Period to wait before injection of chaos in sec + - name: RAMP_TIME + value: '' + + ## env var that describes the library used to execute the chaos + ## default: litmus. Supported values: litmus, powerfulseal, chaoskube + - name: LIB + value: '' + + # provide the chaos namespace + - name: CHAOS_NAMESPACE + value: '' + + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + + - name: CHAOS_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + + diff --git a/contribute/developer-guide/templates/experiment_rbac.tmpl b/contribute/developer-guide/templates/experiment_rbac.tmpl new file mode 100644 index 0000000..cace00c --- /dev/null +++ b/contribute/developer-guide/templates/experiment_rbac.tmpl @@ -0,0 +1,50 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ name }}-sa + namespace: default + labels: + name: {{ name }}-sa + app.kubernetes.io/part-of: litmus +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + name: {{ name }}-sa + namespace: default + labels: + name: {{ name }}-sa + app.kubernetes.io/part-of: litmus +rules: +{% for per in permissions %} + - apiGroups: + {%- for ag in per.apigroups %} + - "{{ ag }}" + {%- endfor %} + resources: + {%- for res in per.resources %} + - "{{ res }}" + {%- endfor %} + verbs: + {%- for vb in per.verbs %} + - "{{ vb }}" + {%- endfor %} + {% endfor %} +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: {{ name }}-sa + namespace: default + labels: + name: {{ name }}-sa + app.kubernetes.io/part-of: litmus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ name }}-sa +subjects: +- kind: ServiceAccount + name: {{ name }}-sa + namespace: default \ No newline at end of file diff --git a/contribute/developer-guide/templates/package.tmpl b/contribute/developer-guide/templates/package.tmpl new file mode 100644 index 0000000..900b068 --- /dev/null +++ b/contribute/developer-guide/templates/package.tmpl @@ -0,0 +1,5 @@ +packageName: {{ category }} +experiments: + - name: {{ name }} + CSV: {{ name }}.chartserviceversion.yaml + desc: "{{ name }}" \ No newline at end of file diff --git a/contribute/developer-guide/templates/types.tmpl b/contribute/developer-guide/templates/types.tmpl new file mode 100644 index 0000000..ae28f1f --- /dev/null +++ b/contribute/developer-guide/templates/types.tmpl @@ -0,0 +1,26 @@ +# ExperimentDetails is for collecting all the experiment-related details +class ExperimentDetails(object): + def __init__(self, ExperimentName=None, EngineName=None, ChaosDuration=None, ChaosInterval=None, RampTime=None, Force=None, ChaosLib=None, + ChaosServiceAccount=None, AppNS=None, AppLabel=None, ChaosInjectCmd=None, AppKind=None, InstanceID=None, ChaosNamespace=None, ChaosPodName=None, Timeout=None, + Delay=None, TargetPods=None, PodsAffectedPerc=None, ChaosKillCmd=None, Sequence=None, LIBImagePullPolicy=None, TargetContainer=None, UID=None): + self.ExperimentName = ExperimentName + self.EngineName = EngineName + self.ChaosDuration = ChaosDuration + self.ChaosInterval = ChaosInterval + self.RampTime = RampTime + self.ChaosLib = ChaosLib + self.AppNS = AppNS + self.AppLabel = AppLabel + self.AppKind = AppKind + self.InstanceID = InstanceID + self.ChaosUID = UID + self.ChaosNamespace = ChaosNamespace + self.ChaosPodName = ChaosPodName + self.Timeout = Timeout + self.Delay = Delay + self.TargetPods = TargetPods + self.PodsAffectedPerc = PodsAffectedPerc + self.LIBImagePullPolicy = LIBImagePullPolicy + self.ChaosInjectCmd = ChaosInjectCmd + self.ChaosKillCmd = ChaosKillCmd + self.TargetContainer = TargetContainer diff --git a/experimentList/__init__.py b/experiments/__init__.py similarity index 100% rename from experimentList/__init__.py rename to experiments/__init__.py diff --git a/experimentList/generic/__init__.py b/experiments/generic/__init__.py similarity index 100% rename from experimentList/generic/__init__.py rename to experiments/generic/__init__.py diff --git a/experimentList/generic/podDelete/__init__.py b/experiments/generic/pod_delete/__init__.py similarity index 100% rename from experimentList/generic/podDelete/__init__.py rename to experiments/generic/pod_delete/__init__.py diff --git a/experimentList/generic/podDelete/podDelete.py b/experiments/generic/pod_delete/pod_delete.py similarity index 96% rename from experimentList/generic/podDelete/podDelete.py rename to experiments/generic/pod_delete/pod_delete.py index 97d3de3..482a9f4 100644 --- a/experimentList/generic/podDelete/podDelete.py +++ b/experiments/generic/pod_delete/pod_delete.py @@ -1,10 +1,10 @@ import pkg.types.types as types -import pkg.generic.podDelete.types.types as experimentDetails -import pkg.generic.podDelete.environment.environment as experimentEnv +import pkg.generic.pod_delete.types.types as experimentDetails +import pkg.generic.pod_delete.environment.environment as experimentEnv import pkg.events.events as events import logging import pkg.status.application as application -import chaosLib.litmus.poddelete.lib.podDelete as litmusLIB +import chaosLib.litmus.pod_delete.lib.pod_delete as litmusLIB import pkg.result.chaosresult as chaosResults import pkg.utils.common.common as common diff --git a/pkg/events/events.py b/pkg/events/events.py index eb9c17b..4d299c1 100644 --- a/pkg/events/events.py +++ b/pkg/events/events.py @@ -1,7 +1,6 @@ from datetime import datetime import pytz from kubernetes import client -from kubernetes.client.rest import ApiException import pkg.utils.k8serror.k8serror as k8serror #CreateEvents create the events in the desired resource @@ -39,7 +38,7 @@ def CreateEvents(eventsDetails , chaosDetails, kind, eventName, clients): try: clients.clientCoreV1.create_namespaced_event(chaosDetails.ChaosNamespace, body=event) except Exception as exp: - return ValueError("Failed to create event with err: %s", exp) + return ValueError("Failed to create event with err: {}".format(exp)) return None #GenerateEvents update the events and increase the count by 1, if already present @@ -67,6 +66,6 @@ def GenerateEvents(eventsDetails, chaosDetails, kind, clients): event.message = eventsDetails.Message try: clients.clientCoreV1.patch_namespaced_event(eventName, chaosDetails.ChaosNamespace, body = event) - except ApiException as exp: + except Exception as exp: return exp return None diff --git a/pkg/generic/podDelete/__init__.py b/pkg/generic/pod_delete/__init__.py similarity index 100% rename from pkg/generic/podDelete/__init__.py rename to pkg/generic/pod_delete/__init__.py diff --git a/pkg/generic/podDelete/environment/__init__.py b/pkg/generic/pod_delete/environment/__init__.py similarity index 100% rename from pkg/generic/podDelete/environment/__init__.py rename to pkg/generic/pod_delete/environment/__init__.py diff --git a/pkg/generic/podDelete/environment/environment.py b/pkg/generic/pod_delete/environment/environment.py similarity index 100% rename from pkg/generic/podDelete/environment/environment.py rename to pkg/generic/pod_delete/environment/environment.py diff --git a/pkg/generic/podDelete/types/__init__.py b/pkg/generic/pod_delete/types/__init__.py similarity index 100% rename from pkg/generic/podDelete/types/__init__.py rename to pkg/generic/pod_delete/types/__init__.py diff --git a/pkg/generic/podDelete/types/types.py b/pkg/generic/pod_delete/types/types.py similarity index 100% rename from pkg/generic/podDelete/types/types.py rename to pkg/generic/pod_delete/types/types.py diff --git a/pkg/templates/chaos-result.j2 b/pkg/result/chaos-result.j2 similarity index 100% rename from pkg/templates/chaos-result.j2 rename to pkg/result/chaos-result.j2 diff --git a/pkg/result/chaosresult.py b/pkg/result/chaosresult.py index 8160fdd..1832d64 100644 --- a/pkg/result/chaosresult.py +++ b/pkg/result/chaosresult.py @@ -14,13 +14,13 @@ def ChaosResult(self, chaosDetails, resultDetails , state, clients): # Initialise experimentLabel experimentLabel = {} - + # It will list all the chaos-result with matching label # Note: We have added labels inside chaos result and looking for matching labels to list the chaos-result try: resultList = clients.clientDyn.resources.get(api_version="litmuschaos.io/v1alpha1", kind="ChaosResult").get(namespace=chaosDetails.ChaosNamespace, label_selector="name=" + resultDetails.Name) except Exception as exp: - return exp + return ValueError("Failed to get ChaosResult with matching label {} in namespace {}".format("name=" + resultDetails.Name, chaosDetails.ChaosNamespace)) # as the chaos pod won't be available for stopped phase # skipping the derivation of labels from chaos pod, if phase is stopped @@ -54,7 +54,7 @@ def InitializeChaosResult(self, chaosDetails , resultDetails , experimentLabel, passedRuns = 0, failedRuns = 0, stoppedRuns = 0, probeSuccessPercentage = "Awaited"): try: - env_tmpl = Environment(loader=PackageLoader('pkg', 'templates'), trim_blocks=True, lstrip_blocks=True, + env_tmpl = Environment(loader=PackageLoader('pkg', 'result'), trim_blocks=True, lstrip_blocks=True, autoescape=select_autoescape(['yaml'])) template = env_tmpl.get_template('chaos-result.j2') updated_chaosresult_template = template.render(name=resultDetails.Name, namespace=chaosDetails.ChaosNamespace, labels=experimentLabel, diff --git a/pkg/utils/common/common.py b/pkg/utils/common/common.py index 0d6c22b..119a595 100644 --- a/pkg/utils/common/common.py +++ b/pkg/utils/common/common.py @@ -34,6 +34,17 @@ def RandomInterval(interval): WaitForDuration(waitTime) return None +#GetTargetContainer will fetch the container name from application pod +#This container will be used as target container +def GetTargetContainer(appNamespace, appName, clients): + + try: + pod = clients.clientCoreV1.read_namespaced_pod(name=appName, namespace=appNamespace) + except Exception as e: + return "", e + + return pod.spec.containers[0].name, None + # GetRunID generate a random def GetRunID(): runId = ''.join(random.choices(string.ascii_uppercase + string.digits, k = 6)) diff --git a/pkg/utils/common/pods.py b/pkg/utils/common/pods.py index 7e8bde4..2434208 100644 --- a/pkg/utils/common/pods.py +++ b/pkg/utils/common/pods.py @@ -86,8 +86,8 @@ def FilterNonChaosPods(self, podList, chaosDetails, clients): nonChaosPods = [] # ignore chaos pods for pod in podList.items: - if (pod.metadata.labels["chaosUID"] != str(chaosDetails.ChaosUID) or pod.metadata.labels["name"] == "chaos-operator"): - nonChaosPods = nonChaosPods.append(pod) + if pod.metadata.labels.get("chaosUID") == "" and pod.metadata.labels.get("name") != "chaos-operator": + nonChaosPods.append(pod) return client.V1PodList(items=nonChaosPods) return podList diff --git a/pkg/templates/__init__.py b/pkg/utils/exec/__init__.py similarity index 100% rename from pkg/templates/__init__.py rename to pkg/utils/exec/__init__.py diff --git a/pkg/utils/exec/exec.py b/pkg/utils/exec/exec.py new file mode 100644 index 0000000..8344d7e --- /dev/null +++ b/pkg/utils/exec/exec.py @@ -0,0 +1,50 @@ + +from kubernetes.stream import stream + +# PodDetails contains all the required variables to exec inside a container +class PodDetails(object): + def __init__(self, PodName=None, Namespace=None, ContainerName=None): + self.PodName = PodName + self.Namespace = Namespace + self.ContainerName = ContainerName + +# checkPodStatus verify the status of given pod & container +def checkPodStatus(pod , containerName): + + if pod.status.phase.lower() != "running": + return ValueError("{} pod is not in running state, phase: {}".format(pod.Name, pod.Status.Phase)) + + for container in pod.status.container_statuses: + if container.name == containerName and not container.ready: + return ValueError("{} container of {} pod is not in ready state, phase: {}".format(container.name, pod.metadata.name, pod.status.phase)) + + return None + +def Exec(commandDetails, clients, command): + + try: + pod = clients.clientCoreV1.read_namespaced_pod(name=commandDetails.PodName, namespace=commandDetails.Namespace) + except Exception as exp: + return "", ValueError("unable to get {} pod in {} namespace, err: {}".format(commandDetails.PodName, commandDetails.Namespace, exp)) + + err = checkPodStatus(pod, commandDetails.ContainerName) + if err != None: + return "", err + + # Calling exec and waiting for response + stream(clients.clientCoreV1.connect_get_namespaced_pod_exec, + commandDetails.PodName, + commandDetails.Namespace, + command=command, + stderr=True, stdin=False, + stdout=True, tty=False) + + return None + +# SetExecCommandAttributes initialise all the pod details to run exec command +def SetExecCommandAttributes(podDetails , PodName, ContainerName, Namespace): + + podDetails.ContainerName = ContainerName + podDetails.Namespace = Namespace + podDetails.PodName = PodName + diff --git a/setup.py b/setup.py index 5973abc..7a033ba 100644 --- a/setup.py +++ b/setup.py @@ -45,13 +45,12 @@ def get_version_from_package() -> str: packages = [ 'chaosLib', 'chaosLib/litmus', - 'chaosLib/litmus/poddelete', - 'chaosLib/litmus/poddelete/lib', + 'chaosLib/litmus/pod_delete', + 'chaosLib/litmus/pod_delete/lib', 'pkg', 'pkg/', 'pkg/maths', 'pkg/result', - 'pkg/templates', 'pkg/types', 'pkg/status', 'pkg/utils', @@ -59,17 +58,18 @@ def get_version_from_package() -> str: 'pkg/utils/client', 'pkg/utils/annotation', 'pkg/utils/common', + 'pkg/utils/exec', 'pkg/generic', 'pkg/events', - 'pkg/generic/podDelete', - 'pkg/generic/podDelete/types', - 'pkg/generic/podDelete/environment', + 'pkg/generic/pod_delete', + 'pkg/generic/pod_delete/types', + 'pkg/generic/pod_delete/environment', 'pkg/status', 'bin', 'bin/experiment', - 'experimentList', - 'experimentList/generic', - 'experimentList/generic/podDelete', + 'experiments', + 'experiments/generic', + 'experiments/generic/pod_delete', ] needs_pytest = set(['pytest', 'test']).intersection(sys.argv) package_data = {