From 1f79cbe29473f3919daada458bae8793813a8243 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 18 Sep 2024 15:43:42 -0500 Subject: [PATCH 01/55] remove kubectl from stop_scenarios --- src/warnet/control.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index cea7bc9a0..60dfc7153 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -28,7 +28,7 @@ wait_for_pod, write_file_to_container, ) -from .process import run_command, stream_command +from .process import run_command console = Console() @@ -83,8 +83,8 @@ def stop_scenario(scenario_name): """Stop a single scenario using Helm""" # Stop the pod immediately (faster than uninstalling) namespace = get_default_namespace() - cmd = f"kubectl --namespace {namespace} delete pod {scenario_name} --grace-period=0 --force" - if stream_command(cmd): + resp = delete_pod(scenario_name, namespace, grace_period=0, force=True) + if resp.status == "Success": console.print(f"[bold green]Successfully stopped scenario: {scenario_name}[/bold green]") else: console.print(f"[bold red]Failed to stop scenario: {scenario_name}[/bold red]") @@ -119,11 +119,6 @@ def uninstall_release(namespace, release_name): subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return f"Initiated uninstall for: {release_name} in namespace {namespace}" - def delete_pod(pod_name, namespace): - cmd = f"kubectl delete pod --ignore-not-found=true {pod_name} -n {namespace} --grace-period=0 --force" - subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" - with ThreadPoolExecutor(max_workers=10) as executor: futures = [] @@ -139,7 +134,16 @@ def delete_pod(pod_name, namespace): # Delete remaining pods pods = get_pods() for pod in pods.items: - futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) + futures.append( + executor.submit( + delete_pod, + pod.metadata.name, + pod.metadata.namespace, + grace_period=0, + force=True, + ignore_not_found=True, + ) + ) # Wait for all tasks to complete and print results for future in as_completed(futures): From 4204be5122b3562841056a379449a3e4ac86af0d Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 18 Sep 2024 15:44:39 -0500 Subject: [PATCH 02/55] remove kubectl from snapshot_bitcoin_datadir --- src/warnet/k8s.py | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 9c18d095d..5ea14b0cb 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -220,15 +220,13 @@ def snapshot_bitcoin_datadir( if resp.peek_stderr(): print(f"Error: {resp.read_stderr()}") resp.close() + local_file_path = Path(local_path) / f"{pod_name}_bitcoin_data.tar.gz" - copy_command = ( - f"kubectl cp {namespace}/{pod_name}:/tmp/bitcoin_data.tar.gz {local_file_path}" - ) - if not stream_command(copy_command): - raise Exception("Failed to copy tar file from pod to local machine") + temp_bitcoin_data_path = "/tmp/bitcoin_data.tar.gz" + copy_file_from_pod(namespace, pod_name, temp_bitcoin_data_path, local_file_path) print(f"Bitcoin data exported successfully to {local_file_path}") - cleanup_command = ["rm", "/tmp/bitcoin_data.tar.gz"] + cleanup_command = ["rm", temp_bitcoin_data_path] stream( sclient.connect_get_namespaced_pod_exec, pod_name, @@ -247,6 +245,36 @@ def snapshot_bitcoin_datadir( print(f"An error occurred: {str(e)}") +def copy_file_from_pod(namespace, pod_name, pod_path, local_path): + exec_command = ["cat", pod_path] + + v1 = client.CoreV1Api() + + # Note: We do not specify the container name here; if we pack multiple containers in a pod + # we will need to change this + resp = stream( + v1.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=exec_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + with open(local_path, "wb") as local_file: + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + local_file.write(resp.read_stdout().encode("utf-8")) + if resp.peek_stderr(): + print("Error:", resp.read_stderr()) + + resp.close() + + def wait_for_pod_ready(name, namespace, timeout=300): sclient = get_static_client() w = watch.Watch() From 7b191100e71fa4b1e163f2667d0da22f47e1ee19 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 18 Sep 2024 21:16:21 -0500 Subject: [PATCH 03/55] `auth`: revamp to not use kubectl Also, provide some logic around overwriting existing entries and also provide a warning when the namepsace is not set. --- src/warnet/users.py | 121 +++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/src/warnet/users.py b/src/warnet/users.py index c85e53585..01ef1f2bd 100644 --- a/src/warnet/users.py +++ b/src/warnet/users.py @@ -1,70 +1,85 @@ -import os -import subprocess import sys import click import yaml +from warnet.constants import KUBECONFIG + @click.command() -@click.argument("kube_config", type=str) -def auth(kube_config: str) -> None: - """ - Authenticate with a warnet cluster using a kube config file - """ - try: - current_kubeconfig = os.environ.get("KUBECONFIG", os.path.expanduser("~/.kube/config")) - combined_kubeconfig = ( - f"{current_kubeconfig}:{kube_config}" if current_kubeconfig else kube_config - ) - os.environ["KUBECONFIG"] = combined_kubeconfig - with open(kube_config) as file: - content = yaml.safe_load(file) - user = content["users"][0] - user_name = user["name"] - user_token = user["user"]["token"] - current_context = content["current-context"] - flatten_cmd = "kubectl config view --flatten" - result_flatten = subprocess.run( - flatten_cmd, shell=True, check=True, capture_output=True, text=True +@click.argument("auth_config", type=str) +def auth(auth_config): + """Authenticate with a Warnet cluster using a kubernetes config file""" + base_config = yaml_try_with_open(KUBECONFIG) + auth_config = yaml_try_with_open(auth_config) + + clusters = "clusters" + if clusters in auth_config: + merge_entries( + base_config.setdefault(clusters, []), auth_config[clusters], "name", "cluster" ) - except subprocess.CalledProcessError as e: - click.secho("Error occurred while executing kubectl config view --flatten:", fg="red") - click.secho(e.stderr, fg="red") - sys.exit(1) - if result_flatten.returncode == 0: - with open(current_kubeconfig, "w") as file: - file.write(result_flatten.stdout) - click.secho(f"Authorization file written to: {current_kubeconfig}", fg="green") - else: - click.secho("Could not create authorization file", fg="red") - click.secho(result_flatten.stderr, fg="red") - sys.exit(result_flatten.returncode) + users = "users" + if users in auth_config: + merge_entries(base_config.setdefault(users, []), auth_config[users], "name", "user") - try: - update_cmd = f"kubectl config set-credentials {user_name} --token {user_token}" - result_update = subprocess.run( - update_cmd, shell=True, check=True, capture_output=True, text=True + contexts = "contexts" + if contexts in auth_config: + merge_entries( + base_config.setdefault(contexts, []), auth_config[contexts], "name", "context" ) - if result_update.returncode != 0: - click.secho("Could not update authorization file", fg="red") - click.secho(result_flatten.stderr, fg="red") - sys.exit(result_flatten.returncode) - except subprocess.CalledProcessError as e: - click.secho("Error occurred while executing kubectl config view --flatten:", fg="red") - click.secho(e.stderr, fg="red") - sys.exit(1) - with open(current_kubeconfig) as file: - contents = yaml.safe_load(file) + new_current_context = auth_config.get("current-context") + base_config["current-context"] = new_current_context - with open(current_kubeconfig, "w") as file: - contents["current-context"] = current_context - yaml.safe_dump(contents, file) + # Check if the new current context has an explicit namespace + context_entry = next( + (ctx for ctx in base_config["contexts"] if ctx["name"] == new_current_context), None + ) + if context_entry and "namespace" not in context_entry["context"]: + click.secho( + f"Warning: The context '{new_current_context}' does not have an explicit namespace.", + fg="yellow", + ) - with open(current_kubeconfig) as file: + with open(KUBECONFIG, "w") as file: + yaml.safe_dump(base_config, file) + click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + + with open(KUBECONFIG) as file: contents = yaml.safe_load(file) click.secho( - f"\nwarnet's current context is now set to: {contents['current-context']}", fg="green" + f"Warnet's current context is now set to: {contents['current-context']}", fg="green" ) + + +def merge_entries(base_list, auth_list, key, entry_type): + base_entry_names = {entry[key] for entry in base_list} # Extract existing names + for entry in auth_list: + if entry[key] in base_entry_names: + if click.confirm( + f"The {entry_type} '{entry[key]}' already exists. Overwrite?", default=False + ): + # Find and replace the existing entry + base_list[:] = [e if e[key] != entry[key] else entry for e in base_list] + click.secho(f"Overwrote {entry_type} '{entry[key]}'", fg="yellow") + else: + click.secho(f"Skipped {entry_type} '{entry[key]}'", fg="yellow") + else: + base_list.append(entry) + click.secho(f"Added new {entry_type} '{entry[key]}'", fg="green") + + +def yaml_try_with_open(filename: str): + try: + with open(filename) as f: + return yaml.safe_load(f) + except FileNotFoundError: + click.secho(f"Could not find: {KUBECONFIG}", fg="red") + sys.exit(1) + except OSError as e: + click.secho(f"An I/O error occurred: {e}", fg="red") + sys.exit(1) + except Exception as e: + click.secho(f"An unexpected error occurred: {e}", fg="red") + sys.exit(1) From 1aed3fa380f3f7a1d6be197bb3be92bd196b6339 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 18 Sep 2024 23:08:22 -0500 Subject: [PATCH 04/55] `_logs`: do a container name dance 'bitcoincore' We need to specify a container name when we have prometheus logging enabled b/c we end up with a couple containers per pod. --- src/warnet/control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 60dfc7153..537642663 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -332,8 +332,10 @@ def _logs(pod_name: str, follow: bool): else: return # cancelled by user + container_name = "bitcoincore" if pod_name.startswith("tank") else None + try: - stream = pod_log(pod_name, container_name=None, follow=follow) + stream = pod_log(pod_name, container_name=container_name, follow=follow) for line in stream.stream(): print(line.decode("utf-8"), end=None) except Exception as e: From c31d0e59a1c7f56627b87cb7d5f531db7b7f928f Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 18 Sep 2024 23:10:51 -0500 Subject: [PATCH 05/55] remove todo from dag_connect --- resources/scenarios/test_scenarios/connect_dag.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/scenarios/test_scenarios/connect_dag.py b/resources/scenarios/test_scenarios/connect_dag.py index 5747291cb..7c9910d61 100644 --- a/resources/scenarios/test_scenarios/connect_dag.py +++ b/resources/scenarios/test_scenarios/connect_dag.py @@ -93,9 +93,6 @@ def run_test(self): self.assert_connection(eight_peers, 9, ConnectionType.DNS) self.assert_connection(nine_peers, 8, ConnectionType.IP) - # TODO: This needs to cause the test to fail - # assert False - self.log.info( f"Successfully ran the connect_dag.py scenario using a temporary file: " f"{os.path.basename(__file__)} " From 5903cb3d89e61263c7740d47a3b12cc917115aa0 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 02:11:59 -0500 Subject: [PATCH 06/55] clarify console text --- src/warnet/control.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 537642663..ae8975bb8 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -83,11 +83,8 @@ def stop_scenario(scenario_name): """Stop a single scenario using Helm""" # Stop the pod immediately (faster than uninstalling) namespace = get_default_namespace() - resp = delete_pod(scenario_name, namespace, grace_period=0, force=True) - if resp.status == "Success": - console.print(f"[bold green]Successfully stopped scenario: {scenario_name}[/bold green]") - else: - console.print(f"[bold red]Failed to stop scenario: {scenario_name}[/bold red]") + delete_pod(scenario_name, namespace, grace_period=0, force=True) + console.print(f"[bold yellow]Requested scenario stop: {scenario_name}[/bold yellow]") # Then uninstall via helm (non-blocking) command = f"helm uninstall {scenario_name} --namespace {namespace} --wait=false" From 592a14847271fffdbdb21e45a1cf559bdf82550d Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 02:12:45 -0500 Subject: [PATCH 07/55] `scenarios_test`: use `warnet logs` I did a drop-in replacement of `kubectl logs` with `warnet logs`. --- test/scenarios_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scenarios_test.py b/test/scenarios_test.py index 0b8ba7a4a..d66943a5c 100755 --- a/test/scenarios_test.py +++ b/test/scenarios_test.py @@ -62,7 +62,7 @@ def check_blocks(self, target_blocks, start: int = 0): try: deployed = scenarios_deployed() commander = deployed[0]["commander"] - command = f"kubectl logs {commander}" + command = f"warnet logs {commander}" print("\ncommander output:") print(run_command(command)) print("\n") From 1cfcc22d9b38d41cc5633b15fb23202c1b3789bb Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 02:13:37 -0500 Subject: [PATCH 08/55] `setup`: remove checks and mentions of kubectl Since we won't be using kubectl, there is no reason for it to be in the setup command. --- src/warnet/project.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 05d6237a4..37f3fc168 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -244,12 +244,6 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus: install_instruction="Please make sure minikube is running", install_url="https://minikube.sigs.k8s.io/docs/start/", ) - kubectl_info = ToolInfo( - tool_name="Kubectl", - is_installed_func=is_kubectl_installed, - install_instruction="Install kubectl.", - install_url="https://kubernetes.io/docs/tasks/tools/install-kubectl/", - ) helm_info = ToolInfo( tool_name="Helm", is_installed_func=is_helm_installed_and_offer_if_not, @@ -275,7 +269,7 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus: print(" ╰───────────────────────────╯ ") print(" ") print(" Let's find out if your system has what it takes to run Warnet...") - print("") + print(" ") try: questions = [ @@ -304,7 +298,6 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus: if is_platform_darwin(): check_results.append(check_installation(minikube_version_info)) check_results.append(check_installation(minikube_running_info)) - check_results.append(check_installation(kubectl_info)) check_results.append(check_installation(helm_info)) else: click.secho("Please re-run setup.", fg="yellow") From ab8460430241b84aa41fab3b45b9905cf9fb1831 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 02:56:32 -0500 Subject: [PATCH 09/55] `down`: fixup messages in console --- src/warnet/control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index ae8975bb8..673d461fc 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -144,7 +144,9 @@ def uninstall_release(namespace, release_name): # Wait for all tasks to complete and print results for future in as_completed(futures): - console.print(f"[yellow]{future.result()}[/yellow]") + result = future.result() + msg = result if isinstance(result, str) else result.metadata.name + console.print(f"[yellow]Deletion: {msg} [/yellow]") console.print("[bold yellow]Teardown process initiated for all components.[/bold yellow]") console.print("[bold yellow]Note: Some processes may continue in the background.[/bold yellow]") From fb2e283a9c5a4efb25fa024bd64ddd035ba9b353 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:03:42 -0500 Subject: [PATCH 10/55] namespaces: remove kubectl This seems like it needs further testing. --- src/warnet/namespaces.py | 44 +++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/warnet/namespaces.py b/src/warnet/namespaces.py index 45bcb7af5..46ec26876 100644 --- a/src/warnet/namespaces.py +++ b/src/warnet/namespaces.py @@ -9,7 +9,7 @@ NAMESPACES_DIR, NAMESPACES_FILE, ) -from .process import run_command, stream_command +from .k8s import CoreV1Api, V1Status, get_static_client def copy_namespaces_defaults(directory: Path): @@ -32,17 +32,14 @@ def namespaces(): """Namespaces commands""" -@click.argument( - "namespaces_dir", type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path) -) @namespaces.command() def list(): """List all namespaces with 'warnet-' prefix""" - cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" - res = run_command(cmd) - all_namespaces = res.split() - warnet_namespaces = [ns for ns in all_namespaces if ns.startswith("warnet-")] - + sclient: CoreV1Api = get_static_client() + all_namespaces = sclient.list_namespace() + warnet_namespaces = [ + ns.metadata.name for ns in all_namespaces.items if ns.metadata.name.startswith("warnet-") + ] if warnet_namespaces: print("Warnet namespaces:") for ns in warnet_namespaces: @@ -56,33 +53,34 @@ def list(): @click.argument("namespace", required=False) def destroy(destroy_all: bool, namespace: str): """Destroy a specific namespace or all warnet- prefixed namespaces""" + sclient: CoreV1Api = get_static_client() if destroy_all: - cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" - res = run_command(cmd) - - # Get the list of namespaces - all_namespaces = res.split() - warnet_namespaces = [ns for ns in all_namespaces if ns.startswith("warnet-")] + all_namespaces = sclient.list_namespace() + warnet_namespaces = [ + ns.metadata.name + for ns in all_namespaces.items + if ns.metadata.name.startswith("warnet-") + ] if not warnet_namespaces: print("No warnet namespaces found to destroy.") return for ns in warnet_namespaces: - destroy_cmd = f"kubectl delete namespace {ns}" - if not stream_command(destroy_cmd): - print(f"Failed to destroy namespace: {ns}") + resp: V1Status = sclient.delete_namespace(ns) + if resp.status: + print(f"Destroyed namespace: {ns} with {resp.status}") else: - print(f"Destroyed namespace: {ns}") + print(f"Failed to destroy namespace: {ns}") elif namespace: if not namespace.startswith("warnet-"): print("Error: Can only destroy namespaces with 'warnet-' prefix") return - destroy_cmd = f"kubectl delete namespace {namespace}" - if not stream_command(destroy_cmd): - print(f"Failed to destroy namespace: {namespace}") + resp: V1Status = sclient.delete_namespace(namespace) + if resp.status: + print(f"Destroying namespace: {namespace} with {resp.status}") else: - print(f"Destroyed namespace: {namespace}") + print(f"Failed to destroy namespace: {namespace}") else: print("Error: Please specify a namespace or use --all flag.") From ab215ebb0503869213deaec96f2a966a673bb7fb Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:06:04 -0500 Subject: [PATCH 11/55] `get_static_client`: swap out type in return value --- src/warnet/k8s.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 5ea14b0cb..e40f95732 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -8,6 +8,7 @@ import yaml from kubernetes import client, config, watch from kubernetes.client.models import CoreV1Event, V1PodList +from kubernetes.client.api import CoreV1Api from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -22,7 +23,7 @@ from .process import run_command, stream_command -def get_static_client() -> CoreV1Event: +def get_static_client() -> CoreV1Api: config.load_kube_config(config_file=KUBECONFIG) return client.CoreV1Api() From 43a46a9d5a00a0229095f974957863f85341be84 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:07:19 -0500 Subject: [PATCH 12/55] `kexec`: add kexec to replace kubectl exec --- src/warnet/k8s.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index e40f95732..aa93f42b7 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -33,6 +33,22 @@ def get_dynamic_client() -> DynamicClient: return DynamicClient(client.ApiClient()) +def kexec(pod: str, namespace: str, cmd: [str]) -> str: + """It's `kubectl exec` but with a k at the beginning so as not to conflict with python's `exec`""" + sclient = get_static_client() + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod, + namespace, + command=cmd, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + return resp + + def get_pods() -> V1PodList: sclient = get_static_client() try: From 502828223a767a0f95073a7f7d1df7adeef6667a Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:09:03 -0500 Subject: [PATCH 13/55] `kubectl_context`: switch up this functional This function is not used as far as I can tell, and it is not tested. Perhaps consider removing or integrating somehow into the namespace work if needed. --- src/warnet/k8s.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index aa93f42b7..da241f1b3 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -21,6 +21,7 @@ LOGGING_NAMESPACE, ) from .process import run_command, stream_command +from .constants import KUBECONFIG def get_static_client() -> CoreV1Api: @@ -100,17 +101,30 @@ def create_kubernetes_object( return obj -def set_kubectl_context(namespace: str) -> bool: +def set_context_namespace(namespace: str): """ - Set the default kubectl context to the specified namespace. + Set the default kubeconfig context to the specified namespace. """ - command = f"kubectl config set-context --current --namespace={namespace}" - result = stream_command(command) - if result: - print(f"Kubectl context set to namespace: {namespace}") - else: - print(f"Failed to set kubectl context to namespace: {namespace}") - return result + with open(KUBECONFIG) as file: + kubeconfig_data = yaml.safe_load(file) + + current_context_name = kubeconfig_data.get("current-context") + if not current_context_name: + raise ValueError("No current context set in kubeconfig.") + + context_entry = None + for context in kubeconfig_data.get("contexts", []): + if context["name"] == current_context_name: + context_entry = context + break + + if not context_entry: + raise ValueError(f"Context '{current_context_name}' not found in kubeconfig.") + + context_entry["context"]["namespace"] = namespace + + with open(KUBECONFIG, "w") as file: + yaml.safe_dump(kubeconfig_data, file) def apply_kubernetes_yaml(yaml_file: str) -> bool: From 52f04bdcd9bd0df53e8c80664b3572a24b8e1110 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:11:00 -0500 Subject: [PATCH 14/55] `apply_kubernetes_yaml`: remove kubectl Not sure if we are using this. Does not appear to be tested. --- src/warnet/k8s.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index da241f1b3..e0c4e778a 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -128,8 +128,13 @@ def set_context_namespace(namespace: str): def apply_kubernetes_yaml(yaml_file: str) -> bool: - command = f"kubectl apply -f {yaml_file}" - return stream_command(command) + v1 = get_static_client() + path = os.path.abspath(yaml_file) + try: + create_from_yaml(v1, path) + return True + except Exception as e: + raise e def apply_kubernetes_yaml_obj(yaml_obj: str) -> None: From 73f1676fb0cf533c51b78a8428cd03dff8b049e1 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:13:17 -0500 Subject: [PATCH 15/55] `delete_namespace`: removed kubectl --- src/warnet/k8s.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index e0c4e778a..97b5bcfdc 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -148,9 +148,10 @@ def apply_kubernetes_yaml_obj(yaml_obj: str) -> None: Path(temp_file_path).unlink() -def delete_namespace(namespace: str) -> bool: - command = f"kubectl delete namespace {namespace} --ignore-not-found" - return run_command(command) +def delete_namespace(namespace: str) -> V1Status: + v1: CoreV1Api = get_static_client() + resp = v1.delete_namespace(namespace) + return resp def delete_pod(pod_name: str) -> bool: From b00c2ee213fa36a800bac43abd60fdcfb1b962b7 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:14:46 -0500 Subject: [PATCH 16/55] `delete_pod`: appears to work --- src/warnet/k8s.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 97b5bcfdc..56e254759 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -7,8 +7,8 @@ import yaml from kubernetes import client, config, watch -from kubernetes.client.models import CoreV1Event, V1PodList from kubernetes.client.api import CoreV1Api +from kubernetes.client.models import V1DeleteOptions, V1PodList, V1Status from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -154,9 +154,27 @@ def delete_namespace(namespace: str) -> V1Status: return resp -def delete_pod(pod_name: str) -> bool: - command = f"kubectl -n {get_default_namespace()} delete pod {pod_name}" - return stream_command(command) +def delete_pod( + pod_name: str, + namespace: str, + grace_period: int = 30, + force: bool = False, + ignore_not_found: bool = True, +) -> Optional[V1Status]: + v1: CoreV1Api = get_static_client() + delete_options = V1DeleteOptions( + grace_period_seconds=grace_period, + propagation_policy="Foreground" if force else "Background", + ) + try: + resp = v1.delete_namespaced_pod(name=pod_name, namespace=namespace, body=delete_options) + return resp + except ApiException as e: + if e.status == 404 and ignore_not_found: + print(f"Pod {pod_name} in namespace {namespace} not found, but ignoring as requested.") + return None + else: + raise def get_default_namespace() -> str: From 67b00ed14b1cca71396a4a16762356fda26866b8 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:16:21 -0500 Subject: [PATCH 17/55] `get_default_namespace`: remove kubectl --- src/warnet/k8s.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 56e254759..763d30113 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -178,18 +178,24 @@ def delete_pod( def get_default_namespace() -> str: - command = "kubectl config view --minify -o jsonpath='{..namespace}'" - try: - kubectl_namespace = run_command(command) - except Exception as e: - print(e) - if str(e).find("command not found"): - print( - "It looks like kubectl is not installed. Please install it to continue: " - "https://kubernetes.io/docs/tasks/tools/" - ) - sys.exit(1) - return kubectl_namespace if kubectl_namespace else DEFAULT_NAMESPACE + with open(KUBECONFIG) as file: + kubeconfig_data = yaml.safe_load(file) + + current_context_name = kubeconfig_data.get("current-context") + if not current_context_name: + raise ValueError("No current context set in kubeconfig.") + + context_entry = None + for context in kubeconfig_data.get("contexts", []): + if context["name"] == current_context_name: + context_entry = context + break + + if not context_entry: + raise ValueError(f"Context '{current_context_name}' not found in kubeconfig.") + + namespace = context_entry["context"]["namespace"] + return namespace def snapshot_bitcoin_datadir( From 8856ad5ad3f6cf1d2dbb6eeb751d2be340f8fca3 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:17:12 -0500 Subject: [PATCH 18/55] `k8s`: fixup imports --- src/warnet/k8s.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 763d30113..eb6a1d9ba 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -1,9 +1,9 @@ import json import os -import sys import tempfile from pathlib import Path from time import sleep +from typing import Optional import yaml from kubernetes import client, config, watch @@ -12,6 +12,7 @@ from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream +from kubernetes.utils import create_from_yaml from .constants import ( CADDY_INGRESS_NAME, From ff1054e0885285c27edd9b30d652f02336418610 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:19:26 -0500 Subject: [PATCH 19/55] `rpc`: remove kubectl Had to do an annoying dance because it seems like stream() wants to convert any json it sees into python primitives. There's gotta be a better way to deal with that, but I came up with crudely checking if we are getting a list or dict and literal_eval-ing it. Maybe I'm missing something. Otherwise, seems to work. --- src/warnet/bitcoin.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index a27da3bc7..a24fceb49 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -23,7 +23,7 @@ def bitcoin(): @click.argument("tank", type=str) @click.argument("method", type=str) @click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments -def rpc(tank: str, method: str, params: str): +def rpc(tank: str, method: str, params: tuple[str, ...]): """ Call bitcoin-cli [params] on """ @@ -35,15 +35,37 @@ def rpc(tank: str, method: str, params: str): print(result) -def _rpc(tank: str, method: str, params: str): +def _rpc(tank: str, method: str, params: tuple[str, ...]) -> str: # bitcoin-cli should be able to read bitcoin.conf inside the container # so no extra args like port, chain, username or password are needed namespace = get_default_namespace() + + sclient = get_static_client() if params: - cmd = f"kubectl -n {namespace} exec {tank} -- bitcoin-cli {method} {' '.join(map(str, params))}" + cmd = ["bitcoin-cli", method] + cmd.extend(params) + else: + cmd = ["bitcoin-cli", method] + resp = stream( + sclient.connect_get_namespaced_pod_exec, + tank, + namespace, + container="bitcoincore", + command=cmd, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + # The k8s lib seems to convert json into its python representation. The following dance seems to + # avoid the worst of it. + if resp.startswith("{") or resp.startswith("["): + literal = ast.literal_eval(resp) + json_string = json.dumps(literal) + return json_string else: - cmd = f"kubectl -n {namespace} exec {tank} -- bitcoin-cli {method}" - return run_command(cmd) + # When bitcoin-cli responds with a txid or similar, don't try to `literal_eval` it. + return resp @bitcoin.command() From a20c949f8bf43cdf0a163f6e5d32b97bde45b868 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:21:35 -0500 Subject: [PATCH 20/55] `debug_log`: just swapped it out with warnet logs --- src/warnet/bitcoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index a24fceb49..b10ab5383 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -74,7 +74,7 @@ def debug_log(tank: str): """ Fetch the Bitcoin Core debug log from """ - cmd = f"kubectl logs {tank}" + cmd = f"warnet logs {tank}" try: print(run_command(cmd)) except Exception as e: From f0a8eb1d26b3d11c9952ecf595e071400e0f111f Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:22:07 -0500 Subject: [PATCH 21/55] `grep_logs`: minor change; seems to work --- src/warnet/bitcoin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index b10ab5383..3b856579e 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -89,6 +89,7 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): """ Grep combined bitcoind logs using regex """ + sclient = get_static_client() try: tanks = get_mission("tank") @@ -111,8 +112,12 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): continue # Get logs from the specific container - command = f"kubectl logs {pod_name} -c {container_name} --timestamps" - logs = run_command(command) + logs = sclient.read_namespaced_pod_log( + name=pod_name, + namespace=get_default_namespace(), + container=container_name, + timestamps=True, + ) if logs is not False: # Process logs From 210ad8bc72e7aa960008eba7cf8821e107178c1c Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:23:27 -0500 Subject: [PATCH 22/55] `get_messages`: lots of changes I made a number of changes to get_messages, not the least of which was swapping out `cat` with `base64` for the purposes of file transfer. Appears to work. --- src/warnet/bitcoin.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 3b856579e..36184d9e3 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -190,21 +190,26 @@ def get_messages(tank_a: str, tank_b: str, chain: str): """ Fetch messages from the message capture files """ + sclient = get_static_client() + subdir = "" if chain == "main" else f"{chain}/" base_dir = f"/root/.bitcoin/{subdir}message_capture" # Get the IP of node_b - cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}'" - tank_b_ip = run_command(cmd).strip() + tank_b_pod: V1Pod = sclient.read_namespaced_pod(name=tank_b, namespace=get_default_namespace()) + tank_b_ip = tank_b_pod.status.pod_ip # Get the service IP of node_b - cmd = f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}'" - tank_b_service_ip = run_command(cmd).strip() + tank_b_service: V1Service = sclient.read_namespaced_service( + name=tank_b, namespace=get_default_namespace() + ) + tank_b_service_ip = tank_b_service.spec.cluster_ip # List directories in the message capture folder - cmd = f"kubectl exec {tank_a} -- ls {base_dir}" - dirs = run_command(cmd).splitlines() + resp = kexec(tank_a, get_default_namespace(), ["ls", base_dir]) + + dirs = resp.splitlines() messages = [] @@ -213,18 +218,15 @@ def get_messages(tank_a: str, tank_b: str, chain: str): for file, outbound in [["msgs_recv.dat", False], ["msgs_sent.dat", True]]: file_path = f"{base_dir}/{dir_name}/{file}" # Fetch the file contents from the container - cmd = f"kubectl exec {tank_a} -- cat {file_path}" - import subprocess - - blob = subprocess.run( - cmd, shell=True, capture_output=True, executable="bash" - ).stdout + resp = kexec(tank_a, get_default_namespace(), ["base64", file_path]) + resp_bytes = base64.b64decode(resp) # Parse the blob - json = parse_raw_messages(blob, outbound) + json = parse_raw_messages(resp_bytes, outbound) messages = messages + json messages.sort(key=lambda x: x["time"]) + return messages From b37cea872acc9be2e95812a6ee99ed1bda805c51 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:24:21 -0500 Subject: [PATCH 23/55] `bitcoin`: import fixups --- src/warnet/bitcoin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 36184d9e3..abb45c034 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,3 +1,6 @@ +import ast +import base64 +import json import os import re import sys @@ -5,12 +8,14 @@ from io import BytesIO import click +from kubernetes.client.models import V1Pod, V1Service +from kubernetes.stream import stream from urllib3.exceptions import MaxRetryError from test_framework.messages import ser_uint256 from test_framework.p2p import MESSAGEMAP -from .k8s import get_default_namespace, get_mission +from .k8s import get_default_namespace, get_mission, get_static_client, kexec from .process import run_command From 0dce428298030faecd2824e41e76c6cb64570c47 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 03:24:33 -0500 Subject: [PATCH 24/55] removed kubectl from setup_minikube.sh I think we probably should just get rid of this file. --- resources/scripts/setup_minikube.sh | 9 --------- 1 file changed, 9 deletions(-) diff --git a/resources/scripts/setup_minikube.sh b/resources/scripts/setup_minikube.sh index f835eed7c..eb364127e 100755 --- a/resources/scripts/setup_minikube.sh +++ b/resources/scripts/setup_minikube.sh @@ -72,15 +72,6 @@ else ERROR_CODE=127 fi -kubectl_path=$(command -v kubectl || true) -if [ -n "$kubectl_path" ]; then - print_partial_message " ⭐️ Found " "kubectl" ": $kubectl_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "kubectl" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://kubernetes.io/docs/tasks/tools/" "$BOLD" - ERROR_CODE=127 -fi - helm_path=$(command -v helm || true) if [ -n "$helm_path" ]; then print_partial_message " ⭐️ Found " "helm" ": $helm_path" "$BOLD" From 147fd195e13b042e8930ccb968144efe26367dc8 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 15:15:48 -0500 Subject: [PATCH 25/55] `pod_log`: add type; add timestamp option --- src/warnet/k8s.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index eb6a1d9ba..f9305f256 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -13,6 +13,7 @@ from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream from kubernetes.utils import create_from_yaml +from urllib3 import HTTPResponse from .constants import ( CADDY_INGRESS_NAME, @@ -394,7 +395,7 @@ def get_ingress_ip_or_host(): return None -def pod_log(pod_name, container_name=None, follow=False): +def pod_log(pod_name, container_name=None, follow=False, timestamps=False) -> HTTPResponse: sclient = get_static_client() try: return sclient.read_namespaced_pod_log( @@ -402,6 +403,7 @@ def pod_log(pod_name, container_name=None, follow=False): namespace=get_default_namespace(), container=container_name, follow=follow, + timestamps=timestamps, _preload_content=False, ) except ApiException as e: From 10cbb906f2db5bc74024252ddbd15a083298ca2f Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 15:16:49 -0500 Subject: [PATCH 26/55] `grep_logs`: fixup and use pod_log instead --- src/warnet/bitcoin.py | 43 +++++++++++++++++++++++++++---------------- src/warnet/k8s.py | 4 ++-- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index abb45c034..88f09ea3e 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,5 +1,6 @@ import ast import base64 +import codecs import json import os import re @@ -15,7 +16,7 @@ from test_framework.messages import ser_uint256 from test_framework.p2p import MESSAGEMAP -from .k8s import get_default_namespace, get_mission, get_static_client, kexec +from .k8s import get_default_namespace, get_mission, get_static_client, kexec, pod_log from .process import run_command @@ -94,15 +95,13 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): """ Grep combined bitcoind logs using regex """ - sclient = get_static_client() - try: tanks = get_mission("tank") except MaxRetryError as e: print(f"{e}") sys.exit(1) - matching_logs = [] + matching_logs: list[tuple[str, any]] = [] for tank in tanks: pod_name = tank.metadata.name @@ -117,18 +116,14 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): continue # Get logs from the specific container - logs = sclient.read_namespaced_pod_log( - name=pod_name, - namespace=get_default_namespace(), - container=container_name, - timestamps=True, - ) - - if logs is not False: - # Process logs - for log_entry in logs.splitlines(): - if re.search(pattern, log_entry): - matching_logs.append((log_entry, pod_name)) + log_stream = pod_log(pod_name, container_name, timestamps=True) + + compiled_pattern = re.compile(pattern) + + for log_line in iter_lines_from_stream(log_stream): + log_entry = log_line.rstrip("\n") + if compiled_pattern.search(log_entry): + matching_logs.append((log_entry, pod_name)) # Sort logs if needed if not no_sort: @@ -153,6 +148,22 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): return matching_logs +def iter_lines_from_stream(log_stream, encoding="utf-8"): + decoder = codecs.getincrementaldecoder(encoding)() + buffer = "" + for chunk in log_stream.stream(): + # Decode the chunk incrementally + text = decoder.decode(chunk) + buffer += text + # Split the buffer into lines + lines = buffer.split("\n") + buffer = lines.pop() # Last item is incomplete line or empty + yield from lines + # Yield any remaining text in the buffer + if buffer: + yield buffer + + @bitcoin.command() @click.argument("tank_a", type=str, required=True) @click.argument("tank_b", type=str, required=True) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index f9305f256..79d772753 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -8,7 +8,7 @@ import yaml from kubernetes import client, config, watch from kubernetes.client.api import CoreV1Api -from kubernetes.client.models import V1DeleteOptions, V1PodList, V1Status +from kubernetes.client.models import V1DeleteOptions, V1Pod, V1PodList, V1Status from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -61,7 +61,7 @@ def get_pods() -> V1PodList: return pod_list -def get_mission(mission: str) -> list[V1PodList]: +def get_mission(mission: str) -> list[V1Pod]: pods = get_pods() crew = [] for pod in pods.items: From 844668a16c43aa06dbbd40d3032eeef340869d17 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 15:34:28 -0500 Subject: [PATCH 27/55] `_rpc`: fix goofy json parsing workaround I went with the _preload_content=False approach --- src/warnet/bitcoin.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 88f09ea3e..8bd0585de 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,7 +1,5 @@ -import ast import base64 import codecs -import json import os import re import sys @@ -62,16 +60,19 @@ def _rpc(tank: str, method: str, params: tuple[str, ...]) -> str: stdin=False, stdout=True, tty=False, + _preload_content=False, ) - # The k8s lib seems to convert json into its python representation. The following dance seems to - # avoid the worst of it. - if resp.startswith("{") or resp.startswith("["): - literal = ast.literal_eval(resp) - json_string = json.dumps(literal) - return json_string - else: - # When bitcoin-cli responds with a txid or similar, don't try to `literal_eval` it. - return resp + stdout = "" + stderr = "" + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + stdout_chunk = resp.read_stdout() + stdout += stdout_chunk + if resp.peek_stderr(): + stderr_chunk = resp.read_stderr() + stderr += stderr_chunk + return stdout + stderr @bitcoin.command() From 10177ede57e323d53666e156b1b0ac26eb108926 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 16:15:26 -0500 Subject: [PATCH 28/55] `k8s`: add get_pod and get_service --- src/warnet/k8s.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 79d772753..1f0e7603e 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -8,7 +8,7 @@ import yaml from kubernetes import client, config, watch from kubernetes.client.api import CoreV1Api -from kubernetes.client.models import V1DeleteOptions, V1Pod, V1PodList, V1Status +from kubernetes.client.models import V1DeleteOptions, V1Pod, V1PodList, V1Service, V1Status from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -52,6 +52,20 @@ def kexec(pod: str, namespace: str, cmd: [str]) -> str: return resp +def get_service(name: str, namespace: Optional[str] = None) -> V1Service: + sclient = get_static_client() + if not namespace: + namespace = get_default_namespace() + return sclient.read_namespaced_service(name=name, namespace=namespace) + + +def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: + sclient = get_static_client() + if not namespace: + namespace = get_default_namespace() + return sclient.read_namespaced_pod(name=name, namespace=namespace) + + def get_pods() -> V1PodList: sclient = get_static_client() try: From f4adc6eebdca41d08d8b3432542f49dda881db7e Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 16:15:45 -0500 Subject: [PATCH 29/55] `bitcoin`: use get_pod and get_service --- src/warnet/bitcoin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 8bd0585de..c54e86a2e 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -7,14 +7,21 @@ from io import BytesIO import click -from kubernetes.client.models import V1Pod, V1Service from kubernetes.stream import stream from urllib3.exceptions import MaxRetryError from test_framework.messages import ser_uint256 from test_framework.p2p import MESSAGEMAP -from .k8s import get_default_namespace, get_mission, get_static_client, kexec, pod_log +from .k8s import ( + get_default_namespace, + get_mission, + get_pod, + get_service, + get_static_client, + kexec, + pod_log, +) from .process import run_command @@ -207,19 +214,15 @@ def get_messages(tank_a: str, tank_b: str, chain: str): """ Fetch messages from the message capture files """ - sclient = get_static_client() - subdir = "" if chain == "main" else f"{chain}/" base_dir = f"/root/.bitcoin/{subdir}message_capture" # Get the IP of node_b - tank_b_pod: V1Pod = sclient.read_namespaced_pod(name=tank_b, namespace=get_default_namespace()) + tank_b_pod = get_pod(tank_b) tank_b_ip = tank_b_pod.status.pod_ip # Get the service IP of node_b - tank_b_service: V1Service = sclient.read_namespaced_service( - name=tank_b, namespace=get_default_namespace() - ) + tank_b_service = get_service(tank_b) tank_b_service_ip = tank_b_service.spec.cluster_ip # List directories in the message capture folder From 366e42aaf31292030ada296e3947a26fd5c3e5a8 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 16:32:08 -0500 Subject: [PATCH 30/55] `_logs`: do not rely on container name Instead rely on the position of the container. Position Rule: we get logs from the first container as reported by V1Pod's spec.container metadata. --- src/warnet/control.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 673d461fc..eec5c9b3c 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -21,6 +21,7 @@ delete_pod, get_default_namespace, get_mission, + get_pod, get_pods, pod_log, snapshot_bitcoin_datadir, @@ -331,7 +332,13 @@ def _logs(pod_name: str, follow: bool): else: return # cancelled by user - container_name = "bitcoincore" if pod_name.startswith("tank") else None + try: + pod = get_pod(pod_name) + container_names = [container.name for container in pod.spec.containers] + container_name = container_names[0] + except Exception as e: + print(f"Could not determine primary container: {e}") + container_name = None try: stream = pod_log(pod_name, container_name=container_name, follow=follow) From 9d47a896fff9517b537c7f7ee9b25e382a94aea3 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 17:02:14 -0500 Subject: [PATCH 31/55] clarify default namespace --- src/warnet/k8s.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 1f0e7603e..ddffcb7a5 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -210,7 +210,9 @@ def get_default_namespace() -> str: if not context_entry: raise ValueError(f"Context '{current_context_name}' not found in kubeconfig.") - namespace = context_entry["context"]["namespace"] + # TODO: need to setting on Warnet's "default" namespace + namespace = context_entry["context"].get("namespace", "warnet") + return namespace From 65f0717b2f2719c14a629e2ba47932a4894a8008 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 19 Sep 2024 17:27:10 -0500 Subject: [PATCH 32/55] `auth`: create kubeconfig from credential file --- src/warnet/users.py | 62 +++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/warnet/users.py b/src/warnet/users.py index 01ef1f2bd..e230547d4 100644 --- a/src/warnet/users.py +++ b/src/warnet/users.py @@ -1,3 +1,4 @@ +import os import sys import click @@ -10,24 +11,33 @@ @click.argument("auth_config", type=str) def auth(auth_config): """Authenticate with a Warnet cluster using a kubernetes config file""" - base_config = yaml_try_with_open(KUBECONFIG) + # TODO: use os.replace for more atomic file writing auth_config = yaml_try_with_open(auth_config) - clusters = "clusters" - if clusters in auth_config: - merge_entries( - base_config.setdefault(clusters, []), auth_config[clusters], "name", "cluster" - ) + is_first_config = False + if not os.path.exists(KUBECONFIG): + with open(KUBECONFIG, "w") as file: + yaml.safe_dump(auth_config, file) + is_first_config = True - users = "users" - if users in auth_config: - merge_entries(base_config.setdefault(users, []), auth_config[users], "name", "user") + base_config = yaml_try_with_open(KUBECONFIG) - contexts = "contexts" - if contexts in auth_config: - merge_entries( - base_config.setdefault(contexts, []), auth_config[contexts], "name", "context" - ) + if not is_first_config: + clusters = "clusters" + if clusters in auth_config: + merge_entries( + base_config.setdefault(clusters, []), auth_config[clusters], "name", "cluster" + ) + + users = "users" + if users in auth_config: + merge_entries(base_config.setdefault(users, []), auth_config[users], "name", "user") + + contexts = "contexts" + if contexts in auth_config: + merge_entries( + base_config.setdefault(contexts, []), auth_config[contexts], "name", "context" + ) new_current_context = auth_config.get("current-context") base_config["current-context"] = new_current_context @@ -42,15 +52,23 @@ def auth(auth_config): fg="yellow", ) - with open(KUBECONFIG, "w") as file: - yaml.safe_dump(base_config, file) - click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + try: + with open(KUBECONFIG, "w") as file: + yaml.safe_dump(base_config, file) + click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + except OSError as e: + click.secho(f"Error writing to {KUBECONFIG}: {e}", fg="red") + sys.exit(1) - with open(KUBECONFIG) as file: - contents = yaml.safe_load(file) - click.secho( - f"Warnet's current context is now set to: {contents['current-context']}", fg="green" - ) + try: + with open(KUBECONFIG) as file: + contents = yaml.safe_load(file) + click.secho( + f"Warnet's current context is now set to: {contents['current-context']}", fg="green" + ) + except (OSError, yaml.YAMLError) as e: + click.secho(f"Error reading from {KUBECONFIG}: {e}", fg="red") + sys.exit(1) def merge_entries(base_list, auth_list, key, entry_type): From 83f5f4da6ff5a01bc7a3396209334e6e94f1010f Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 12:00:55 -0500 Subject: [PATCH 33/55] fix colorization of log output --- src/warnet/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index eec5c9b3c..e3ad2649c 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -343,7 +343,7 @@ def _logs(pod_name: str, follow: bool): try: stream = pod_log(pod_name, container_name=container_name, follow=follow) for line in stream.stream(): - print(line.decode("utf-8"), end=None) + click.secho(line.decode("utf-8")) except Exception as e: print(e) except KeyboardInterrupt: From fb0c1090188b81b484928b7292162e43d57813ac Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 12:01:26 -0500 Subject: [PATCH 34/55] clean up imports --- src/warnet/k8s.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index ddffcb7a5..f8630bf26 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -17,13 +17,10 @@ from .constants import ( CADDY_INGRESS_NAME, - DEFAULT_NAMESPACE, INGRESS_NAMESPACE, KUBECONFIG, LOGGING_NAMESPACE, ) -from .process import run_command, stream_command -from .constants import KUBECONFIG def get_static_client() -> CoreV1Api: From 26d177ebae35eb887be269f08bd5c9a49e873cc4 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 15:50:32 -0500 Subject: [PATCH 35/55] `control`: catch None type; fix coloring --- src/warnet/control.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index e3ad2649c..1b387eafb 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -146,8 +146,10 @@ def uninstall_release(namespace, release_name): # Wait for all tasks to complete and print results for future in as_completed(futures): result = future.result() - msg = result if isinstance(result, str) else result.metadata.name - console.print(f"[yellow]Deletion: {msg} [/yellow]") + msg = "" + if result: + msg = result if isinstance(result, str) else result.metadata.name + click.secho(f"Deletion: {msg}", fg="yellow") console.print("[bold yellow]Teardown process initiated for all components.[/bold yellow]") console.print("[bold yellow]Note: Some processes may continue in the background.[/bold yellow]") From 030524737a29eca6d2596a4963a52964d2661739 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 16:52:14 -0500 Subject: [PATCH 36/55] `test_base`: use shlex to handle cmd splitting shlex appears to handle escape sequences nicely. --- test/test_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_base.py b/test/test_base.py index 1a2a4c983..dfb916c94 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -3,6 +3,7 @@ import logging.config import os import re +import shlex import threading from pathlib import Path from subprocess import run @@ -66,7 +67,7 @@ def assert_log_msgs(self): def warnet(self, cmd): self.log.debug(f"Executing warnet command: {cmd}") - command = ["warnet"] + cmd.split() + command = ["warnet"] + shlex.split(cmd) # shlex handles escape sequences nicely proc = run(command, capture_output=True) if proc.stderr: raise Exception(proc.stderr.decode().strip()) From b277248d266089a48d9b6488cf002cc7e3b43ad4 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 21:39:33 -0500 Subject: [PATCH 37/55] `k8s`: add a K8sError I figure we should add an error type for the k8s file, that way we can try...except everything from that file using just one error type. --- src/warnet/k8s.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index f8630bf26..d0aab6d30 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -23,6 +23,10 @@ ) +class K8sError(Exception): + pass + + def get_static_client() -> CoreV1Api: config.load_kube_config(config_file=KUBECONFIG) return client.CoreV1Api() From 333e27731ac8a1407f6a81c5281c8ce8fb582c17 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 21:40:35 -0500 Subject: [PATCH 38/55] `k8s`: update type in create_kubernetes_object --- src/warnet/k8s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index d0aab6d30..11c74a652 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -105,7 +105,7 @@ def get_edges() -> any: def create_kubernetes_object( - kind: str, metadata: dict[str, any], spec: dict[str, any] = None + kind: str, metadata: dict[str, any], spec: Optional[dict[str, any]] = None ) -> dict[str, any]: metadata["namespace"] = get_default_namespace() obj = { From b943c8170f2ab7d12f7486410b1026445b96ef6b Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 21:42:33 -0500 Subject: [PATCH 39/55] `k8s`: add open and write fns for KUBECONFIG --- src/warnet/k8s.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 11c74a652..113d679e9 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -460,3 +460,23 @@ def write_file_to_container(pod_name, container_name, dst_path, data): return True except Exception as e: print(f"Failed to copy data to {pod_name}({container_name}):{dst_path}:\n{e}") + +def open_kubeconfig(kubeconfig_path: str = KUBECONFIG) -> dict: + try: + with open(kubeconfig_path) as file: + return yaml.safe_load(file) + except FileNotFoundError as e: + raise K8sError(f"Kubeconfig file {kubeconfig_path} not found.") from e + except yaml.YAMLError as e: + raise K8sError(f"Error parsing kubeconfig: {e}") from e + + +def write_kubeconfig(kube_config: dict) -> None: + dir_name = os.path.dirname(KUBECONFIG) + try: + with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as temp_file: + yaml.safe_dump(kube_config, temp_file) + os.replace(temp_file.name, KUBECONFIG) + except Exception as e: + os.remove(temp_file.name) + raise K8sError(f"Error writing kubeconfig: {KUBECONFIG}") from e From 66d74b6ef33ff5ccb95b9e61e2ddb8a2d1dc70c4 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 21:43:17 -0500 Subject: [PATCH 40/55] `k8s`: update namespace and context checking fns --- src/warnet/k8s.py | 90 ++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 113d679e9..6902c8472 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -118,30 +118,63 @@ def create_kubernetes_object( return obj -def set_context_namespace(namespace: str): - """ - Set the default kubeconfig context to the specified namespace. - """ - with open(KUBECONFIG) as file: - kubeconfig_data = yaml.safe_load(file) - +def get_context_entry(kubeconfig_data: dict) -> dict: current_context_name = kubeconfig_data.get("current-context") if not current_context_name: - raise ValueError("No current context set in kubeconfig.") - - context_entry = None - for context in kubeconfig_data.get("contexts", []): - if context["name"] == current_context_name: - context_entry = context - break + raise K8sError(f"Could not determine current context from config data: {kubeconfig_data}") + + context_entry = next( + ( + ctx + for ctx in kubeconfig_data.get("contexts", []) + if ctx.get("name") == current_context_name + ), + None, + ) if not context_entry: - raise ValueError(f"Context '{current_context_name}' not found in kubeconfig.") + raise K8sError(f"Context '{current_context_name}' not found in kubeconfig.") + + return context_entry + + +def set_context_namespace(namespace: str) -> None: + """ + Set the namespace within the KUBECONFIG's current context + """ + try: + kubeconfig_data = open_kubeconfig() + except K8sError as e: + raise K8sError(f"Could not open KUBECONFIG: {KUBECONFIG}") from e + + try: + context_entry = get_context_entry(kubeconfig_data) + except K8sError as e: + raise K8sError(f"Could not get context entry for {KUBECONFIG}") from e context_entry["context"]["namespace"] = namespace - with open(KUBECONFIG, "w") as file: - yaml.safe_dump(kubeconfig_data, file) + try: + write_kubeconfig(kubeconfig_data) + except Exception as e: + raise K8sError(f"Could not write to KUBECONFIG: {KUBECONFIG}") from e + + +def get_default_namespace() -> str: + try: + kubeconfig_data = open_kubeconfig() + except K8sError as e: + raise K8sError(f"Could not open KUBECONFIG: {KUBECONFIG}") from e + + try: + context_entry = get_context_entry(kubeconfig_data) + except K8sError as e: + raise K8sError(f"Could not get context entry for {KUBECONFIG}") from e + + # TODO: need to settle on Warnet's "default" namespace + namespace = context_entry["context"].get("namespace", "warnet") + + return namespace def apply_kubernetes_yaml(yaml_file: str) -> bool: @@ -194,29 +227,6 @@ def delete_pod( raise -def get_default_namespace() -> str: - with open(KUBECONFIG) as file: - kubeconfig_data = yaml.safe_load(file) - - current_context_name = kubeconfig_data.get("current-context") - if not current_context_name: - raise ValueError("No current context set in kubeconfig.") - - context_entry = None - for context in kubeconfig_data.get("contexts", []): - if context["name"] == current_context_name: - context_entry = context - break - - if not context_entry: - raise ValueError(f"Context '{current_context_name}' not found in kubeconfig.") - - # TODO: need to setting on Warnet's "default" namespace - namespace = context_entry["context"].get("namespace", "warnet") - - return namespace - - def snapshot_bitcoin_datadir( pod_name: str, chain: str, local_path: str = "./", filters: list[str] = None ) -> None: From f6802684e8a30442506c2763fd3866e446629533 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 21:44:20 -0500 Subject: [PATCH 41/55] `users`: improve auth fn Incorporates better error handling --- src/warnet/users.py | 61 +++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/warnet/users.py b/src/warnet/users.py index e230547d4..b4e05f56a 100644 --- a/src/warnet/users.py +++ b/src/warnet/users.py @@ -2,25 +2,38 @@ import sys import click -import yaml from warnet.constants import KUBECONFIG +from warnet.k8s import K8sError, open_kubeconfig, write_kubeconfig @click.command() @click.argument("auth_config", type=str) def auth(auth_config): """Authenticate with a Warnet cluster using a kubernetes config file""" - # TODO: use os.replace for more atomic file writing - auth_config = yaml_try_with_open(auth_config) + try: + auth_config = open_kubeconfig(auth_config) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {auth_config}", fg="red") + sys.exit(1) is_first_config = False if not os.path.exists(KUBECONFIG): - with open(KUBECONFIG, "w") as file: - yaml.safe_dump(auth_config, file) + try: + write_kubeconfig(auth_config) is_first_config = True + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) - base_config = yaml_try_with_open(KUBECONFIG) + try: + base_config = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) if not is_first_config: clusters = "clusters" @@ -53,20 +66,19 @@ def auth(auth_config): ) try: - with open(KUBECONFIG, "w") as file: - yaml.safe_dump(base_config, file) - click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") - except OSError as e: - click.secho(f"Error writing to {KUBECONFIG}: {e}", fg="red") + write_kubeconfig(base_config) + click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") sys.exit(1) try: - with open(KUBECONFIG) as file: - contents = yaml.safe_load(file) - click.secho( - f"Warnet's current context is now set to: {contents['current-context']}", fg="green" - ) - except (OSError, yaml.YAMLError) as e: + base_config = open_kubeconfig(KUBECONFIG) + click.secho( + f"Warnet's current context is now set to: {base_config['current-context']}", fg="green" + ) + except K8sError as e: click.secho(f"Error reading from {KUBECONFIG}: {e}", fg="red") sys.exit(1) @@ -86,18 +98,3 @@ def merge_entries(base_list, auth_list, key, entry_type): else: base_list.append(entry) click.secho(f"Added new {entry_type} '{entry[key]}'", fg="green") - - -def yaml_try_with_open(filename: str): - try: - with open(filename) as f: - return yaml.safe_load(f) - except FileNotFoundError: - click.secho(f"Could not find: {KUBECONFIG}", fg="red") - sys.exit(1) - except OSError as e: - click.secho(f"An I/O error occurred: {e}", fg="red") - sys.exit(1) - except Exception as e: - click.secho(f"An unexpected error occurred: {e}", fg="red") - sys.exit(1) From a5cb478dd6ff5f3a1de65412b30da3e1d25afb13 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 22:53:54 -0500 Subject: [PATCH 42/55] `docs`: minor doc updates --- docs/connecting-local-nodes.md | 3 +++ docs/install.md | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/connecting-local-nodes.md b/docs/connecting-local-nodes.md index 89bade682..f1744f4ad 100644 --- a/docs/connecting-local-nodes.md +++ b/docs/connecting-local-nodes.md @@ -4,6 +4,9 @@ [Telepresence](https://github.com/telepresenceio/telepresence) can be used to make a connection from the cluster to your local machine. Telepresence is designed to intercept cluster commmunication and forward it to your local machine so we will have to install a dummy pod and service to receive the traffic that will get forwarded. +> [!NOTE] +> This guide uses `kubectl` which is available from the [Kubernetes website](https://kubernetes.io/docs/tasks/tools/). + ### Run Warnet network ```shell diff --git a/docs/install.md b/docs/install.md index c7ef14c69..61dc24e98 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,12 +1,12 @@ # Installing Warnet -Warnet requires Kubernetes (k8s) and helm in order to run the network. Kubernetes can be run remotely or locally (with minikube or Docker Desktop). `kubectl` and `helm` must be run locally to administer the network. +Warnet requires Kubernetes (k8s) and Helm in order to run the network. Kubernetes can be run remotely or locally (with minikube or Docker Desktop). `helm` must be run locally to administer the network. ## Dependencies ### Remote (cloud) cluster -The only two dependencies of Warnet are `helm` and `kubectl` configured to talk to your cloud cluster. +The only dependency of Warnet is `helm`. ### Running Warnet Locally @@ -30,15 +30,14 @@ minikube start Minikube has a [guide](https://kubernetes.io/docs/tutorials/hello-minikube/) on getting started which could be useful to validate that your minikube is running correctly. -### Testing kubectl and helm +### Testing helm -The following commands should run on both local and remote clusters. Do not proceed unless kubectl and helm are working. +The following commands should run on both local and remote clusters. Do not proceed unless helm is working. ```shell helm repo add examples https://helm.github.io/examples helm install hello examples/hello-world helm list -kubectl get pods helm uninstall hello ``` From 1237d6727e1549dd9fcb2bcc9dea088563a0b7e6 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 15:58:24 -0500 Subject: [PATCH 43/55] `bitcoin`: remove debug_log --- src/warnet/bitcoin.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index c54e86a2e..afd449ce0 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -22,7 +22,6 @@ kexec, pod_log, ) -from .process import run_command @click.group(name="bitcoin") @@ -82,19 +81,6 @@ def _rpc(tank: str, method: str, params: tuple[str, ...]) -> str: return stdout + stderr -@bitcoin.command() -@click.argument("tank", type=str, required=True) -def debug_log(tank: str): - """ - Fetch the Bitcoin Core debug log from - """ - cmd = f"warnet logs {tank}" - try: - print(run_command(cmd)) - except Exception as e: - print(f"{e}") - - @bitcoin.command() @click.argument("pattern", type=str, required=True) @click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) From 84d24c193b4c675276336d85e5999cd47b9f558e Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 15:58:45 -0500 Subject: [PATCH 44/55] `control`: add explicit ns to run --debug --- src/warnet/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 1b387eafb..2ca095238 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -295,7 +295,7 @@ def filter(path): wait_for_pod(name) _logs(pod_name=name, follow=True) print("Deleting pod...") - delete_pod(name) + delete_pod(name, namespace=namespace) @click.command() From dc560e0779adf1299493c9e69dc11539c5592b31 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 15:59:26 -0500 Subject: [PATCH 45/55] `control`: remove contrived get_default_namespace This use of get_default_namespace relies on the brittle idea that get_pods will always use get_default_namespace. Currently, get_pods currently does, in fact, use get_default_namespace. However, I don't like that we use get_default_namespace out of band relative to get_pods. I'd prefer that we do not mention namespaces at all in our message to users unless we can prove it. --- src/warnet/control.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 2ca095238..d44c61a6e 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -307,18 +307,16 @@ def logs(pod_name: str, follow: bool): def _logs(pod_name: str, follow: bool): - namespace = get_default_namespace() - if pod_name == "": try: pods = get_pods() pod_list = [item.metadata.name for item in pods.items] except Exception as e: - print(f"Could not fetch any pods in namespace {namespace}: {e}") + print(f"Could not fetch any pods: {e}") return if not pod_list: - print(f"Could not fetch any pods in namespace {namespace}") + print("Could not fetch any pods") return q = [ From 9d20ef8b616d3c7e7c7b0f5371cb1548f3ecd7b0 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 16:02:46 -0500 Subject: [PATCH 46/55] `k8s`: use our internal get_pod fn --- src/warnet/k8s.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 6902c8472..0eeb593e2 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -87,8 +87,7 @@ def get_mission(mission: str) -> list[V1Pod]: def get_pod_exit_status(pod_name): try: - sclient = get_static_client() - pod = sclient.read_namespaced_pod(name=pod_name, namespace=get_default_namespace()) + pod = get_pod(pod_name) for container_status in pod.status.container_statuses: if container_status.state.terminated: return container_status.state.terminated.exit_code From c3ad1c0de374b0da6fb0af6dbe6ea5e132254016 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 16:03:12 -0500 Subject: [PATCH 47/55] `k8s`: use default namespace for `edges()` --- src/warnet/k8s.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 0eeb593e2..9ec195108 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -98,8 +98,9 @@ def get_pod_exit_status(pod_name): def get_edges() -> any: + namespace = get_default_namespace() sclient = get_static_client() - configmap = sclient.read_namespaced_config_map(name="edges", namespace="warnet") + configmap = sclient.read_namespaced_config_map(name="edges", namespace=namespace) return json.loads(configmap.data["data"]) From 6d49cafe707cb7dc1a02900caa08dee806095896 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 16:10:01 -0500 Subject: [PATCH 48/55] DEFAULT_NAMESPACE: set it to "default" --- src/warnet/constants.py | 2 +- src/warnet/k8s.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 99bdf2c5c..245c53def 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -11,7 +11,7 @@ tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1) ] -DEFAULT_NAMESPACE = "warnet" +DEFAULT_NAMESPACE = "default" LOGGING_NAMESPACE = "warnet-logging" INGRESS_NAMESPACE = "ingress" HELM_COMMAND = "helm upgrade --install --create-namespace" diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 9ec195108..fca03b55f 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -17,6 +17,7 @@ from .constants import ( CADDY_INGRESS_NAME, + DEFAULT_NAMESPACE, INGRESS_NAMESPACE, KUBECONFIG, LOGGING_NAMESPACE, @@ -171,8 +172,7 @@ def get_default_namespace() -> str: except K8sError as e: raise K8sError(f"Could not get context entry for {KUBECONFIG}") from e - # TODO: need to settle on Warnet's "default" namespace - namespace = context_entry["context"].get("namespace", "warnet") + namespace = context_entry["context"].get("namespace", DEFAULT_NAMESPACE) return namespace From 006dcc600461450ba26eea0c6574051055e20f8c Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 16:12:34 -0500 Subject: [PATCH 49/55] `k8s`: use default namespace in apply fn --- src/warnet/k8s.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index fca03b55f..a3c9fad96 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -178,10 +178,11 @@ def get_default_namespace() -> str: def apply_kubernetes_yaml(yaml_file: str) -> bool: + namespace = get_default_namespace() v1 = get_static_client() path = os.path.abspath(yaml_file) try: - create_from_yaml(v1, path) + create_from_yaml(v1, path, namespace=namespace) return True except Exception as e: raise e From fcc9ed82d04056bc666358dfbd9aafa1b7107cb1 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 26 Sep 2024 09:29:11 -0500 Subject: [PATCH 50/55] `debug_log`: bring it back --- src/warnet/bitcoin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index afd449ce0..c54e86a2e 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -22,6 +22,7 @@ kexec, pod_log, ) +from .process import run_command @click.group(name="bitcoin") @@ -81,6 +82,19 @@ def _rpc(tank: str, method: str, params: tuple[str, ...]) -> str: return stdout + stderr +@bitcoin.command() +@click.argument("tank", type=str, required=True) +def debug_log(tank: str): + """ + Fetch the Bitcoin Core debug log from + """ + cmd = f"warnet logs {tank}" + try: + print(run_command(cmd)) + except Exception as e: + print(f"{e}") + + @bitcoin.command() @click.argument("pattern", type=str, required=True) @click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) From a2a69f30f80c08a70f018d2d98866cfedd11d24b Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 15:28:39 -0500 Subject: [PATCH 51/55] make ruff happy --- src/warnet/bitcoin.py | 3 +-- src/warnet/k8s.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index c54e86a2e..3750b3bf6 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -8,10 +8,9 @@ import click from kubernetes.stream import stream -from urllib3.exceptions import MaxRetryError - from test_framework.messages import ser_uint256 from test_framework.p2p import MESSAGEMAP +from urllib3.exceptions import MaxRetryError from .k8s import ( get_default_namespace, diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index a3c9fad96..afb3d1204 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -472,6 +472,7 @@ def write_file_to_container(pod_name, container_name, dst_path, data): except Exception as e: print(f"Failed to copy data to {pod_name}({container_name}):{dst_path}:\n{e}") + def open_kubeconfig(kubeconfig_path: str = KUBECONFIG) -> dict: try: with open(kubeconfig_path) as file: From 81314567219f011f249ffa9cb20d1ae760e9990a Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 13:33:41 -0500 Subject: [PATCH 52/55] k8s: add `continue` to `wait_for_init` --- src/warnet/k8s.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index afb3d1204..669ef1c51 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -392,6 +392,8 @@ def wait_for_init(pod_name, timeout=300): ): pod = event["object"] if pod.metadata.name == pod_name: + if not pod.status.init_container_statuses: + continue for init_container_status in pod.status.init_container_statuses: if init_container_status.state.running: print(f"initContainer in pod {pod_name} is ready") From df18670e4f213a9c54b2a3c01b15a1f3d839fd4e Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 2 Oct 2024 11:00:24 -0500 Subject: [PATCH 53/55] add constants as they appear in the helm charts --- src/warnet/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 245c53def..a3ffe29a1 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -16,6 +16,9 @@ INGRESS_NAMESPACE = "ingress" HELM_COMMAND = "helm upgrade --install --create-namespace" +BITCOINCORE_CONTAINER = "bitcoincore" +COMMANDER_CONTAINER = "commander" + # Directories and files for non-python assets, e.g., helm charts, example scenarios, default configs SRC_DIR = files("warnet") RESOURCES_DIR = files("resources") From f851e7caaeaade7160e2fdebb270285923bf18f3 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 2 Oct 2024 11:01:05 -0500 Subject: [PATCH 54/55] use constants in the `log` function --- src/warnet/control.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index d44c61a6e..a32c198e0 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -16,7 +16,12 @@ from rich.prompt import Confirm, Prompt from rich.table import Table -from .constants import COMMANDER_CHART, LOGGING_NAMESPACE +from .constants import ( + BITCOINCORE_CONTAINER, + COMMANDER_CHART, + COMMANDER_CONTAINER, + LOGGING_NAMESPACE, +) from .k8s import ( delete_pod, get_default_namespace, @@ -334,11 +339,22 @@ def _logs(pod_name: str, follow: bool): try: pod = get_pod(pod_name) - container_names = [container.name for container in pod.spec.containers] - container_name = container_names[0] + eligible_container_names = [BITCOINCORE_CONTAINER, COMMANDER_CONTAINER] + available_container_names = [container.name for container in pod.spec.containers] + container_name = next( + ( + container_name + for container_name in available_container_names + if container_name in eligible_container_names + ), + None, + ) + if not container_name: + print("Could not determine primary container.") + return except Exception as e: - print(f"Could not determine primary container: {e}") - container_name = None + print(f"Error getting pods. Could not determine primary container: {e}") + return try: stream = pod_log(pod_name, container_name=container_name, follow=follow) From f1249c151bfb09b625b231ec8f8d6f5759beef38 Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 2 Oct 2024 11:01:29 -0500 Subject: [PATCH 55/55] use constant in the bitcoin `rpc` function --- src/warnet/bitcoin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 3750b3bf6..08de7423d 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -12,6 +12,7 @@ from test_framework.p2p import MESSAGEMAP from urllib3.exceptions import MaxRetryError +from .constants import BITCOINCORE_CONTAINER from .k8s import ( get_default_namespace, get_mission, @@ -60,7 +61,7 @@ def _rpc(tank: str, method: str, params: tuple[str, ...]) -> str: sclient.connect_get_namespaced_pod_exec, tank, namespace, - container="bitcoincore", + container=BITCOINCORE_CONTAINER, command=cmd, stderr=True, stdin=False,