From 8cbea2ed122b89ab1a7af4492deef313e76e1682 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Tue, 5 Jul 2022 20:52:56 +0300 Subject: [PATCH] feat: add support for downloading the correct version of kubeseal binary (#12) --- CHANGELOG.md | 4 +++ README.md | 2 ++ poetry.lock | 18 +++++----- pyproject.toml | 3 +- src/kubeseal_auto/cluster.py | 18 +++++++--- src/kubeseal_auto/host.py | 65 +++++++++++++++++++++++++++++++++++ src/kubeseal_auto/kubeseal.py | 14 +++++--- 7 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 src/kubeseal_auto/host.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9622e34..24669b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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). +## [0.4.0] - 2022-07-05 +### Added +- kubeseal-auto now downloads the same version of kubeseal binary as the version of sealed-secret controller (only in non-detached mode) + ## [0.3.1] - 2022-07-04 ### Fixed - TypeError: argument of type 'NoneType' is not iterable in _find_sealed_secrets_controller diff --git a/README.md b/README.md index d6d3d2a..79e80ca 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ pipx install kubeseal-auto ``` ## Usage +By default, the script will check the version of sealed-secret controller and download the corresponding kubeseal binary to ~/bin directory. To run the script in fully interactive mode: ```bash @@ -24,6 +25,7 @@ kubeseal-auto --fetch # Generate SealedSecret with local certificate kubeseal-auto --cert -kubeseal-cert.crt ``` +> Note: In the detached mode kubeseal-auto will not download the kubeseal binary and will look for in $PATH. To select kubeconfig context: ```bash diff --git a/poetry.lock b/poetry.lock index d9ef439..b7ede98 100644 --- a/poetry.lock +++ b/poetry.lock @@ -215,21 +215,21 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-auto [[package]] name = "requests" -version = "2.27.1" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" @@ -302,7 +302,7 @@ test = ["websockets"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "d30544ee23d4364482a0186065a3f43b5319b5e8c5ab6d728d1d3389fb8bdb1d" +content-hash = "01513ba70169224fa4c4b7c49591054ed9329ee80f4315de10388d970bd021de" [metadata.files] asttokens = [ @@ -435,8 +435,8 @@ questionary = [ {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, ] requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] requests-oauthlib = [ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, diff --git a/pyproject.toml b/pyproject.toml index 4094a7b..2ccaeea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "kubeseal-auto" -version = "0.3.1" +version = "0.4.0" description = "An interactive wrapper for kubeseal binary" authors = ["Vadim Gedz "] license = "MIT" @@ -17,6 +17,7 @@ questionary = "^1.10.0" icecream = "^2.1.2" PyYAML = "^6.0" colorama = "^0.4.4" +requests = "^2.28.1" [tool.poetry.dev-dependencies] diff --git a/src/kubeseal_auto/cluster.py b/src/kubeseal_auto/cluster.py index 7f8d98a..6c7c736 100644 --- a/src/kubeseal_auto/cluster.py +++ b/src/kubeseal_auto/cluster.py @@ -4,11 +4,14 @@ from icecream import ic from kubernetes import client, config +from kubeseal_auto.host import Host + class Cluster: def __init__(self, select_context: bool): self.context = self._set_context(select_context=select_context) config.load_kube_config(context=self.context) + self.host = Host() self.controller = self._find_sealed_secrets_controller() @staticmethod @@ -30,8 +33,7 @@ def get_all_namespaces() -> list: ic(ns_list) return ns_list - @staticmethod - def _find_sealed_secrets_controller() -> dict: + def _find_sealed_secrets_controller(self) -> dict: click.echo("===> Searching for SealedSecrets controller") expected_label = "app.kubernetes.io/instance" @@ -44,10 +46,15 @@ def _find_sealed_secrets_controller() -> dict: if deployment.metadata.labels[expected_label] == "sealed-secrets": name = deployment.metadata.labels[expected_label] namespace = deployment.metadata.namespace + version = deployment.metadata.labels["app.kubernetes.io/version"] click.echo( - f"===> Found the following controller: {Fore.CYAN}{namespace}/{name}" + f"===> Found the following controller: {Fore.CYAN}{namespace}/{name}:{version}" ) - return {"name": name, "namespace": namespace} + self.host.ensure_kubeseal_binary(version=version) + return {"name": name, "namespace": namespace, "version": version} + + click.echo("===> No controller found") + exit(1) click.echo("===> No controller found") exit(1) @@ -82,5 +89,8 @@ def get_controller_name(self): def get_controller_namespace(self): return self.controller["namespace"] + def get_controller_version(self): + return self.controller["version"].split("v")[-1] + def get_context(self): return self.context diff --git a/src/kubeseal_auto/host.py b/src/kubeseal_auto/host.py new file mode 100644 index 0000000..e370d21 --- /dev/null +++ b/src/kubeseal_auto/host.py @@ -0,0 +1,65 @@ +import os +import platform + +import click +import requests +from icecream import ic + + +class Host: + def __init__(self): + self.base_url = ( + "https://github.com/bitnami-labs/sealed-secrets/releases/download" + ) + self.bin_location = f"{os.path.expanduser('~')}/bin" + self.cpu_type = self._get_cpu_type() + self.system = self._get_system_type() + + @staticmethod + def _get_cpu_type(): + return platform.machine() + + @staticmethod + def _get_system_type(): + if platform.system() == "Darwin": + return "darwin" + elif platform.system() == "Linux": + return "linux" + else: + return "unsupported" + + def _download_kubeseal_binary(self, version: str): + click.echo("Downloading kubeseal binary") + + url = f"{self.base_url}/v{version}/kubeseal-{version}-{self.system}-{self.cpu_type}.tar.gz" + ic(url) + + if not os.path.exists(self.bin_location): + os.makedirs(self.bin_location) + + click.echo(f"Downloading {url}") + with requests.get( + f"{self.base_url}/v{version}/kubeseal-{version}-{self.system}-{self.cpu_type}.tar.gz" + ) as r: + with open( + f"/tmp/kubeseal-{version}-{self.system}-{self.cpu_type}.tar.gz", "wb" + ) as f: + f.write(r.content) + + os.system( + f"tar -xvf " + f"/tmp/kubeseal-{version}-{self.system}-{self.cpu_type}.tar.gz " + f"-C {self.bin_location} kubeseal" + ) + os.rename( + f"{self.bin_location}/kubeseal", f"{self.bin_location}/kubeseal-{version}" + ) + os.remove(f"/tmp/kubeseal-{version}-{self.system}-{self.cpu_type}.tar.gz") + + def ensure_kubeseal_binary(self, version: str): + version = version.split("v")[-1] + if not os.path.exists(f"{self.bin_location}/kubeseal-{version}"): + click.echo( + f"kubeseal binary not found at {self.bin_location}/kubeseal-{version}" + ) + self._download_kubeseal_binary(version) diff --git a/src/kubeseal_auto/kubeseal.py b/src/kubeseal_auto/kubeseal.py index 24e6bad..b999f72 100644 --- a/src/kubeseal_auto/kubeseal.py +++ b/src/kubeseal_auto/kubeseal.py @@ -17,16 +17,22 @@ class Kubeseal: def __init__(self, select_context: bool, certificate=None): self.detached_mode = False + self.binary = "kubeseal" + if certificate is not None: click.echo("===> Working in a detached mode") self.detached_mode = True self.certificate = certificate else: + home_dir = os.path.expanduser("~") self.cluster = Cluster(select_context=select_context) self.controller_name = self.cluster.get_controller_name() self.controller_namespace = self.cluster.get_controller_namespace() self.current_context_name = self.cluster.get_context() self.namespaces_list = self.cluster.get_all_namespaces() + self.binary = ( + f"{home_dir}/bin/kubeseal-{self.cluster.get_controller_version()}" + ) self.temp_file = NamedTemporaryFile() @@ -128,13 +134,13 @@ def seal(self, secret_name: str): click.echo("===> Sealing generated secret file") if self.detached_mode: command = ( - f"kubeseal --format=yaml " + f"{self.binary} --format=yaml " f"--cert={self.certificate} < {self.temp_file.name} " f"> {secret_name}.yaml" ) else: command = ( - f"kubeseal --format=yaml " + f"{self.binary} --format=yaml " f"--context={self.current_context_name} " f"--controller-namespace={self.controller_namespace} " f"--controller-name={self.controller_name} < {self.temp_file.name} " @@ -160,12 +166,12 @@ def merge(self, secret_name: str): click.echo(f"===> Updating {secret_name}") if self.detached_mode: command = ( - f"kubeseal --format=yaml --merge-into {secret_name} " + f"{self.binary} --format=yaml --merge-into {secret_name} " f"--cert={self.certificate} < {self.temp_file.name} " ) else: command = ( - f"kubeseal --format=yaml --merge-into {secret_name} " + f"{self.binary} --format=yaml --merge-into {secret_name} " f"--context={self.current_context_name} " f"--controller-namespace={self.controller_namespace} " f"--controller-name={self.controller_name} < {self.temp_file.name}"