From ff72d30627b245bc44969e707fce3d33894d442b Mon Sep 17 00:00:00 2001 From: Piotr Date: Tue, 28 May 2024 23:28:54 +0200 Subject: [PATCH] feat: scenario init expansion Signed-off-by: Piotr --- killercoda_cli/__about__.py | 2 +- killercoda_cli/cli.py | 18 +---- killercoda_cli/scenario_init.py | 113 ++++++++++++++++++++++++++++++++ pyproject.toml | 4 +- tests/test_scenario_init.py | 109 ++++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 killercoda_cli/scenario_init.py create mode 100755 tests/test_scenario_init.py diff --git a/killercoda_cli/__about__.py b/killercoda_cli/__about__.py index c8a6e5d..82237a2 100644 --- a/killercoda_cli/__about__.py +++ b/killercoda_cli/__about__.py @@ -2,4 +2,4 @@ # # SPDX-License-Identifier: MIT # This is setup in build process in CI, but also servers as a record for the --version flag -__version__ = "1.0.3" +__version__ = "1.0.8" diff --git a/killercoda_cli/cli.py b/killercoda_cli/cli.py index c85847f..682ade7 100755 --- a/killercoda_cli/cli.py +++ b/killercoda_cli/cli.py @@ -6,6 +6,7 @@ import sys from typing import List, Optional from killercoda_cli.__about__ import __version__ +from killercoda_cli.scenario_init import init_project class FileOperation: """ @@ -369,23 +370,6 @@ def execute_file_operations(file_operations): elif operation.operation == "rename": os.rename(operation.path, operation.content) -def init_project(): - """initialize a new project by creating index.json file""" - if os.path.exists("index.json"): - print("The 'index.json' file already exists. Please edit the existing file.") - return - - index_data = { - "details": { - "title": "Project Title", - "description": "Project Description", - "steps": [], - } - } - with open("index.json", "w") as index_file: - json.dump(index_data, index_file, ensure_ascii=False, indent=4) - print("Project initialized successfully. Please edit the 'index.json' file to add steps.") - def main(): """ This function orchestrates the entire process of adding a new step to the scenario, diff --git a/killercoda_cli/scenario_init.py b/killercoda_cli/scenario_init.py new file mode 100644 index 0000000..1a6fab4 --- /dev/null +++ b/killercoda_cli/scenario_init.py @@ -0,0 +1,113 @@ +import json +import os +import inquirer + +# Define environment and backend options +environments = { + "ubuntu": "Ubuntu 20.04 with Docker and Podman", + "ubuntu-4GB": "Ubuntu 20.04 with Docker and Podman, 4GB environment", + "kubernetes-kubeadm-1node": "Kubeadm cluster with one control plane, taint removed, ready to schedule workload, 2GB environment", + "kubernetes-kubeadm-1node-4GB": "Kubeadm cluster with one control plane, taint removed, ready to schedule workload, 4GB environment", + "kubernetes-kubeadm-2nodes": "Kubeadm cluster with one control plane and one node, ready to schedule workload, 4GB environment" +} + +backends = { + "kubernetes-kubeadm-1node": "Kubernetes kubeadm 1 node", + "kubernetes-kubeadm-2nodes": "Kubernetes kubeadm 2 nodes", + "ubuntu": "Ubuntu 20.04" +} + +time_choices = [f"{i} minutes" for i in range(15, 50, 5)] +difficulty_choices = ["beginner", "intermediate", "advanced"] + +def get_value(prompt, value_type, choices=None): + if choices: + question = [inquirer.List('choice', message=prompt, choices=choices)] + answer = inquirer.prompt(question) + return answer['choice'] + else: + if value_type == "boolean": + question = [inquirer.Confirm('confirm', message=prompt, default=True)] + answer = inquirer.prompt(question) + return answer['confirm'] + else: + question = [inquirer.Text('value', message=prompt)] + answer = inquirer.prompt(question) + return answer['value'] + +def populate_schema(schema, parent_key=""): + result = {} + for key, value_type in schema.items(): + full_key = f"{parent_key}.{key}" if parent_key else key + if isinstance(value_type, dict): + result[key] = populate_schema(value_type, full_key) + elif isinstance(value_type, list): + if full_key == "details.steps" or full_key == "details.assets.host01": + result[key] = [] # Initialize as empty list + else: + result[key] = [] + while True: + add_item = get_value(f"Do you want to add an item to the list '{full_key}'?", "boolean") + if add_item: + result[key].append(populate_schema(value_type[0], full_key)) + else: + break + else: + if full_key == "time": + result[key] = get_value(f"Select value for {full_key}", value_type, time_choices) + elif full_key == "difficulty": + result[key] = get_value(f"Select value for {full_key}", value_type, difficulty_choices) + elif full_key == "backend.imageid": + result[key] = get_value(f"Select value for {full_key}", value_type, backends) + else: + result[key] = get_value(f"Enter value for {full_key}", value_type) + return result + +def init_project(): + """initialize a new project by creating index.json file""" + if os.path.exists("index.json"): + print("The 'index.json' file already exists. Please edit the existing file.") + return + + schema = { + "title": "string", + "description": "string", + "difficulty": "string", + "time": "string", + "details": { + "steps": [ + {} + ], + "assets": { + "host01": [ + {} + ] + } + }, + "backend": { + "imageid": "string" + } + } + + populated_data = populate_schema(schema) + + # Set static values for intro and finish + populated_data["details"]["intro"] = {"text": "intro.md"} + populated_data["details"]["finish"] = {"text": "finish.md"} + + if get_value("Do you want to enable Theia?", "boolean"): + populated_data["interface"] = {"layout": "ide"} + + with open("index.json", "w") as index_file: + json.dump(populated_data, index_file, ensure_ascii=False, indent=4) + + # Create intro.md and finish.md if they don't exist + if not os.path.exists("intro.md"): + with open("intro.md", "w") as intro_file: + intro_file.write("# Introduction\n") + + if not os.path.exists("finish.md"): + with open("finish.md", "w") as finish_file: + finish_file.write("# Finish\n") + + print("Project initialized successfully. Please edit the 'index.json' file to add steps.") diff --git a/pyproject.toml b/pyproject.toml index f976bf1..2454f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,9 @@ description = 'A CLI helper for writing killercoda scenarios and managing steps' readme = "README.md" requires-python = ">=3.8" license = "MIT" +dependencies = [ + "inquirer", +] keywords = [] authors = [ { name = "Piotr Zaniewski", email = "piotrzan@gmail.com" }, @@ -24,7 +27,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = [] [project.urls] Documentation = "https://github.com/unknown/killercoda-cli#readme" diff --git a/tests/test_scenario_init.py b/tests/test_scenario_init.py new file mode 100755 index 0000000..b19cf2d --- /dev/null +++ b/tests/test_scenario_init.py @@ -0,0 +1,109 @@ +import unittest +from unittest import mock +from unittest.mock import patch +from killercoda_cli import scenario_init + +class TestScenarioInit(unittest.TestCase): + + @patch("inquirer.prompt") + @patch("os.path.exists", return_value=False) + @patch("builtins.open", new_callable=mock.mock_open) + @patch("json.dump") + def test_init_project(self, mock_json_dump, mock_open, mock_exists, mock_prompt): + mock_prompt.side_effect = [ + {'value': 'Project Title'}, + {'value': 'Project Description'}, + {'choice': 'beginner'}, + {'choice': '15 minutes'}, + {'choice': 'kubernetes-kubeadm-1node'}, + {'confirm': True} + ] + + scenario_init.init_project() + + expected_data = { + "title": "Project Title", + "description": "Project Description", + "difficulty": "beginner", + "time": "15 minutes", + "details": { + "intro": {"text": "intro.md"}, + "finish": {"text": "finish.md"}, + "steps": [], + "assets": {"host01": []} + }, + "backend": {"imageid": "kubernetes-kubeadm-1node"}, + "interface": {"layout": "ide"} + } + + # Check that the index.json file was created with the expected data + calls = [ + mock.call("index.json", "w"), + mock.call("intro.md", "w"), + mock.call("finish.md", "w") + ] + mock_open.assert_has_calls(calls, any_order=True) + mock_json_dump.assert_called_once_with(expected_data, mock.ANY, ensure_ascii=False, indent=4) + + @patch("os.path.exists", return_value=True) + @patch("builtins.print") + def test_init_project_existing_index(self, mock_print, mock_exists): + scenario_init.init_project() + mock_print.assert_called_once_with("The 'index.json' file already exists. Please edit the existing file.") + + @patch("os.path.exists", side_effect=lambda path: path == "intro.md") + @patch("builtins.open", new_callable=mock.mock_open) + @patch("inquirer.prompt") + def test_init_project_create_finish_md(self, mock_prompt, mock_open, mock_exists): + mock_prompt.side_effect = [ + {'value': 'Project Title'}, + {'value': 'Project Description'}, + {'choice': 'beginner'}, + {'choice': '15 minutes'}, + {'choice': 'kubernetes-kubeadm-1node'}, + {'confirm': True} + ] + + scenario_init.init_project() + mock_open.assert_any_call("finish.md", "w") + mock_open().write.assert_any_call("# Finish\n") + + @patch("os.path.exists", side_effect=lambda path: path == "finish.md") + @patch("builtins.open", new_callable=mock.mock_open) + @patch("inquirer.prompt") + def test_init_project_create_intro_md(self, mock_prompt, mock_open, mock_exists): + mock_prompt.side_effect = [ + {'value': 'Project Title'}, + {'value': 'Project Description'}, + {'choice': 'beginner'}, + {'choice': '15 minutes'}, + {'choice': 'kubernetes-kubeadm-1node'}, + {'confirm': True} + ] + + scenario_init.init_project() + mock_open.assert_any_call("intro.md", "w") + mock_open().write.assert_any_call("# Introduction\n") + + @patch("os.path.exists", return_value=False) + @patch("builtins.open", new_callable=mock.mock_open) + @patch("json.dump") + @patch("inquirer.prompt") + def test_init_project_files_created(self, mock_prompt, mock_json_dump, mock_open, mock_exists): + mock_prompt.side_effect = [ + {'value': 'Project Title'}, + {'value': 'Project Description'}, + {'choice': 'beginner'}, + {'choice': '15 minutes'}, + {'choice': 'kubernetes-kubeadm-1node'}, + {'confirm': True} + ] + + scenario_init.init_project() + mock_open.assert_any_call("intro.md", "w") + mock_open().write.assert_any_call("# Introduction\n") + mock_open.assert_any_call("finish.md", "w") + mock_open().write.assert_any_call("# Finish\n") + +if __name__ == "__main__": + unittest.main()