diff --git a/.DS_Store b/.DS_Store index f2f7696..b76446c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.env b/.env index bbae92f..f64fca2 100644 --- a/.env +++ b/.env @@ -1,3 +1,6 @@ +#Shell runtime +SHELL_TYPE=bash + #Multipass Ubuntu Version MULTIPASS_INSTANCE=22.04 diff --git a/.gitignore b/.gitignore index 8b29db5..f7fbb78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ -.idea/ \ No newline at end of file +.idea/ +virtual-cluster \ No newline at end of file diff --git a/Readme.md b/Readme.md index 56462a6..beca4de 100644 --- a/Readme.md +++ b/Readme.md @@ -9,9 +9,16 @@ --- -Version : 0.0.2 +Version : 0.1.0 --- +## What's new? + +- The base language of this project has been changed to go language. +- There are more advantages than Python in CLI production, and open source called [cobra](https://github.com/spf13/cobra) was used for this project +- Apply the concept of concurrency programming using goroutine to show improved performance compared to Python version + +I'll organize the details with the patch note ASAP ## Required Spec @@ -25,7 +32,7 @@ Version : 0.0.2 - If your environment is 8GB of RAM : 1 master, 1 worker node is recommended - Least : 16 GB of RAM - Stable : 32 GB of RAM or upper -- Python3 : ver 3.8 upper +- Golang : **Upper than v 1.18** --- @@ -40,6 +47,8 @@ Version : 0.0.2 - Install helm - https://helm.sh/docs/intro/install/ +**If more than one requisite is not satisfied, CLI will return error like underbelow** +![img](./img/3.png) --- ## Before start @@ -52,12 +61,20 @@ git clone https://github.com/J-hoplin1/K3S-Virtual-Cluster.git cd K3S-Virtual-Cluster ``` -2. Install requirements.txt +2. Install required packages + +``` +go install +``` + +3. Compile ``` -pip3 install -r requirements.txt +go build ``` +Will going to provide a solution that is registered in the environmental variable and used in near future ☺️ + 3. Make your K3S Version of dotenv file. Default is `v1.26.6+k3s1`. Find available version [here](https://github.com/k3s-io/k3s/releases) ``` @@ -77,30 +94,30 @@ K3S_VERSION=v1.26.6+k3s1 - **Due to lack of computing resources, master / worker node may not initialize properly sometimes. Please check computing resource before initiate cluster. (Usually occured in 8/16 GB RAM PC)** ``` - python3 cluster.py -c init + ./virtual-cluster cluster init ``` - Terminate cluster ``` - python3 cluster.py -c terminate + ./virtual-cluster cluster terminate ``` - Add new worker-node to cluster (Scale-Out) - `-n` option is required when using `-c add` option ``` - python3 cluster.py -c add -n + ./virtual-cluster cluster add -n ``` - Connect to node instance's shell - `-n` option is required when using `-c shell` option ``` - python3 cluster.py -c shell -n + ./virtual-cluster cluster shell -n ``` - Delete worker-node from cluster - **Warning : This option ignore daemonsets and delete local datas of node!** - `-n` option is required when using `-c delete` option ``` - python3 cluster.py -c delete -n + ./virtual-cluster cluster delete -n ``` --- @@ -188,7 +205,3 @@ python3 cluster.py -c add -n --- -## TODO List - -- [ ] Polymorphism Support about helm chart -- [ ] Apply dashboard diff --git a/app.py b/app.py deleted file mode 100644 index 2257ab4..0000000 --- a/app.py +++ /dev/null @@ -1,274 +0,0 @@ -import subprocess -import os -from exception import exceptions -from utils.utility import Utility -from decorator.instanceNameAlreadyTakenChecker import InstanceNameAlreadyTakenChecker -from decorator.instanceExistChecker import InstanceExistChecker - - -class Assets(object): - MASTER_NODE_KEY = 'master-node' - MASTER_CONFIG = './nodes/master/config.json' - WORKER_CONFIG = './nodes/worker/config.json' - SCRIPT_PATH = './scripts' - KUBE_CONFIG_DIR = f"{os.environ['HOME']}/.kube" - KUBE_CONFIG = f"{os.environ['HOME']}/.kube/config" - KWARGS_WORKER_NODE_KEY = 'node_name' - - -class NodeType(object): - MASTER = 'master' - WORKER = 'worker' - - -class Resolver(Utility): - def __init__(self): - pass - - def cluster_init(self, version): - os.system('clear') - - def NodeInit(name, cpu, memory, disk, tp, *args): - if tp == NodeType.MASTER: - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/nodeInit.sh", name, - cpu, memory, disk, tp, version], stdout=subprocess.PIPE) - elif tp == NodeType.WORKER: - token, ip = args - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/nodeInit.sh", name, cpu, - memory, disk, tp, token, ip, version], stdout=subprocess.PIPE) - else: - raise exceptions.IllegalControlException("Invalid node type") - - def getNodeIP(name): - return subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/getNodeIP.sh", name], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() - - if not os.path.exists('./nodes'): - raise exceptions.DirectoryNotFound('nodes') - - # Get master node config - masterConfig: dict = self.readConfig(Assets.MASTER_CONFIG) - - print(self.getNormalMessage("Complete to load : Master config")) - - # Get worker node config - workerConfig: dict = self.readConfig(Assets.WORKER_CONFIG) - - print(self.getNormalMessage("Complete to load : Worker config")) - - # Initialize master node config - - if Assets.MASTER_NODE_KEY not in masterConfig: - raise exceptions.MasterNodeConfigNotFound() - - # Check - if not self.checkInstanceNameNotInUse(Assets.MASTER_NODE_KEY): - raise exceptions.InvalidNodeGenerationDetected( - self.getCriticalMessage( - f"Instance with '{Assets.MASTER_NODE_KEY}' already in use! Stop generating cluster.") - ) - - print(self.getNormalMessage( - f"Initiating {Assets.MASTER_NODE_KEY} ...")) - masterNodeName: str = Assets.MASTER_NODE_KEY - masterNodeInfo: dict = masterConfig[masterNodeName] - - ''' - If invalid config type - - Node config type should be dictionary - - -> Master Node : Raise Exception - -> Worker Node : Ignore - ''' - if not self.typeChecker(masterNodeInfo, dict): - raise exceptions.InvalidConfigType(dict) - - masterNodeCPU = masterNodeInfo.get( - 'cpu', os.environ['MASTER_DEFAULT_CPU']) - masterNodeMemory = masterNodeInfo.get( - 'memory', os.environ['MASTER_DEFAULT_MEMORY']) - masterNodeStorage = masterNodeInfo.get( - 'disk', os.environ['MASTER_DEFAULT_STORAGE']) - NodeInit(masterNodeName, masterNodeCPU, masterNodeMemory, - masterNodeStorage, NodeType.MASTER) - - masterNodeIP = getNodeIP(masterNodeName) - masterConfig[masterNodeName]["ip"] = masterNodeIP - masterNodeToken = subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/getMasterToken.sh", masterNodeName], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() - masterConfig[masterNodeName]["token"] = masterNodeToken - - if (not masterNodeIP) or (not masterNodeToken): - raise exceptions.ImproperMasterNodeGenerated() - print("\n") - print(self.getSpecialMessage(f"[[ Master node config values ]]")) - print(self.getSpecialMessage(f"Master Node IP : {masterNodeIP}")) - print(self.getSpecialMessage( - f"Master Node Token : {masterNodeToken}\n")) - print(self.getNormalMessage("Complete to build master node ...")) - - print("\n") - # Initialize worker node config - nodeCount = len(workerConfig) - failedNodeCount = 0 - - for index, items in enumerate(workerConfig.items()): - i, j = items - if not self.validateNodeName(i): - print(self.getCriticalMessage( - f"Fail to generate node - Illegal node name config : {i}")) - failedNodeCount += 1 - continue - workerNodeName: str = i - print(self.getNormalMessage( - f"[[ Initializing Node : {i} ({index + 1}/{nodeCount}) ]]")) - if not self.checkInstanceNameNotInUse(workerNodeName): - print(self.getCriticalMessage( - f"Instance with name '{workerNodeName}' already in use! Ignore generating worker-node config '{i}'")) - continue - - workerNodeInfo: dict = j - - if not self.typeChecker(workerNodeInfo, dict): - print(self.getCriticalMessage( - f"Invalid config type of worker node : {workerNodeName}!")) - continue - print(self.getNormalMessage( - f"Initiating worker node : {workerNodeName} ...")) - workerNodeCPU = workerNodeInfo.get( - 'cpu', os.environ['WORKER_DEFAULT_CPU']) - workerNodeMemory = workerNodeInfo.get( - 'memory', os.environ['WORKER_DEFAULT_MEMORY']) - workerNodeStorage = workerNodeInfo.get( - 'disk', os.environ['WORKER_DEFAULT_STORAGE']) - NodeInit(workerNodeName, workerNodeCPU, workerNodeMemory, - workerNodeStorage, NodeType.WORKER, masterNodeToken, masterNodeIP) - workerNodeIP = getNodeIP(workerNodeName) - workerConfig[i]["ip"] = workerNodeIP - print(self.getNormalMessage( - f"Complete to build worker node : {workerNodeName} ...\n")) - - self.saveConfig(Assets.MASTER_CONFIG, masterConfig) - self.saveConfig(Assets.WORKER_CONFIG, workerConfig) - - # Make kube config directory if not exist - if not os.path.exists(Assets.KUBE_CONFIG_DIR): - os.mkdir(Assets.KUBE_CONFIG_DIR) - - # If kubeconfig exist, make it copy - if os.path.exists(Assets.KUBE_CONFIG): - os.rename(Assets.KUBE_CONFIG, - f"{Assets.KUBE_CONFIG_DIR}/config_cp") - print(self.getSpecialMessage( - f"Saving previous kubectl config file as {Assets.KUBE_CONFIG_DIR}/config_cp ...")) - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/getKubeConfig.sh", - masterNodeName, masterNodeIP, Assets.KUBE_CONFIG]) - - print(f"""\n -{self.getNormalMessage(f"Success - {nodeCount - failedNodeCount}/{nodeCount}")} -{self.getCriticalMessage(f"Failed - {failedNodeCount}/{nodeCount}",False)} - """) - - print(self.getSpecialMessage(f"πŸš€ Cluster ready!")) - - def terminate_cluster(self): - print("It may take a while... please wait") - # Get master node config - masterConfig: dict = self.readConfig(Assets.MASTER_CONFIG) - - # Get worker node config - workerConfig: dict = self.readConfig(Assets.WORKER_CONFIG) - - instanceList = list(workerConfig.keys()) - instanceList.append(Assets.MASTER_NODE_KEY) - for i in instanceList: - if not self.validateNodeName(i): - print(self.getCriticalMessage( - f"Illegal node name config : {i}. Ignore this node from terminate process.")) - continue - print(self.getNormalMessage(f"Terminating node : {i}")) - subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/terminateCluster.sh", i]) - - if self.typeChecker(masterConfig[Assets.MASTER_NODE_KEY], dict): - if "ip" in masterConfig[Assets.MASTER_NODE_KEY]: - masterConfig[Assets.MASTER_NODE_KEY]["ip"] = "" - if "token" in masterConfig[Assets.MASTER_NODE_KEY]: - masterConfig[Assets.MASTER_NODE_KEY]["token"] = "" - - for i, j in workerConfig.items(): - if self.typeChecker(j, dict): - if "ip" in j: - workerConfig[i]["ip"] = "" - self.saveConfig(Assets.MASTER_CONFIG, masterConfig) - self.saveConfig(Assets.WORKER_CONFIG, workerConfig) - - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/purgeInstance.sh"]) - print(self.getNormalMessage("Complete to terminate cluster!")) - - @InstanceNameAlreadyTakenChecker() - def add_node(self, name, *args, **kwargs): - version = args[0][0] - masterConfig: dict = self.readConfig(Assets.MASTER_CONFIG) - # Get worker node config - workerConfig: dict = self.readConfig(Assets.WORKER_CONFIG) - - # name should be same with as in config file - if name not in workerConfig.keys(): - raise exceptions.WrongArgumentGiven( - f"Name '{name}' not found in worker config") - - workerNodeName = kwargs[Assets.KWARGS_WORKER_NODE_KEY] - - if not self.typeChecker(workerConfig[name], dict): - raise exceptions.InvalidConfigType(dict) - - workerNodeInfo = workerConfig[name] - workerNodeCPU = workerNodeInfo.get( - 'cpu', os.environ['WORKER_DEFAULT_CPU']) - workerNodeMemory = workerNodeInfo.get( - 'memory', os.environ['WORKER_DEFAULT_MEMORY']) - workerNodeStorage = workerNodeInfo.get( - 'disk', os.environ['WORKER_DEFAULT_STORAGE']) - token = masterConfig[Assets.MASTER_NODE_KEY]["token"] - ip = masterConfig[Assets.MASTER_NODE_KEY]["ip"] - if (not ip) or (not token): - raise exceptions.MasterNodeNotFound() - print(self.getNormalMessage( - f"Initiating worker node : {workerNodeName} ...")) - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/nodeInit.sh", workerNodeName, workerNodeCPU, - workerNodeMemory, workerNodeStorage, NodeType.WORKER, token, ip, version], stdout=subprocess.PIPE) - print(self.getNormalMessage( - f"Complete to build worker node : {workerNodeName} ...")) - workerConfig[name]["ip"] = subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/getNodeIP.sh", workerNodeName], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() - self.saveConfig(Assets.WORKER_CONFIG, workerConfig) - - @InstanceExistChecker() - def connectShell(self, name, **kwargs): - node_name = kwargs[Assets.KWARGS_WORKER_NODE_KEY] - subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/connectShell.sh", node_name]) - - @InstanceExistChecker() - def deleteNode(self, name, **kwargs): - workerConfig: dict = self.readConfig(Assets.WORKER_CONFIG) - - node_name = kwargs[Assets.KWARGS_WORKER_NODE_KEY] - if node_name == Assets.MASTER_NODE_KEY: - raise exceptions.IllegalControlException( - "Can't delete worker node by alone!") - - print(self.getNormalMessage(f"Delete node '{node_name}' from cluster")) - subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/deleteNode.sh", Assets.MASTER_NODE_KEY, node_name]) - print(self.getNormalMessage( - f"Complete to delete node '{node_name}' from cluster")) - print(self.getNormalMessage(f"Terminating instance : '{node_name}'")) - subprocess.run( - ["bash", f"{Assets.SCRIPT_PATH}/terminateCluster.sh", node_name]) - print(self.getNormalMessage( - f"Complete to terminate instance : '{node_name}'")) - workerConfig[name]["ip"] = "" - self.saveConfig(Assets.WORKER_CONFIG, workerConfig) - subprocess.run(["bash", f"{Assets.SCRIPT_PATH}/purgeInstance.sh"]) diff --git a/cluster.py b/cluster.py deleted file mode 100644 index ad73e8d..0000000 --- a/cluster.py +++ /dev/null @@ -1,47 +0,0 @@ -import argparse -import os -import dotenv -from app import Resolver -from exception import exceptions - -env_file = dotenv.find_dotenv() -dotenv.load_dotenv(env_file) - -resolver = Resolver() - -parser = argparse.ArgumentParser(description="K3S + Multipass virtual cluster") -parser.add_argument( - "-c", "--cluster", help=""" - Initiate cluster with : 'init' - Terminate cluster with : 'terminate' - Add node with : 'add' - Delete node with : 'delete' - Connect to node's shell with : 'shell' - """, required=True) -parser.add_argument( - "-n", "--name", help="Required if you use option of '-c' as 'add' and 'shell'. Node's name" -) - -argList = parser.parse_args() - -try: - if argList.cluster == "init": - resolver.cluster_init(os.environ["K3S_VERSION"]) - elif argList.cluster == "terminate": - resolver.terminate_cluster() - elif argList.cluster == "add": - if not argList.name: - raise exceptions.RequiredCommandLineOptionLost('-n') - resolver.add_node(argList.name, os.environ["K3S_VERSION"]) - elif argList.cluster == "shell": - if not argList.name: - raise exceptions.RequiredCommandLineOptionLost('-n') - resolver.connectShell(argList.name) - elif argList.cluster == "delete": - if not argList.name: - raise exceptions.RequiredCommandLineOptionLost('-n') - resolver.deleteNode(argList.name) - else: - raise exceptions.WrongArgumentGiven() -except Exception as e: - print(e) diff --git a/cmd/cluster/add.go b/cmd/cluster/add.go new file mode 100644 index 0000000..2ce22e2 --- /dev/null +++ b/cmd/cluster/add.go @@ -0,0 +1,35 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "github.com/spf13/cobra" + "virtual-cluster/service/cluster" +) + +// cluster/addCmd represents the cluster/add command +var addCmd = &cobra.Command{ + Use: "add", + Short: "Add node to your cluster", + Long: `Add node to your cluster`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if value, flagErr := cmd.Flags().GetString("name"); flagErr != nil { + err = flagErr + return + } else { + if len(args) > 0 { + err = errors.New("Unnecessary arguments found") + return + } + err = cluster.AddNode(value) + } + return + }, +} + +func init() { + addCmd.Flags().StringVarP(&nodename, "name", "n", "", "Name of node you want to add to cluster. Name should exist in config file") + addCmd.MarkFlagRequired("name") +} diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go new file mode 100644 index 0000000..4c22518 --- /dev/null +++ b/cmd/cluster/cluster.go @@ -0,0 +1,32 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var nodename string + +// clusterCmd represents the cluster command +var ClusterCmd = &cobra.Command{ + Use: "cluster", + Short: "Control your cluster", + Long: `Initialize your own multi-node cluster in your PC! +add node to your cluster, remove node from your cluster and terminate cluster + +Usage : vc cluster [command] [flag] +`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + ClusterCmd.AddCommand(addCmd) + ClusterCmd.AddCommand(removeCmd) + ClusterCmd.AddCommand(shellCmd) + ClusterCmd.AddCommand(initCmd) + ClusterCmd.AddCommand(terminateCmd) +} diff --git a/cmd/cluster/delete.go b/cmd/cluster/delete.go new file mode 100644 index 0000000..fb2d1e0 --- /dev/null +++ b/cmd/cluster/delete.go @@ -0,0 +1,35 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "github.com/spf13/cobra" + "virtual-cluster/service/cluster" +) + +// cluster/deleteCmd represents the cluster/delete command +var removeCmd = &cobra.Command{ + Use: "remove", + Short: "Delete specific node from cluster", + Long: `Delete specific node from cluster`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if value, flagErr := cmd.Flags().GetString("name"); flagErr != nil { + err = flagErr + return + } else { + if len(args) > 0 { + err = errors.New("Unnecessary arguments found") + return + } + err = cluster.RemoveNode(value) + } + return + }, +} + +func init() { + removeCmd.Flags().StringVarP(&nodename, "name", "n", "", "Name of node you want to delete") + addCmd.MarkFlagRequired("name") +} diff --git a/cmd/cluster/init.go b/cmd/cluster/init.go new file mode 100644 index 0000000..1e34bb8 --- /dev/null +++ b/cmd/cluster/init.go @@ -0,0 +1,29 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "virtual-cluster/service/cluster" + + "github.com/spf13/cobra" +) + +// cluster/initCmd represents the cluster/init command +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize your cluster", + Long: `Initialize your cluster.Flag or arguments not required`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) > 0 { + err = errors.New("Unnecessary arguments found") + return + } + err = cluster.InitializeCluster() + return + }, +} + +func init() { +} diff --git a/cmd/cluster/shell.go b/cmd/cluster/shell.go new file mode 100644 index 0000000..292d02c --- /dev/null +++ b/cmd/cluster/shell.go @@ -0,0 +1,35 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "github.com/spf13/cobra" + "virtual-cluster/service/cluster" +) + +// cluster/shellCmd represents the shell command +var shellCmd = &cobra.Command{ + Use: "shell", + Short: "Connect to shell of specific node.", + Long: `Connect to shell of specific node.`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if value, flagErr := cmd.Flags().GetString("name"); flagErr != nil { + err = flagErr + return + } else { + if len(args) > 0 { + err = errors.New("Unnecessary arguments found") + return + } + err = cluster.ConnectToNodeShell(value) + } + return + }, +} + +func init() { + shellCmd.Flags().StringVarP(&nodename, "name", "n", "", "Name of node you want to connect") + addCmd.MarkFlagRequired("name") +} diff --git a/cmd/cluster/terminate.go b/cmd/cluster/terminate.go new file mode 100644 index 0000000..37be0e5 --- /dev/null +++ b/cmd/cluster/terminate.go @@ -0,0 +1,29 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "github.com/spf13/cobra" + "virtual-cluster/service/cluster" +) + +// cluster/terminateCmd represents the cluster/terminate command +var terminateCmd = &cobra.Command{ + Use: "terminate", + Short: "Terminate specific node from cluster", + Long: `Terminate specific node from cluster.Flag or arguments not required and ignored`, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) > 0 { + err = errors.New("Unnecessary arguments found") + return + } + err = cluster.TerminateCluster() + return + }, +} + +func init() { + +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e5ab2a9 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,46 @@ +/* +Copyright Β© 2023 NAME HERE +*/ +package cmd + +import ( + "os" + cmd "virtual-cluster/cmd/cluster" + "virtual-cluster/utility" + + "github.com/spf13/cobra" +) + +var ( + cfgFile string + RootCmd = &cobra.Command{ + Use: "vc", + Short: "Making a virtual Kubernetes Cluster", + Long: utility.InfoMessageString(`Making a virtual Kubernetes Cluster. +It build cluster based on multipass and K3S`), + Run: func(cmd *cobra.Command, args []string) { + os.Exit(0) + }, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.Help() + return nil + }, + } +) + +func init() { + RootCmd.AddCommand(cmd.ClusterCmd) + /** + Check required directory exist + */ + for _, v := range []string{utility.NODE_CONFIG_PATH, utility.SCRIPTS_PATH} { + if x := utility.CheckFileOrDirectoryExist(v); !x { + utility.CriticalMessage("Required directory not found : ", v) + os.Exit(1) + } + } +} + +func Execute() { + RootCmd.Execute() +} diff --git a/decorator/__init__.py b/decorator/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/decorator/instanceExistChecker.py b/decorator/instanceExistChecker.py deleted file mode 100644 index 94c60ef..0000000 --- a/decorator/instanceExistChecker.py +++ /dev/null @@ -1,16 +0,0 @@ -from utils.utility import Utility -from exception import exceptions - - -class InstanceExistChecker(Utility): - - def __init__(self) -> None: - super().__init__() - - def __call__(self, fn): - def wrapper(instance, name, *args, **kwargs): - if self.checkInstanceNameNotInUse(name): - raise exceptions.NodeNotFound(name) - result = fn(instance, name, node_name=name) - return result - return wrapper diff --git a/decorator/instanceNameAlreadyTakenChecker.py b/decorator/instanceNameAlreadyTakenChecker.py deleted file mode 100644 index 5298fca..0000000 --- a/decorator/instanceNameAlreadyTakenChecker.py +++ /dev/null @@ -1,18 +0,0 @@ -from utils.utility import Utility -from exception import exceptions - - -class InstanceNameAlreadyTakenChecker(Utility): - def __init__(self) -> None: - super().__init__() - - def __call__(self, fn): - def wrapper(instance, name, *args, **kwargs): - # Check instance name in use - if not self.checkInstanceNameNotInUse(name): - raise exceptions.InvalidNodeGenerationDetected( - self.getCriticalMessage( - f"Name with '{name}' already in use! Ignore generating worker-node config '{name}'")) - result = fn(instance, name, args, node_name=name) - return result - return wrapper diff --git a/exception/__init__.py b/exception/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exception/exceptions.py b/exception/exceptions.py deleted file mode 100644 index 224ae82..0000000 --- a/exception/exceptions.py +++ /dev/null @@ -1,66 +0,0 @@ -class DirectoryNotFound(Exception): - def __init__(self, directoryName): - super().__init__(f"Directory '{directoryName}' not found") - - -class KubectlMayNotInstalled(Exception): - def __init__(self): - super().__init__(f"'~/.kube' directory not found. You need to install kubectl fisrt.") - - -class IllegalControlException(Exception): - def __init__(self, illegalcontent=None): - super().__init__(f"Illegal control detected : {illegalcontent}") - - -class MasterNodeConfigNotFound(Exception): - def __init__(self): - super().__init__("Master node config should define with key name 'master-node' in nodes/master/config.json") - - -class NoneTypeValueDetected(Exception): - def __init__(self): - super().__init__("None type value expected!") - - -class WrongArgumentGiven(Exception): - def __init__(self,msg): - super().__init__(f"Wrong argument value given : {msg}") - - -class ImproperMasterNodeGenerated(Exception): - def __init__(self): - statement = """ - Something went wrong while generating Master Node! - - Please check computer's resource and network! - - 1. Please delete master node instance using 'python3 cluster.py -c terminate' - 2. Restart Multipass - """ - super().__init__(statement) - -class MasterNodeNotFound(Exception): - def __init__(self): - super().__init__("Master node not generated! Please initiate cluster before add worker node") - -class InvalidNodeGenerationDetected(Exception): - def __init__(self,msg): - super().__init__(msg) -class RequiredCommandLineOptionLost(Exception): - def __init__(self, name): - super().__init__(f"Required option '{name}' lost") - -class NodeNotFound(Exception): - def __init__(self,name): - super().__init__(f"Node not found : {name}") - -class InvalidConfigType(Exception): - def __init__(self, tp): - super().__init__( - f"Invalid config type detected. Should be type '{tp}'") - -class InvalidNodeName(Exception): - def __init__(self,name): - super().__init__( - f"Invalid node name '{name}'. Should satisfy regular expression : ^[A-Za-z0-9][A-Za-z0-9-]*$" - ) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..069f907 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module virtual-cluster + +go 1.20 + +require ( + github.com/fatih/color v1.15.0 + github.com/joho/godotenv v1.5.1 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..175f40a --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/img/.DS_Store b/img/.DS_Store index dda86b0..a6a4fe5 100644 Binary files a/img/.DS_Store and b/img/.DS_Store differ diff --git a/img/2.png b/img/2.png index f29e175..a62b1b1 100644 Binary files a/img/2.png and b/img/2.png differ diff --git a/img/3.png b/img/3.png new file mode 100644 index 0000000..bab8e03 Binary files /dev/null and b/img/3.png differ diff --git a/license b/license index 09a9d3a..e69de29 100644 --- a/license +++ b/license @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Junho Yoon(a.k.a Hoplin) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/main.go b/main.go new file mode 100644 index 0000000..fd6fc07 --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +/* +Copyright Β© 2023 Hoplin +*/ +package main + +import ( + "github.com/joho/godotenv" + "os" + "virtual-cluster/utility" +) +import "virtual-cluster/cmd" + +func main() { + /** + Check requirement satisfied + */ + requirement_err := utility.CheckRequirementsInstalled() + if requirement_err != nil { + utility.CriticalMessage(requirement_err) + os.Exit(1) + } + /** + Load dotenv + */ + envErr := godotenv.Load() + + if envErr != nil { + utility.CriticalMessage("Application crushed :Fail to load .env file") + os.Exit(1) + } + cmd.Execute() +} diff --git a/nodes/.DS_Store b/nodes/.DS_Store deleted file mode 100644 index 3f5af0a..0000000 Binary files a/nodes/.DS_Store and /dev/null differ diff --git a/nodes/master/config.json b/nodes/master/config.json index 83321c0..314117f 100644 --- a/nodes/master/config.json +++ b/nodes/master/config.json @@ -1,9 +1,9 @@ { - "master-node": { - "cpu": "2", - "memory": "2048M", - "disk": "20G", - "ip": "", - "token": "" - } + "master-node": { + "cpu": "2", + "memory": "2048M", + "disk": "20G", + "ip": "192.168.64.191", + "token": "K106eb01c15bb146bd09e7c22dcbcc9971b57bdb139b050956762e123cdabeb1edb::server:ae75524b09fa49214a0d23a444e6297b" + } } \ No newline at end of file diff --git a/nodes/worker/config.json b/nodes/worker/config.json index 833e1bf..45ba5fc 100644 --- a/nodes/worker/config.json +++ b/nodes/worker/config.json @@ -1,8 +1,14 @@ { - "worker-node-1": { - "cpu": "1", - "memory": "2048M", - "disk": "20G", - "ip": "" - } + "worker-node-1": { + "cpu": "1", + "memory": "2048M", + "disk": "20G", + "ip": "192.168.64.193" + }, + "worker-node-2": { + "cpu": "1", + "memory": "2048M", + "disk": "20G", + "ip": "192.168.64.192" + } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e34af78..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -colorama -python-dotenv \ No newline at end of file diff --git a/scripts/nodeInit.sh b/scripts/nodeInit.sh index a658ca7..c2994fc 100644 --- a/scripts/nodeInit.sh +++ b/scripts/nodeInit.sh @@ -12,7 +12,6 @@ k3s_version=$8 common_path=$(dirname $(realpath $0))/common if [ $node_type = "master" ]; then - echo "Here" multipass launch --name $node_name --cpus $node_cpu --memory $node_memory --disk $node_disk multipass exec $node_name -- /bin/bash -c "curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL='$k3s_version' K3S_KUBECONFIG_MODE='644' INSTALL_K3S_EXEC='--disable traefik' sh -" multipass transfer $common_path/master-node-util.sh $node_name:master-node-util.sh @@ -23,6 +22,7 @@ else multipass exec $node_name -- /bin/bash -c "curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL='$k3s_version' K3S_TOKEN='$master_token' K3S_URL=https://$master_ip:6443 sh -" fi + multipass transfer $common_path/util.sh $node_name:util.sh multipass exec $node_name -- /bin/bash -c "chmod u+rwx ~/util.sh" multipass exec $node_name -- /bin/bash -c "~/util.sh" \ No newline at end of file diff --git a/service/cluster/add.go b/service/cluster/add.go new file mode 100644 index 0000000..82a532c --- /dev/null +++ b/service/cluster/add.go @@ -0,0 +1,49 @@ +package cluster + +import ( + "errors" + "virtual-cluster/utility" +) + +func AddNode(name string) error { + // Check name is valid convention + if nameValidate := utility.NodeNameValidater(name); !nameValidate { + return errors.New("Node naming convention violated : " + name) + } + + // Check if multipass instance with same name is running + // If invalid args given, return error + if validate := utility.CheckNodeNameExist(name); validate { + return errors.New(utility.CriticalMessageString("Node name with '", name, "' already running")) + } + + masterConfig, workerConfig, err := utility.GetMasterWorkerConfig() + if err != nil { + return err + } + target, checkConfigExist := workerConfig[name] + // Check config exist + if !checkConfigExist { + return errors.New(utility.CriticalMessageString("'", name, "' set not exist in worker node config file")) + } + + utility.InfoMessage("βœ… Complete to load node's information - ", name) + // If master node's token and ip is not an empty string + master := masterConfig[utility.MASTER_NODE_KEY] + if master.Ip == "" || master.Token == "" { + return errors.New(utility.CriticalMessageString("Master node's token or ip is empty string! Integrity broken!")) + } + + // Run add node + if err := target.Add(name, master.Ip, master.Token); err != nil { + utility.CriticalMessage("πŸ˜“ Failed to build worker node : ", name, "(Reason : ", err.Error(), ")") + return err + } + utility.InfoMessage("✨ Complete to build worker node : ", name) + + // Sync config file + if err := utility.SyncConfigFile(workerConfig, utility.WORKER_CONFIG); err != nil { + utility.CriticalMessage("Fail to synchronize worker node config - ", err.Error()) + } + return nil +} diff --git a/service/cluster/delete.go b/service/cluster/delete.go new file mode 100644 index 0000000..7d69533 --- /dev/null +++ b/service/cluster/delete.go @@ -0,0 +1,44 @@ +package cluster + +import ( + "errors" + "virtual-cluster/utility" +) + +func RemoveNode(name string) error { + if name == utility.MASTER_NODE_KEY { + return errors.New(utility.CriticalMessageString("Can't remove master node by alone")) + } + // Check name is valid convention + if nameValidate := utility.NodeNameValidater(name); !nameValidate { + return errors.New("Node naming convention violated : " + name) + } + + // Find if node is running + if validate := utility.CheckNodeNameExist(name); !validate { + return errors.New(utility.CriticalMessageString("Node name with '", name, "' not found")) + } + + // Get worker config and check if config set exist + workerConfig, err := utility.GetWorkerConfig() + if err != nil { + return err + } + target, checkConfigExist := workerConfig[name] + if !checkConfigExist { + return errors.New(utility.CriticalMessageString("'", name, "' set not exist in worker node config file")) + } + + utility.InfoMessage("βœ… Complete to validate node's information - ", name) + if err := target.Remove(name); err != nil { + utility.CriticalMessage("πŸ˜“ Failed to remove worker node : ", name, "(Reason : ", err.Error(), ")") + return err + } + utility.InfoMessage("✨ Complete to remove worker node : ", name) + + // Sync config file + if err := utility.SyncConfigFile(workerConfig, utility.WORKER_CONFIG); err != nil { + utility.CriticalMessage("Fail to synchronize worker node config - ", err.Error()) + } + return nil +} diff --git a/service/cluster/init.go b/service/cluster/init.go new file mode 100644 index 0000000..379a0c1 --- /dev/null +++ b/service/cluster/init.go @@ -0,0 +1,131 @@ +package cluster + +import ( + "errors" + "fmt" + "os" + "sync" + "time" + "virtual-cluster/utility" +) + +func getNodeIp() {} + +func InitializeCluster() error { + // Clear console + utility.ClearConsole() + + // Get master node, worker node config + masterConfig, workerConfig, err := utility.GetMasterWorkerConfig() + if err != nil { + return err + } + // Master node key exist + if _, ok := masterConfig[utility.MASTER_NODE_KEY]; !ok { + return errors.New("Master node config not found. Key should be " + utility.MASTER_NODE_KEY) + } + utility.InfoMessage("βœ… Complete to load master/worker nodes config!") + + // Master Node + utility.InfoMessage("πŸš€ Start initializing master node...") + masterNode := masterConfig[utility.MASTER_NODE_KEY] + if masterNodeErr := masterNode.Init(utility.MASTER_NODE_KEY); masterNodeErr != nil { + return masterNodeErr + } + /** + Sync token and ip of master node. + + If one of these gets failed, Unable to generate worker node + */ + masterIp, ipSyncErr := masterNode.SyncIP(utility.MASTER_NODE_KEY) + if ipSyncErr != nil { + return ipSyncErr + } + masterToken, tokenSyncErr := masterNode.SyncToken(utility.MASTER_NODE_KEY) + if tokenSyncErr != nil { + return tokenSyncErr + } + fmt.Println() + utility.SpecialMessage("[[ Master node config values ]]") + utility.SpecialMessage("πŸ“ Master Node IP : ", masterIp) + utility.SpecialMessage("πŸ“ Master Node Token : ", masterToken) + utility.InfoMessage("✨ Complete to build Master node") + fmt.Println() + // Go routine + wg := sync.WaitGroup{} + nodeInitResCh := make(chan *utility.NodeInitResult) + terminate := make(chan bool) + // Add wait grouo -> '+1' about listener + wg.Add(len(workerConfig) + 1) + + go WorkerNodeListener(len(workerConfig), &wg, nodeInitResCh, terminate) + for k, v := range workerConfig { + go v.Init(k, masterIp, masterToken, &wg, nodeInitResCh) + } + wg.Wait() + + fmt.Println() + utility.InfoMessage("🏷️ Generating kubeconfig file as - ", utility.KUBE_CONFIG) + /** + If directory not exist, make directory + */ + if exist := utility.CheckFileOrDirectoryExist(utility.KUBE_CONFIG_DIR); !exist { + // Mkdir의 λ‘λ²ˆμ§Έ λ§€κ°œλ³€μˆ˜μ—λŠ” λ””λ ‰ν† λ¦¬μ˜ κΆŒν•œμ΄ λ“€μ–΄κ°€κ²Œ λœλ‹€. + _ = os.Mkdir(utility.KUBE_CONFIG_DIR, 0755) + } + if exist := utility.CheckFileOrDirectoryExist(utility.KUBE_CONFIG); exist { + _ = os.Rename(utility.KUBE_CONFIG, utility.KUBE_CONFIG_DIR+"/config_cp") + utility.SpecialMessage("🏷️ kubeconfig file exist before will be save as - ", utility.KUBE_CONFIG_DIR+"/config_cp") + } + if cmd := utility.GetCommandWithoutShown(utility.SCRIPTS_PATH+"/getKubeConfig.sh", utility.MASTER_NODE_KEY, masterIp, utility.KUBE_CONFIG).Run(); cmd != nil { + utility.CriticalMessage("πŸ˜“ Fail to generate kubeconfig file. Please generate local kubeconfig manually. - ", cmd.Error()) + } + utility.InfoMessage("🏷️ kubeconfig file generated") + + /** + Sync config file + */ + if err := utility.SyncConfigFile(masterConfig, utility.MASTER_CONFIG); err != nil { + utility.CriticalMessage("πŸ˜“ Fail to synchronize master node config - ", err.Error()) + } + if err := utility.SyncConfigFile(workerConfig, utility.WORKER_CONFIG); err != nil { + utility.CriticalMessage("πŸ˜“ Fail to synchronize worker node config - ", err.Error()) + } + utility.SpecialMessage("πŸ“‘ Cluster ready!") + return nil +} + +func WorkerNodeListener(nodecount int, wg *sync.WaitGroup, nodeInitResCh chan *utility.NodeInitResult, terminate chan bool) { + time.Tick(10) + var counter int + var successNodeCounter int + for { + select { + case res := <-nodeInitResCh: + counter += 1 + if res.Success { + successNodeCounter += 1 + utility.InfoMessage("✨ Complete to build worker node : ", res.Nodename) + } else { + utility.CriticalMessage("πŸ˜“ Failed to build worker node : ", res.Nodename, "(Reason : ", res.Message, ")") + } + // If all of the node initialized, make an event to channel + if counter == nodecount { + /** + Channel은 GoRoutine κ°„μ˜ 톡신을 μœ„ν•œ μˆ˜λ‹¨μ΄λΌλŠ” λ³Έμ§ˆμ„ μžŠμ§€λ§μž + https://brownbears.tistory.com/315 + */ + go func() { + terminate <- true + }() + + } + case <-terminate: + fmt.Println() + utility.InfoMessage("⭕️ Success - ", successNodeCounter, "/", nodecount) + utility.CriticalMessage("❌ Failed - ", nodecount-successNodeCounter, "/", nodecount) + wg.Done() + return + } + } +} diff --git a/service/cluster/shell.go b/service/cluster/shell.go new file mode 100644 index 0000000..32219a3 --- /dev/null +++ b/service/cluster/shell.go @@ -0,0 +1,27 @@ +package cluster + +import ( + "errors" + "virtual-cluster/utility" +) + +func ConnectToNodeShell(name string) (err error) { + // Check if it's proper name + if validate := utility.NodeNameValidater(name); !validate { + err = errors.New("Node naming convention violated : " + name) + return + } + + if validate := utility.CheckNodeNameExist(name); !validate { + err = errors.New(utility.CriticalMessageString("Node name not found : ", name)) + return + } + + if _, _, err = utility.GetMasterWorkerConfig(); err != nil { + return errors.New(utility.CriticalMessageString("'", name, "' is not a kubernetes cluster instance")) + } + + cmd := utility.GetCommand(utility.SCRIPTS_PATH+"/connectShell.sh", name) + _ = cmd.Run() + return nil +} diff --git a/service/cluster/terminate.go b/service/cluster/terminate.go new file mode 100644 index 0000000..9a67837 --- /dev/null +++ b/service/cluster/terminate.go @@ -0,0 +1,75 @@ +package cluster + +import ( + "sync" + "virtual-cluster/utility" +) + +func TerminateCluster() (err error) { + masterConfig, workerConfig, err := utility.GetMasterWorkerConfig() + if err != nil { + return err + } + totalTask := len(masterConfig) + len(workerConfig) + endChannel := make(chan string) + terminateChannel := make(chan any) + + wg := sync.WaitGroup{} + wg.Add(totalTask + 1) + + go TerminateListener(totalTask, &wg, endChannel, terminateChannel) + + // Master node + for k, v := range masterConfig { + if ck := utility.CheckNodeNameExist(k); ck { + go v.Terminate(k, endChannel) + } else { + // Do not create go routine if node not exist + utility.CriticalMessage("🫒 Node name with '" + k + "' is not running!") + wg.Done() + } + } + + // Worker node + for k, v := range workerConfig { + if ck := utility.CheckNodeNameExist(k); ck { + go v.Terminate(k, endChannel) + } else { + // Do not create go routine if node not exist + utility.CriticalMessage("🫒 Node name with '" + k + "' is not running!") + endChannel <- k + } + } + wg.Wait() + // Purge Multipass Instance + utility.PurgeMultipassInstance() + // Sync config file + utility.InfoMessage("✨ Multipass instances purged!") + if err := utility.SyncConfigFile(masterConfig, utility.MASTER_CONFIG); err != nil { + utility.CriticalMessage("Fail to synchronize master node config - ", err.Error()) + } + if err := utility.SyncConfigFile(workerConfig, utility.WORKER_CONFIG); err != nil { + utility.CriticalMessage("Fail to synchronize worker node config - ", err.Error()) + } + return nil +} + +func TerminateListener(taskCount int, wg *sync.WaitGroup, end chan string, terminate chan any) { + var counter int + for { + select { + case <-end: + counter += 1 + wg.Done() + if counter == taskCount { + go func() { + terminate <- true + }() + } + case <-terminate: + utility.InfoMessage("✨ All of the node terminated!") + wg.Done() + return + } + } +} diff --git a/utility/command.go b/utility/command.go new file mode 100644 index 0000000..32dbb68 --- /dev/null +++ b/utility/command.go @@ -0,0 +1,29 @@ +package utility + +import ( + "bytes" + "os" + "os/exec" +) + +func GetCommand(commandString ...string) (cmd *exec.Cmd) { + cmd = exec.Command(GetShellRuntime(), commandString...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return +} + +func GetCommandWithoutShown(commandString ...string) (cmd *exec.Cmd) { + cmd = exec.Command(GetShellRuntime(), commandString...) + var t bytes.Buffer + cmd.Stdout = &t + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return +} + +func GetPureCommand(commandString ...string) (cmd *exec.Cmd) { + cmd = exec.Command(GetShellRuntime(), commandString...) + return +} diff --git a/utility/config.go b/utility/config.go new file mode 100644 index 0000000..7d7209f --- /dev/null +++ b/utility/config.go @@ -0,0 +1,53 @@ +package utility + +import ( + "errors" + "os" + "path/filepath" +) + +var ( + DIR, err = filepath.Abs(filepath.Dir(os.Args[0])) + KUBE_CONFIG = os.Getenv("HOME") + "/.kube/config" + KUBE_CONFIG_DIR = os.Getenv("HOME") + "/.kube" + MASTER_NODE_KEY = "master-node" + MASTER_CONFIG = "./nodes/master/config.json" + WORKER_CONFIG = "./nodes/worker/config.json" + SCRIPTS_PATH = "./scripts" + NODE_CONFIG_PATH = "./nodes" + JSON_INDENT = "\t" +) + +func GetShellRuntime() string { + return os.Getenv("SHELL_TYPE") +} + +func GetK3SVersion() (version string, err error) { + version = os.Getenv("K3S_VERSION") + // if version is invalid + if version == "" { + err = errors.New("K3S version not found. Please set K3S Version before initialize cluster") + } + return +} + +func GetMasterWorkerConfig() (master MasterJson, worker WorkerJson, err error) { + // Get master node, worker node config + master, masterErr := GetMasterConfig() + worker, workerErr := GetWorkerConfig() + + getMsg := func(tp string) string { + return CriticalMessageString("Fail to parse " + tp + "file (Invalid format)") + } + // Check error while get config + if masterErr != nil { + err = errors.New(getMsg("master node")) + return + } else { + if workerErr != nil { + err = errors.New(CriticalMessageString(getMsg("worker node"))) + return + } + } + return +} diff --git a/utility/node.go b/utility/node.go new file mode 100644 index 0000000..5ebeddf --- /dev/null +++ b/utility/node.go @@ -0,0 +1,219 @@ +package utility + +import ( + "encoding/json" + "errors" + "io" + "os" + "strings" + "sync" +) + +const ( + MASTER_NODE = "master" + WORKER_NODE = "worker" +) + +type MasterConfig struct { + Cpu string `json:"cpu"` + Memory string `json:"memory"` + Disk string `json:"disk"` + Ip string `json:"ip"` + Token string `json:"token"` +} + +type WorkerConfig struct { + Cpu string `json:"cpu"` + Memory string `json:"memory"` + Disk string `json:"disk"` + Ip string `json:"ip"` +} + +type WorkerJson map[string]*WorkerConfig + +type MasterJson map[string]*MasterConfig + +type ClusterNode interface { + SyncIP(name string) (string, error) + Terminate(name string, end chan string) + CheckResourceAndReturnDefaultValue() +} + +func (m *MasterConfig) Init(name string) error { + if nameValidate := NodeNameValidater(name); !nameValidate { + return errors.New("Node naming convention violated : " + name) + } + // Instance Check + if exit := CheckNodeNameExist(name); exit { + return errors.New("Node name '" + name + "' already running") + } + // Resource check and change to default value + m.CheckResourceAndReturnDefaultValue() + if version, err := GetK3SVersion(); err != nil { + return err + } else { + cmd := GetCommandWithoutShown(SCRIPTS_PATH+"/nodeInit.sh", name, m.Cpu, m.Memory, m.Disk, MASTER_NODE, version) + + if initErr := cmd.Run(); initErr != nil { + return initErr + } + } + return nil +} + +func (m *MasterConfig) Terminate(name string, end chan string) { + _ = GetCommandWithoutShown(SCRIPTS_PATH+"/terminateCluster.sh", name).Run() + // Clear ip, token info + m.Ip = "" + m.Token = "" + InfoMessage("πŸ‘‹ Complete to terminate node - ", name) + end <- name +} + +func (m *MasterConfig) SyncIP(name string) (string, error) { + if result, err := GetPureCommand(SCRIPTS_PATH+"/getNodeIP.sh", name).Output(); err != nil { + return "", err + } else { + m.Ip = strings.Trim(string(result), "\n") + return m.Ip, err + } +} + +func (m *MasterConfig) SyncToken(name string) (string, error) { + if result, err := GetPureCommand(SCRIPTS_PATH+"/getMasterToken.sh", name).Output(); err != nil { + return "", err + } else { + m.Token = strings.Trim(string(result), "\n") + return m.Token, err + } +} + +func (m *MasterConfig) CheckResourceAndReturnDefaultValue() { + if m.Cpu == "" { + m.Cpu = os.Getenv("MASTER_DEFAULT_CPU") + } + if m.Disk == "" { + m.Cpu = os.Getenv("MASTER_DEFAULT_MEMORY") + } + if m.Disk == "" { + m.Disk = os.Getenv("MASTER_DEFAULT_STORAGE") + } +} + +func (w *WorkerConfig) Init(name string, masterIp string, masterToken string, wg *sync.WaitGroup, res chan *NodeInitResult) { + var result *NodeInitResult + InfoMessage("πŸš€ Start initializing worker node : ", name) + if nameValidate := NodeNameValidater(name); !nameValidate { + result = &NodeInitResult{false, name, "Node naming convention violated : " + name} + } else { + if version, err := GetK3SVersion(); err != nil { + result = &NodeInitResult{false, name, err.Error()} + } else { + // Check resource config and change to default value + w.CheckResourceAndReturnDefaultValue() + + cmd := GetCommandWithoutShown(SCRIPTS_PATH+"/nodeInit.sh", name, w.Cpu, w.Memory, w.Disk, WORKER_NODE, masterToken, masterIp, version) + if initErr := cmd.Run(); initErr != nil { + result = &NodeInitResult{false, name, initErr.Error()} + } else { + _, _ = w.SyncIP(name) + result = &NodeInitResult{true, name, ""} + } + } + } + _, _ = w.SyncIP(name) + wg.Done() + res <- result + return +} + +func (w *WorkerConfig) Terminate(name string, end chan string) { + _ = GetCommandWithoutShown(SCRIPTS_PATH+"/terminateCluster.sh", name).Run() + // Clear IP + w.Ip = "" + end <- name +} + +func (w *WorkerConfig) SyncIP(name string) (string, error) { + if result, err := GetPureCommand(SCRIPTS_PATH+"/getNodeIP.sh", name).Output(); err != nil { + return "", err + } else { + w.Ip = strings.Trim(string(result), "\n") + return w.Ip, err + } +} + +func (w *WorkerConfig) Remove(name string) (err error) { + InfoMessage("πŸ‘‹ Removing node - ", name) + _ = GetCommandWithoutShown(SCRIPTS_PATH+"/deleteNode.sh", MASTER_NODE_KEY, name).Run() + _ = GetCommandWithoutShown(SCRIPTS_PATH+"/terminateCluster.sh", name).Run() + _ = GetCommandWithoutShown(SCRIPTS_PATH + "/purgeInstance.sh").Run() + w.Ip = "" + return +} + +func (w *WorkerConfig) Add(name string, masterIp string, masterToken string) (err error) { + InfoMessage("πŸš€ Initializing node - ", name) + w.CheckResourceAndReturnDefaultValue() + err = GetCommandWithoutShown(SCRIPTS_PATH+"/nodeInit.sh", name, w.Cpu, w.Memory, w.Disk, WORKER_NODE, masterToken, masterIp).Run() + _, _ = w.SyncIP(name) + return +} + +func (m *WorkerConfig) CheckResourceAndReturnDefaultValue() { + if m.Cpu == "" { + m.Cpu = os.Getenv("WORKER_DEFAULT_CPU") + } + if m.Memory == "" { + m.Memory = os.Getenv("WORKER_DEFAULT_MEMORY") + } + if m.Disk == "" { + m.Disk = os.Getenv("WORKER_DEFAULT_STORAGE") + } +} + +type NodeInitResult struct { + Success bool + Nodename string + Message string +} + +func Readfile(filePath string) (file []byte, err error) { + var fInstance *os.File + fInstance, err = os.Open(filePath) + if err != nil { + return + } + file, err = io.ReadAll(fInstance) + return +} + +func GetMasterConfig() (config MasterJson, err error) { + var jsonByte []byte + if jsonByte, err = Readfile(MASTER_CONFIG); err != nil { + return + } + err = json.Unmarshal(jsonByte, &config) + return +} + +func GetWorkerConfig() (config WorkerJson, err error) { + var jsonByte []byte + if jsonByte, err = Readfile(WORKER_CONFIG); err != nil { + return + } + err = json.Unmarshal(jsonByte, &config) + return +} + +func SyncConfigFile(config any, destination string) error { + var err error + var file []byte + file, err = json.MarshalIndent(config, "", JSON_INDENT) + err = os.WriteFile(destination, file, 0755) + return err +} + +func PurgeMultipassInstance() { + _ = GetCommandWithoutShown(SCRIPTS_PATH + "/purgeInstance.sh").Run() +} diff --git a/utility/printer.go b/utility/printer.go new file mode 100644 index 0000000..57fd748 --- /dev/null +++ b/utility/printer.go @@ -0,0 +1,43 @@ +package utility + +import ( + "fmt" + "github.com/fatih/color" +) + +func ClearConsole() { + fmt.Printf("\x1b[2J") +} + +func CriticalMessage(msg ...interface{}) { + + color.Red(fmt.Sprintln(msg...)) +} + +func CriticalMessageString(msg ...interface{}) string { + return color.RedString(fmt.Sprintln(msg...)) +} + +func WarnMessage(msg ...interface{}) { + color.Yellow(fmt.Sprintln(msg...)) +} + +func WarnMessageString(msg ...interface{}) string { + return color.YellowString(fmt.Sprintln(msg...)) +} + +func InfoMessage(msg ...interface{}) { + color.Green(fmt.Sprintln(msg...)) +} + +func InfoMessageString(msg ...interface{}) string { + return color.GreenString(fmt.Sprintln(msg...)) +} + +func SpecialMessage(msg ...interface{}) { + color.Magenta(fmt.Sprintln(msg...)) +} + +func SpecialMessageString(msg ...interface{}) string { + return color.MagentaString(fmt.Sprintln(msg...)) +} diff --git a/utility/validator.go b/utility/validator.go new file mode 100644 index 0000000..a4d3ebf --- /dev/null +++ b/utility/validator.go @@ -0,0 +1,84 @@ +package utility + +import ( + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) + +var ( + NodeNamePattern *regexp.Regexp +) + +func init() { + // Node name pattern + compile, err := regexp.Compile("^[A-Za-z0-9][A-Za-z0-9-]*$") + NodeNamePattern = compile + + if err != nil { + panic(fmt.Sprintln("Unable to compile regular expression statement : ", err)) + } +} + +func NodeNameValidater(name string) bool { + return NodeNamePattern.MatchString(name) +} + +func CheckRequirementsInstalled() error { + msgBuilder := func(msg string) string { + return fmt.Sprintf("'%s' may not installed or not enrolled in PATH", msg) + } + /* + Check requirements on init + - Multipass + - Kubectl + - Helm + */ + requirements := make(map[string]*exec.Cmd) + requirements["multipass"] = exec.Command("multipass", "--help") + requirements["kubectl"] = exec.Command("kubectl", "--help") + requirements["helm"] = exec.Command("helm", "--help") + for k, cmd := range requirements { + if err := cmd.Run(); err != nil { + return errors.New(msgBuilder(k)) + } + } + return nil +} + +func CheckNodeNameExist(name string) bool { + command := exec.Command("multipass", "list") + output, _ := command.CombinedOutput() + instanceList := strings.Split(string(output), "\n")[1:] + for _, v := range instanceList { + if filtered := strings.Split(v, " ")[0]; filtered == name { + return true + } + } + return false +} + +func CheckIsClusterInstance(name string) bool { + masters, _ := GetMasterConfig() + // Able to check key exist with second return value + _, masterCheck := masters[name] + workerCheck := CheckIsWorkerInstance(name) + return masterCheck || workerCheck +} + +func CheckIsWorkerInstance(name string) bool { + workers, _ := GetWorkerConfig() + _, workerCheck := workers[name] + return workerCheck +} + +func CheckFileOrDirectoryExist(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } else { + return true + } +} diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/utils/utility.py b/utils/utility.py deleted file mode 100644 index 06c8994..0000000 --- a/utils/utility.py +++ /dev/null @@ -1,47 +0,0 @@ -import json,subprocess,re -from colorama import Fore, Style - -class Utility(object): - def typeChecker(self, obj, tp): - return type(obj) == tp - - def readConfig(self, file) -> dict: - with open(file, 'r') as config: - content: dict = json.load(config) - return content - - def saveConfig(self, file, object): - with open(file, 'w') as config: - json.dump(object, config, indent=4) - - def validateNodeName(self,name: str): - return bool(re.match('^[A-Za-z0-9][A-Za-z0-9-]*$',name)) - - def getCriticalMessage(self,msg,watermark=True): - watermark = "Critical : " if watermark else "" - return Fore.RED + f"{watermark}{msg}" + Style.RESET_ALL - - def getNormalMessage(self,msg): - return Fore.GREEN + msg + Style.RESET_ALL - def getWarningMessage(self,msg): - return Fore.YELLOW + f"Warn : {msg}" + Style.RESET_ALL - - def getSpecialMessage(self,msg): - return Fore.MAGENTA + msg + Style.RESET_ALL - - def checkInstanceNameNotInUse(self,name) -> bool: - hit = True - ''' - Slice 0 index value : 0 index result of command is index of column - ''' - for i in map(lambda x: x.split(), - subprocess.run(["multipass", "list"], stdout=subprocess.PIPE) - .stdout.decode('utf-8') - .split("\n")[1:]): - try: - if i[0] == name: - hit = False - break - except: - continue - return hit \ No newline at end of file