From 0cb818404aae5b06383cd49fa0fd86f8cb888a87 Mon Sep 17 00:00:00 2001 From: Markus Pesch Date: Thu, 23 Feb 2023 14:26:07 +0100 Subject: [PATCH] Initial Commit --- .editorconfig | 12 +++++ .vscode/settings.json | 4 ++ README.md | 34 ++++++++++++ main.py | 117 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 .editorconfig create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100755 main.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a1027e5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f1dc2e8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f25ae1c --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# kcf - kubectl fetcher + +A small python script to merge a remote kubectl config with a local stored +configuration file. It supports cli flags. The following + +```bash +$ ./main.py -h +usage: main.py [-h] --host HOSTNAME [--port PORT] [--username USERNAME] [--password PASSWORD] [--identity-file IDENTITY_FILE] [--identity-passphrase IDENTITY_PASSPHRASE] + [--remote-config REMOTE_CONFIG] [--remove-cluster-name REMOTE_CLUSTER_NAME] [--remove-user-name REMOTE_USER_NAME] [--local-config LOCAL_CONFIG] + [--local-cluster-name LOCAL_CLUSTER_NAME] [--local-user-name LOCAL_USER_NAME] + +options: + -h, --help show this help message and exit + --host HOSTNAME SSH-Server + --port PORT SSH-Port + --username USERNAME Remote Unix User + --password PASSWORD Remote password + --identity-file IDENTITY_FILE + Path to private SSH-Key + --identity-passphrase IDENTITY_PASSPHRASE + Passphrase of the SSH-Key + --remote-config REMOTE_CONFIG + Remote kubectl config + --remove-cluster-name REMOTE_CLUSTER_NAME + Name of the cluster + --remove-user-name REMOTE_USER_NAME + Name of the user + --local-config LOCAL_CONFIG + Local kubectl config + --local-cluster-name LOCAL_CLUSTER_NAME + Name of the cluster + --local-user-name LOCAL_USER_NAME + Name of the user +``` diff --git a/main.py b/main.py new file mode 100755 index 0000000..ff7eae3 --- /dev/null +++ b/main.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import argparse +import paramiko +import os +import yaml +from yaml.loader import SafeLoader +from cryptography.hazmat.primitives import serialization as crypto_serialization +from cryptography.hazmat.primitives.asymmetric import ed25519, dsa, rsa, ec +from io import StringIO +from scp import SCPClient +from paramiko import RSAKey, Ed25519Key, ECDSAKey, DSSKey, PKey, SSHClient + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--host", dest="hostname", required=True, type=str, help="SSH-Server") + parser.add_argument("--port", dest="port", default=22, type=int, help="SSH-Port") + parser.add_argument("--username", dest="username", default=os.getlogin(), type=str, help="Remote Unix User") + parser.add_argument("--password", dest="password", default=None, type=str, help="Remote password") + parser.add_argument("--identity-file", dest="identity_file", default=None, type=str, help="Path to private SSH-Key") + parser.add_argument("--identity-passphrase", dest="identity_passphrase", default=None, type=str, help="Passphrase of the SSH-Key") + parser.add_argument("--remote-config", dest="remote_config", default="~/.kube/config", type=str, help="Remote kubectl config") + parser.add_argument("--remove-cluster-name", dest="remote_cluster_name", default="kubernetes", type=str, help="Name of the cluster") + parser.add_argument("--remove-user-name", dest="remote_user_name", default="kubernetes-admin", type=str, help="Name of the user") + parser.add_argument("--local-config", dest="local_config", default="~/.kube/config", type=str, help="Local kubectl config") + parser.add_argument("--local-cluster-name", dest="local_cluster_name", default="kubernetes", type=str, help="Name of the cluster") + parser.add_argument("--local-user-name", dest="local_user_name", default="kubernetes-admin", type=str, help="Name of the user") + args = parser.parse_args() + + ssh_client = create_ssh_client(args.hostname, args.port, args.username, args.identity_file, args.identity_passphrase) + scp_client = SCPClient(ssh_client.get_transport()) + + scp_client.get(args.remote_config, local_path="/tmp/admin.config") + + with open("/tmp/admin.config", 'r', encoding="UTF-8") as f: + remote_config = yaml.load(f, Loader=SafeLoader) + + with open(args.local_config, 'r', encoding="UTF-8") as f: + local_config = yaml.load(f, Loader=SafeLoader) + + + # Remove old objects + local_config["clusters"] = list(filter(lambda cluster: cluster["name"] != args.local_cluster_name, local_config["clusters"])) + local_config["users"] = list(filter(lambda user: user["name"] != args.local_user_name, local_config["users"])) + + # Add new objects + local_config["clusters"] = local_config["clusters"] + list(filter(lambda cluster: cluster["name"] == args.remote_cluster_name, remote_config["clusters"])) + local_config["users"] = local_config["users"] + list(filter(lambda user: user["name"] == args.remote_user_name, remote_config["users"])) + + # Rename object attributes + for cluster in list(filter(lambda cluster: cluster["name"] == args.remote_cluster_name, remote_config["clusters"])): + cluster["name"] = args.local_cluster_name + + for user in list(filter(lambda user: user["name"] == args.remote_user_name, remote_config["users"])): + user["name"] = args.local_user_name + + # Store new local_config + with open(args.local_config, 'w', encoding="UTF-8") as f: + yaml.dump(local_config, f) + +def create_ssh_client(hostname: str, port: str, username: str, identity_file: str, identity_passphrase: str) -> SSHClient: + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + ssh_private_key = PKey + + if identity_file != None and os.path.isfile(identity_file): + ssh_private_key_file_obj = open(file=identity_file, mode="r", encoding="UTF-8") + ssh_private_key = from_private_key(ssh_private_key_file_obj, identity_passphrase) + + ssh_client.connect(username=username, hostname = hostname, port = port, pkey=ssh_private_key, compress=True) + + return ssh_client + + +def from_private_key(file_obj, password=None) -> PKey: + private_key = None + file_bytes = bytes(file_obj.read(), "utf-8") + try: + key = crypto_serialization.load_ssh_private_key( + file_bytes, + password=password, + ) + file_obj.seek(0) + except ValueError: + key = crypto_serialization.load_pem_private_key( + file_bytes, + password=password, + ) + if password: + encryption_algorithm = crypto_serialization.BestAvailableEncryption( + password + ) + else: + encryption_algorithm = crypto_serialization.NoEncryption() + + file_obj = StringIO( + key.private_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PrivateFormat.OpenSSH, + encryption_algorithm, + ).decode("utf-8") + ) + if isinstance(key, rsa.RSAPrivateKey): + private_key = RSAKey.from_private_key(file_obj, password) + elif isinstance(key, ed25519.Ed25519PrivateKey): + private_key = Ed25519Key.from_private_key(file_obj, password) + elif isinstance(key, ec.EllipticCurvePrivateKey): + private_key = ECDSAKey.from_private_key(file_obj, password) + elif isinstance(key, dsa.DSAPrivateKey): + private_key = DSSKey.from_private_key(file_obj, password) + else: + raise TypeError + return private_key + +if __name__ == "__main__": + main()