From 56bc1c3c8051b4ca324eabccdfa94aeba21fd71b Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:17:19 -0500 Subject: [PATCH 01/76] constants: make the default namespace "default" The general agreement we came to was to use "default" as the default namespace. I took a moment to search the code base for instances of hard-coded "warnet" namespaces, and I could only really find one. It was in the commander file -- specifically in the network flag area. I updated this in another commit. --- src/warnet/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 3bf666007..a3ffe29a1 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" From c848848d6809050440cec11a9d1ba73a26ddec9a Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:44:25 -0500 Subject: [PATCH 02/76] constants: add `wargames` prefix Until we start using labels in earnest for labeling and querying namespaces, this prefix will have to suffice for our labeling and querying needs. --- src/warnet/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index a3ffe29a1..883211a33 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -14,6 +14,7 @@ DEFAULT_NAMESPACE = "default" LOGGING_NAMESPACE = "warnet-logging" INGRESS_NAMESPACE = "ingress" +WARGAMES_NAMESPACE_PREFIX = "wargames-" HELM_COMMAND = "helm upgrade --install --create-namespace" BITCOINCORE_CONTAINER = "bitcoincore" From 39db0b557adc60f8059be09f2562f71444002378 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 18:07:34 -0500 Subject: [PATCH 03/76] constants: add k8s internal namespaces We want to know the namespaces used internally by k8s so that we can filter them out when querying namespaces. We can also likely replace this logic after we create a labeling strategy for our namespaces. --- src/warnet/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 883211a33..3ecddedb6 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -15,6 +15,7 @@ LOGGING_NAMESPACE = "warnet-logging" INGRESS_NAMESPACE = "ingress" WARGAMES_NAMESPACE_PREFIX = "wargames-" +KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] HELM_COMMAND = "helm upgrade --install --create-namespace" BITCOINCORE_CONTAINER = "bitcoincore" From 4683dc96c96f77adc1241a3a4d57abec73840cd4 Mon Sep 17 00:00:00 2001 From: josibake Date: Fri, 13 Sep 2024 13:08:31 +0200 Subject: [PATCH 04/76] refactor: namespaces.yaml, namespace-defaults.yaml namespaces.yaml is meant for describing the overall structure of what you want with specific overrides for specific users as needed. the "default" roles should be defined in namespace-defaults.yaml so that they are automatically applied by default for each user in each namespace. at a lower level, defaults that should be applied by default for *any* namespaces deployment should be defined in values.yaml. namespace-defaults.yaml is meant to override values.yaml in the event for a particular namespaces deployment the admin wants to create tailor made roles and permisssions. otherwise, this can stay empty and whatever is in values.yaml will be applied. update example prefix to wargames, to illustrate this is not relying on a default namespace of warnet. this probably needs some more thought, but I think its best to address how to pipe through the name in a followup rather than slow this PR down. --- .../namespace-defaults.yaml | 24 +++--- .../two_namespaces_two_users/namespaces.yaml | 74 +------------------ 2 files changed, 15 insertions(+), 83 deletions(-) diff --git a/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml b/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml index 91ac2fc67..75cc8e42c 100644 --- a/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml +++ b/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml @@ -3,14 +3,16 @@ users: roles: - pod-viewer - pod-manager -roles: - - name: pod-viewer - rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] - - name: pod-manager - rules: - - apiGroups: [""] - resources: ["pods", "configmaps"] - verbs: ["get", "list", "watch", "create", "update", "delete"] +# the pod-viewer and pod-manager roles are the default +# roles defined in values.yaml for the namespaces charts +# +# if you need a different set of roles for a particular namespaces +# deployment, you can override values.yaml by providing your own +# role definitions below +# +# roles: +# - name: my-custom-role +# rules: +# - apiGroups: "" +# resources: "" +# verbs: "" diff --git a/resources/namespaces/two_namespaces_two_users/namespaces.yaml b/resources/namespaces/two_namespaces_two_users/namespaces.yaml index 4172657b8..542456ef6 100644 --- a/resources/namespaces/two_namespaces_two_users/namespaces.yaml +++ b/resources/namespaces/two_namespaces_two_users/namespaces.yaml @@ -1,5 +1,5 @@ namespaces: - - name: warnet-red-team + - name: wargames-red-team users: - name: alice roles: @@ -8,42 +8,7 @@ namespaces: roles: - pod-viewer - pod-manager - roles: - - name: pod-viewer - rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] - verbs: ["get"] - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["events"] - verbs: ["get"] - - name: pod-manager - rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch", "create", "delete", "update"] - - apiGroups: [""] - resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] - verbs: ["get", "create"] - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get", "create"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["events"] - verbs: ["get"] - - name: warnet-blue-team + - name: wargames-blue-team users: - name: mallory roles: @@ -52,38 +17,3 @@ namespaces: roles: - pod-viewer - pod-manager - roles: - - name: pod-viewer - rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] - verbs: ["get"] - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["events"] - verbs: ["get"] - - name: pod-manager - rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch", "create", "delete", "update"] - - apiGroups: [""] - resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] - verbs: ["get", "create"] - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get", "create"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list"] - - apiGroups: [""] - resources: ["events"] - verbs: ["get"] From 305c1891f92806f5cab89ef8fcbf4911db2f4e8d Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 23 Sep 2024 22:25:08 -0500 Subject: [PATCH 05/76] charts: allow users access to services We will need to allow users access to services for launching scenarios. --- resources/charts/namespaces/values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/charts/namespaces/values.yaml b/resources/charts/namespaces/values.yaml index 61f946879..8a884c278 100644 --- a/resources/charts/namespaces/values.yaml +++ b/resources/charts/namespaces/values.yaml @@ -7,14 +7,14 @@ roles: - name: pod-viewer rules: - apiGroups: [""] - resources: ["pods"] + resources: ["pods", "services"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] verbs: ["get"] - apiGroups: [""] resources: ["configmaps", "secrets"] - verbs: ["get"] + verbs: ["get", "list"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list"] @@ -24,14 +24,14 @@ roles: - name: pod-manager rules: - apiGroups: [""] - resources: ["pods"] + resources: ["pods", "services"] verbs: ["get", "list", "watch", "create", "delete", "update"] - apiGroups: [""] resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] verbs: ["get", "create"] - apiGroups: [""] resources: ["configmaps", "secrets"] - verbs: ["get", "create"] + verbs: ["get", "list", "create"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list"] From 61cff4a61c84ec6c117b55920234cf9d672d8071 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:11:13 -0500 Subject: [PATCH 06/76] charts: add namespace to sa permissions --- resources/charts/namespaces/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/charts/namespaces/values.yaml b/resources/charts/namespaces/values.yaml index 8a884c278..e4e95400d 100644 --- a/resources/charts/namespaces/values.yaml +++ b/resources/charts/namespaces/values.yaml @@ -16,7 +16,7 @@ roles: resources: ["configmaps", "secrets"] verbs: ["get", "list"] - apiGroups: [""] - resources: ["persistentvolumeclaims"] + resources: ["persistentvolumeclaims", "namespaces"] verbs: ["get", "list"] - apiGroups: [""] resources: ["events"] @@ -33,7 +33,7 @@ roles: resources: ["configmaps", "secrets"] verbs: ["get", "list", "create"] - apiGroups: [""] - resources: ["persistentvolumeclaims"] + resources: ["persistentvolumeclaims", "namespaces"] verbs: ["get", "list"] - apiGroups: [""] resources: ["events"] From f34113adf25b00556087ca8285e07c84921a09d0 Mon Sep 17 00:00:00 2001 From: josibake Date: Fri, 13 Sep 2024 14:14:10 +0200 Subject: [PATCH 07/76] deploy: allow a namespace parameter in `deploy` Allowing a namespace parameter in deploy means that we can easily launch tanks into a given namespace, and we can safely ignore this parameter in other functions because it defaults to None which the code will handle gracefully. --- src/warnet/deploy.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index c175dd6d9..88f281651 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -2,6 +2,7 @@ import sys import tempfile from pathlib import Path +from typing import Optional import click import yaml @@ -19,6 +20,7 @@ NAMESPACES_CHART_LOCATION, NAMESPACES_FILE, NETWORK_FILE, + WARGAMES_NAMESPACE_PREFIX, ) from .k8s import get_default_namespace, wait_for_ingress_controller, wait_for_pod_ready from .process import stream_command @@ -42,13 +44,14 @@ def validate_directory(ctx, param, value): callback=validate_directory, ) @click.option("--debug", is_flag=True) -def deploy(directory, debug): +@click.option("--namespace", type=str) +def deploy(directory, debug, namespace): """Deploy a warnet with topology loaded from """ directory = Path(directory) if (directory / NETWORK_FILE).exists(): dl = deploy_logging_stack(directory, debug) - deploy_network(directory, debug) + deploy_network(directory, debug, namespace=namespace) df = deploy_fork_observer(directory, debug) if dl | df: deploy_ingress(debug) @@ -189,14 +192,15 @@ def deploy_fork_observer(directory: Path, debug: bool) -> bool: return True -def deploy_network(directory: Path, debug: bool = False): +def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str] = None): network_file_path = directory / NETWORK_FILE defaults_file_path = directory / DEFAULTS_FILE with network_file_path.open() as f: network_file = yaml.safe_load(f) - namespace = get_default_namespace() + if not namespace: + namespace = get_default_namespace() for node in network_file["nodes"]: click.echo(f"Deploying node: {node.get('name')}") @@ -237,9 +241,10 @@ def deploy_namespaces(directory: Path): names = [n.get("name") for n in namespaces_file["namespaces"]] for n in names: - if not n.startswith("warnet-"): - click.echo( - f"Failed to create namespace: {n}. Namespaces must start with a 'warnet-' prefix." + if not n.startswith(WARGAMES_NAMESPACE_PREFIX): + click.secho( + f"Failed to create namespace: {n}. Namespaces must start with a '{WARGAMES_NAMESPACE_PREFIX}' prefix.", + fg="red", ) return From bb0e9cb916673aeed46aafeab3fd2bf0c2f49e4a Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 12:35:18 -0500 Subject: [PATCH 08/76] admin: add create_kubeconfigs func --- src/warnet/admin.py | 94 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index f194e16bd..70d48dfa1 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -4,9 +4,11 @@ import click from rich import print as richprint -from .constants import NETWORK_DIR +from .constants import NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX +from .k8s import get_kubeconfig_value, get_namespaces_by_prefix, get_service_accounts_in_namespace from .namespaces import copy_namespaces_defaults, namespaces from .network import copy_network_defaults +from .process import run_command @click.group(name="admin", hidden=True) @@ -33,3 +35,93 @@ def init(): f"[green]Copied network and namespace example files to {Path(current_dir) / NETWORK_DIR.name}[/green]" ) richprint(f"[green]Created warnet project structure in {current_dir}[/green]") + + +@admin.command() +@click.option( + "--kubeconfig-dir", + default="kubeconfigs", + help="Directory to store kubeconfig files (default: kubeconfigs)", +) +@click.option( + "--token-duration", + default=172800, + type=int, + help="Duration of the token in seconds (default: 48 hours)", +) +def create_kubeconfigs(kubeconfig_dir, token_duration): + """Create kubeconfig files for all ServiceAccounts in warnet team namespaces starting with .""" + kubeconfig_dir = os.path.expanduser(kubeconfig_dir) + + cluster_name = get_kubeconfig_value("{.clusters[0].name}") + cluster_server = get_kubeconfig_value("{.clusters[0].cluster.server}") + cluster_ca = get_kubeconfig_value("{.clusters[0].cluster.certificate-authority-data}") + + os.makedirs(kubeconfig_dir, exist_ok=True) + + # Get all namespaces that start with prefix + # This assumes when deploying multiple namespacs for the purpose of team games, all namespaces start with a prefix, + # e.g., tabconf-wargames-*. Currently, this is a bit brittle, but we can improve on this in the future + # by automatically applying a TEAM_PREFIX when creating the get_warnet_namespaces + # TODO: choose a prefix convention and have it managed by the helm charts instead of requiring the + # admin user to pipe through the correct string in multiple places. Another would be to use + # labels instead of namespace naming conventions + warnet_namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + + for v1namespace in warnet_namespaces: + namespace = v1namespace.metadata.name + click.echo(f"Processing namespace: {namespace}") + service_accounts = get_service_accounts_in_namespace(namespace) + + for sa in service_accounts: + # Create a token for the ServiceAccount with specified duration + command = f"kubectl create token {sa} -n {namespace} --duration={token_duration}s" + try: + token = run_command(command) + except Exception as e: + click.echo( + f"Failed to create token for ServiceAccount {sa} in namespace {namespace}. Error: {str(e)}. Skipping..." + ) + continue + + # Create a kubeconfig file for the user + kubeconfig_file = os.path.join(kubeconfig_dir, f"{sa}-{namespace}-kubeconfig") + + # TODO: move yaml out of python code to resources/manifests/ + # + # might not be worth it since we are just reading the yaml to then create a bunch of values and its not + # actually used to deploy anything into the cluster + # Then benefit would be making this code a bit cleaner and easy to follow, fwiw + kubeconfig_content = f"""apiVersion: v1 +kind: Config +clusters: +- name: {cluster_name} + cluster: + server: {cluster_server} + certificate-authority-data: {cluster_ca} +users: +- name: {sa} + user: + token: {token} +contexts: +- name: {sa}-{namespace} + context: + cluster: {cluster_name} + namespace: {namespace} + user: {sa} +current-context: {sa}-{namespace} +""" + with open(kubeconfig_file, "w") as f: + f.write(kubeconfig_content) + + click.echo(f" Created kubeconfig file for {sa}: {kubeconfig_file}") + + click.echo("---") + click.echo( + f"All kubeconfig files have been created in the '{kubeconfig_dir}' directory with a duration of {token_duration} seconds." + ) + click.echo("Distribute these files to the respective users.") + click.echo( + "Users can then use by running `warnet auth ` or with kubectl by specifying the --kubeconfig flag or by setting the KUBECONFIG environment variable." + ) + click.echo(f"Note: The tokens will expire after {token_duration} seconds.") From 542f38aee1103cfbcd9c03fee2b066b2833d8cfc Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 12:30:41 -0500 Subject: [PATCH 09/76] service_accounts: add sa func to admin section --- src/warnet/admin.py | 2 ++ src/warnet/service_accounts.py | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/warnet/service_accounts.py diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 70d48dfa1..5f2233cec 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -9,6 +9,7 @@ from .namespaces import copy_namespaces_defaults, namespaces from .network import copy_network_defaults from .process import run_command +from .service_accounts import service_accounts @click.group(name="admin", hidden=True) @@ -18,6 +19,7 @@ def admin(): admin.add_command(namespaces) +admin.add_command(service_accounts) @admin.command() diff --git a/src/warnet/service_accounts.py b/src/warnet/service_accounts.py new file mode 100644 index 000000000..e32b4acb2 --- /dev/null +++ b/src/warnet/service_accounts.py @@ -0,0 +1,38 @@ +import click + +from .constants import ( + WARGAMES_NAMESPACE_PREFIX, +) +from .k8s import get_static_client + + +@click.group(name="service-accounts") +def service_accounts(): + """Service account commands""" + + +@service_accounts.command() +def list(): + """List all service accounts with 'wargames-' prefix""" + # Load the kubeconfig file + sclient = get_static_client() + namespaces = sclient.list_namespace().items + + filtered_namespaces = [ + ns.metadata.name + for ns in namespaces + if ns.metadata.name.startswith(WARGAMES_NAMESPACE_PREFIX) + ] + + if len(filtered_namespaces) == 0: + click.secho("Could not find any matching service accounts.", fg="yellow") + + for namespace in filtered_namespaces: + click.secho(f"Service accounts in namespace: {namespace}") + service_accounts = sclient.list_namespaced_service_account(namespace=namespace).items + + if len(service_accounts) == 0: + click.secho("...Could not find any matching service accounts", fg="yellow") + + for sa in service_accounts: + click.secho(f"- {sa.metadata.name}", fg="green") From 4518cc6854c911974a7fab489c4507ec45747a90 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 23 Sep 2024 22:21:43 -0500 Subject: [PATCH 10/76] namespaces: remove the dir from the `namespaces` I don't think the dir argument is necessary because we are just querying the cluster. --- src/warnet/namespaces.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/warnet/namespaces.py b/src/warnet/namespaces.py index 45bcb7af5..5ec3bd837 100644 --- a/src/warnet/namespaces.py +++ b/src/warnet/namespaces.py @@ -32,9 +32,6 @@ 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""" From 714eff067f38e43de750bbac13570286f2766343 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 30 Sep 2024 01:45:05 -0500 Subject: [PATCH 11/76] namespaces: flesh out wargames prefix --- src/warnet/namespaces.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/warnet/namespaces.py b/src/warnet/namespaces.py index 5ec3bd837..12357525b 100644 --- a/src/warnet/namespaces.py +++ b/src/warnet/namespaces.py @@ -8,6 +8,7 @@ DEFAULTS_NAMESPACE_FILE, NAMESPACES_DIR, NAMESPACES_FILE, + WARGAMES_NAMESPACE_PREFIX, ) from .process import run_command, stream_command @@ -34,11 +35,13 @@ def namespaces(): @namespaces.command() def list(): - """List all namespaces with 'warnet-' prefix""" + """List all namespaces with 'wargames-' 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-")] + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] if warnet_namespaces: print("Warnet namespaces:") @@ -52,14 +55,16 @@ def list(): @click.option("--all", "destroy_all", is_flag=True, help="Destroy all warnet- prefixed namespaces") @click.argument("namespace", required=False) def destroy(destroy_all: bool, namespace: str): - """Destroy a specific namespace or all warnet- prefixed namespaces""" + """Destroy a specific namespace or all 'wargames-' prefixed namespaces""" 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-")] + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] if not warnet_namespaces: print("No warnet namespaces found to destroy.") @@ -72,8 +77,8 @@ def destroy(destroy_all: bool, namespace: str): else: print(f"Destroyed namespace: {ns}") elif namespace: - if not namespace.startswith("warnet-"): - print("Error: Can only destroy namespaces with 'warnet-' prefix") + if not namespace.startswith(f"{WARGAMES_NAMESPACE_PREFIX}"): + print(f"Error: Can only destroy namespaces with '{WARGAMES_NAMESPACE_PREFIX}' prefix") return destroy_cmd = f"kubectl delete namespace {namespace}" From 2bcf737464536349283dad7a4089e85d372b3af8 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:12:25 -0500 Subject: [PATCH 12/76] status: add namespace to `status` --- src/warnet/status.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/warnet/status.py b/src/warnet/status.py index ebbd245d4..df62ed2df 100644 --- a/src/warnet/status.py +++ b/src/warnet/status.py @@ -45,10 +45,11 @@ def status(): table.add_column("Component", style="cyan") table.add_column("Name", style="green") table.add_column("Status", style="yellow") + table.add_column("Namespace", style="green") # Add tanks to the table for tank in tanks: - table.add_row("Tank", tank["name"], tank["status"]) + table.add_row("Tank", tank["name"], tank["status"], tank["namespace"]) # Add a separator if there are both tanks and scenarios if tanks and scenarios: @@ -58,7 +59,7 @@ def status(): active = 0 if scenarios: for scenario in scenarios: - table.add_row("Scenario", scenario["name"], scenario["status"]) + table.add_row("Scenario", scenario["name"], scenario["status"], scenario["namespace"]) if scenario["status"] == "running" or scenario["status"] == "pending": active += 1 else: @@ -86,9 +87,23 @@ def status(): def _get_tank_status(): tanks = get_mission("tank") - return [{"name": tank.metadata.name, "status": tank.status.phase.lower()} for tank in tanks] + return [ + { + "name": tank.metadata.name, + "status": tank.status.phase.lower(), + "namespace": tank.metadata.namespace, + } + for tank in tanks + ] def _get_deployed_scenarios(): commanders = get_mission("commander") - return [{"name": c.metadata.name, "status": c.status.phase.lower()} for c in commanders] + return [ + { + "name": c.metadata.name, + "status": c.status.phase.lower(), + "namespace": c.metadata.namespace, + } + for c in commanders + ] From 527e583fe296e522b4f0c1210ce553a9a03114ed Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 30 Sep 2024 01:49:21 -0500 Subject: [PATCH 13/76] workflow: add test to git workflow --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a50534ff..acac68266 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: - services_test.py - signet_test.py - scenarios_test.py + - namespace_admin_test.py steps: - uses: actions/checkout@v4 - uses: azure/setup-helm@v4.2.0 From 997ca62df84ad32e6d632cbfad36ed88c5387bea Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 12:34:00 -0500 Subject: [PATCH 14/76] k8s: add ns, sa, and config helper funcs --- src/warnet/k8s.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 589169e24..6cfc0c19c 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -359,3 +359,25 @@ 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 get_kubeconfig_value(jsonpath): + command = f"kubectl config view --minify -o jsonpath={jsonpath}" + return run_command(command) + + +def get_namespaces_by_prefix(prefix: str): + """ + Get all namespaces beginning with `prefix`. Returns empty list of no namespaces with the specified prefix are found. + """ + command = "kubectl get namespaces -o jsonpath={.items[*].metadata.name}" + namespaces = run_command(command).split() + return [ns for ns in namespaces if ns.startswith(prefix)] + + +def get_service_accounts_in_namespace(namespace): + """ + Get all service accounts in a namespace. Returns an empty list if no service accounts are found in the specified namespace. + """ + command = f"kubectl get serviceaccounts -n {namespace} -o jsonpath={{.items[*].metadata.name}}" + # skip the default service account created by k8s + service_accounts = run_command(command).split() + return [sa for sa in service_accounts if sa != "default"] From fc7797137dea10cf4514bb91b0ce64ca488310cc Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:19:04 -0500 Subject: [PATCH 15/76] k8s: fix static_client type --- src/warnet/k8s.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 6cfc0c19c..b45c3d2a1 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -8,7 +8,8 @@ import yaml from kubernetes import client, config, watch -from kubernetes.client.models import CoreV1Event, V1Pod, V1PodList +from kubernetes.client import CoreV1Api +from kubernetes.client.models import V1Pod, V1PodList from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -23,7 +24,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 ab42fc341ab96da47f6e2f28a08250cc8d986009 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:45:46 -0500 Subject: [PATCH 16/76] k8s: update getting namespace logic --- src/warnet/k8s.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index b45c3d2a1..865011860 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -9,7 +9,7 @@ import yaml from kubernetes import client, config, watch from kubernetes.client import CoreV1Api -from kubernetes.client.models import V1Pod, V1PodList +from kubernetes.client.models import V1Namespace, V1Pod, V1PodList from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -360,18 +360,32 @@ 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 get_kubeconfig_value(jsonpath): command = f"kubectl config view --minify -o jsonpath={jsonpath}" return run_command(command) -def get_namespaces_by_prefix(prefix: str): +def get_namespaces() -> list[V1Namespace]: + sclient = get_static_client() + try: + return sclient.list_namespace().items + + except ApiException as e: + if e.status == 403: + ns = sclient.read_namespace(name=get_default_namespace()) + return [ns] + else: + return [] + + +def get_namespaces_by_prefix(prefix: str) -> list[V1Namespace]: """ Get all namespaces beginning with `prefix`. Returns empty list of no namespaces with the specified prefix are found. """ - command = "kubectl get namespaces -o jsonpath={.items[*].metadata.name}" - namespaces = run_command(command).split() - return [ns for ns in namespaces if ns.startswith(prefix)] + namespaces = get_namespaces() + return [ns for ns in namespaces if ns.metadata.name.startswith(prefix)] def get_service_accounts_in_namespace(namespace): From 0a2bd1a927501a7cb4d096f6cbf5c5298f0cd284 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 18:08:50 -0500 Subject: [PATCH 17/76] k8s: ignore internal namespaces --- src/warnet/k8s.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 865011860..6447f1b79 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -18,6 +18,7 @@ CADDY_INGRESS_NAME, DEFAULT_NAMESPACE, INGRESS_NAMESPACE, + KUBE_INTERNAL_NAMESPACES, KUBECONFIG, LOGGING_NAMESPACE, ) @@ -370,7 +371,11 @@ def get_kubeconfig_value(jsonpath): def get_namespaces() -> list[V1Namespace]: sclient = get_static_client() try: - return sclient.list_namespace().items + return [ + ns + for ns in sclient.list_namespace().items + if ns.metadata.name not in KUBE_INTERNAL_NAMESPACES + ] except ApiException as e: if e.status == 403: From ffdfec27bb71eba19caeee4bef6518e40ee4293c Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 30 Sep 2024 22:34:42 -0500 Subject: [PATCH 18/76] k8s & control: query all namespaces This expands `get_pods` view to query all namespaces. It also flattens out list[V1PodList] into just list[V1Pod]. This affected two downstream functions: `down` and `_logs`. I updated them accordingly. --- src/warnet/control.py | 4 ++-- src/warnet/k8s.py | 24 +++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 43c895cfb..7fd12b4dd 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -144,7 +144,7 @@ def delete_pod(pod_name, namespace): # Delete remaining pods pods = get_pods() - for pod in pods.items: + for pod in pods: futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) # Wait for all tasks to complete and print results @@ -312,7 +312,7 @@ def _logs(pod_name: str, follow: bool): if pod_name == "": try: pods = get_pods() - pod_list = [item.metadata.name for item in pods.items] + pod_list = [item.metadata.name for item in pods] except Exception as e: print(f"Could not fetch any pods in namespace {namespace}: {e}") return diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 6447f1b79..04cd5549a 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -35,13 +35,19 @@ def get_dynamic_client() -> DynamicClient: return DynamicClient(client.ApiClient()) -def get_pods() -> V1PodList: +def get_pods() -> list[V1Pod]: sclient = get_static_client() - try: - pod_list: V1PodList = sclient.list_namespaced_pod(get_default_namespace()) - except Exception as e: - raise e - return pod_list + pods: list[V1Pod] = [] + namespaces = get_namespaces() + for ns in namespaces: + namespace = ns.metadata.name + try: + pod_list: V1PodList = sclient.list_namespaced_pod(namespace) + for pod in pod_list.items: + pods.append(pod) + except Exception as e: + raise e + return pods def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: @@ -51,10 +57,10 @@ def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: return sclient.read_namespaced_pod(name=name, namespace=namespace) -def get_mission(mission: str) -> list[V1PodList]: +def get_mission(mission: str) -> list[V1Pod]: pods = get_pods() - crew = [] - for pod in pods.items: + crew: list[V1Pod] = [] + for pod in pods: if "mission" in pod.metadata.labels and pod.metadata.labels["mission"] == mission: crew.append(pod) return crew From b9ebff96869cfae84f4b1bf7300803ebc5b41503 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 00:58:26 -0500 Subject: [PATCH 19/76] k8s: add optional ns to get_pod_exit_stats --- src/warnet/k8s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 04cd5549a..b4d29e95b 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -66,10 +66,12 @@ def get_mission(mission: str) -> list[V1Pod]: return crew -def get_pod_exit_status(pod_name): +def get_pod_exit_status(pod_name, namespace: Optional[str] = None): + if not namespace: + namespace = get_default_namespace() try: sclient = get_static_client() - pod = sclient.read_namespaced_pod(name=pod_name, namespace=get_default_namespace()) + pod = sclient.read_namespaced_pod(name=pod_name, namespace=namespace) for container_status in pod.status.container_statuses: if container_status.state.terminated: return container_status.state.terminated.exit_code From a22cea8909be8241f126e4ac7d8676ac04c0e748 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 00:59:04 -0500 Subject: [PATCH 20/76] k8s: add optional ns to delete_pod --- src/warnet/k8s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index b4d29e95b..c81fc0481 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -135,8 +135,10 @@ def delete_namespace(namespace: str) -> bool: return run_command(command) -def delete_pod(pod_name: str) -> bool: - command = f"kubectl -n {get_default_namespace()} delete pod {pod_name}" +def delete_pod(pod_name: str, namespace: Optional[str] = None) -> bool: + if not namespace: + namespace = get_default_namespace() + command = f"kubectl -n {namespace} delete pod {pod_name}" return stream_command(command) From e2aeecda85269beb2cdf4a4d6a9ad42a525ccede Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 00:59:31 -0500 Subject: [PATCH 21/76] k8s: add optional ns to get_edges --- src/warnet/k8s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index c81fc0481..82ff17ea9 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -81,9 +81,11 @@ def get_pod_exit_status(pod_name, namespace: Optional[str] = None): return None -def get_edges() -> any: +def get_edges(namespace: Optional[str] = None) -> any: + if not namespace: + 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 1a2541809473dfb56bcc3b5a524da8332a68a798 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 00:59:51 -0500 Subject: [PATCH 22/76] k8s: add optional ns to snapshot_bitcoin_datadir --- 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 82ff17ea9..35e5e4148 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -160,9 +160,14 @@ def get_default_namespace() -> str: def snapshot_bitcoin_datadir( - pod_name: str, chain: str, local_path: str = "./", filters: list[str] = None + pod_name: str, + chain: str, + local_path: str = "./", + filters: list[str] = None, + namespace: Optional[str] = None, ) -> None: - namespace = get_default_namespace() + if not namespace: + namespace = get_default_namespace() sclient = get_static_client() try: From ac113880c50550d9b8141dea6411506a4949a62d Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:00:15 -0500 Subject: [PATCH 23/76] k8s: add optional ns to wait_for_init --- src/warnet/k8s.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 35e5e4148..ff496d151 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -291,9 +291,10 @@ def wait_for_pod_ready(name, namespace, timeout=300): return False -def wait_for_init(pod_name, timeout=300): +def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None): + if not namespace: + namespace = get_default_namespace() sclient = get_static_client() - namespace = get_default_namespace() w = watch.Watch() for event in w.stream( sclient.list_namespaced_pod, namespace=namespace, timeout_seconds=timeout From 9c0f82cfc144a6e9bb4f2a54b41e993d6568c96f Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:00:29 -0500 Subject: [PATCH 24/76] k8s: add optional ns to pod_log --- src/warnet/k8s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index ff496d151..75a3c0a6d 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -332,12 +332,14 @@ 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, namespace: Optional[str] = None): sclient = get_static_client() + if not namespace: + namespace = get_default_namespace() try: return sclient.read_namespaced_pod_log( name=pod_name, - namespace=get_default_namespace(), + namespace=namespace, container=container_name, follow=follow, _preload_content=False, From f1d8d5c04733f52b89c72af7b7944f7b450616a5 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:00:43 -0500 Subject: [PATCH 25/76] k8s: add optional ns to wait_for_pod --- src/warnet/k8s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 75a3c0a6d..6f48adc99 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -348,10 +348,12 @@ def pod_log(pod_name, container_name=None, follow=False, namespace: Optional[str raise Exception(json.loads(e.body.decode("utf-8"))["message"]) from None -def wait_for_pod(pod_name, timeout_seconds=10): +def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): + if not namespace: + namespace = get_default_namespace() sclient = get_static_client() while timeout_seconds > 0: - pod = sclient.read_namespaced_pod_status(name=pod_name, namespace=get_default_namespace()) + pod = sclient.read_namespaced_pod_status(name=pod_name, namespace=namespace) if pod.status.phase != "Pending": return sleep(1) From 1593cfe2164c26fd9032a7bff5c1b973f0889e86 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:00:57 -0500 Subject: [PATCH 26/76] k8s: add optional ns to write_file_to_container --- src/warnet/k8s.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 6f48adc99..53f3ecb14 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -360,9 +360,12 @@ def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): timeout_seconds -= 1 -def write_file_to_container(pod_name, container_name, dst_path, data): +def write_file_to_container( + pod_name, container_name, dst_path, data, namespace: Optional[str] = None +): + if not namespace: + namespace = get_default_namespace() sclient = get_static_client() - namespace = get_default_namespace() exec_command = ["sh", "-c", f"cat > {dst_path}"] try: res = stream( From 402ee83487f6805aa340830b277be0c2261bfe57 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 12:38:29 -0500 Subject: [PATCH 27/76] k8s: add namespace to `wait_for_init` --- src/warnet/k8s.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 53f3ecb14..40dabe292 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -303,10 +303,10 @@ def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None): if pod.metadata.name == pod_name: 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") + print(f"initContainer in pod {pod_name} ({namespace}) is ready") w.stop() return True - print(f"Timeout waiting for initContainer in {pod_name} to be ready.") + print(f"Timeout waiting for initContainer in {pod_name} ({namespace})to be ready.") return False From 1ecdcdf9ab349523187e2d3468101f3b64aa0c1f Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:03:27 -0500 Subject: [PATCH 28/76] network: add namespace to `network` `_connected` --- src/warnet/network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/warnet/network.py b/src/warnet/network.py index 401ab5106..a894cafc9 100644 --- a/src/warnet/network.py +++ b/src/warnet/network.py @@ -58,7 +58,9 @@ def _connected(end="\n"): for tank in tanks: # Get actual try: - peerinfo = json.loads(_rpc(tank.metadata.name, "getpeerinfo", "")) + peerinfo = json.loads( + _rpc(tank.metadata.name, "getpeerinfo", "", namespace=tank.metadata.namespace) + ) actual = 0 for peer in peerinfo: if is_connection_manual(peer): From 6242838d1410e9c05eaaa50e9984ce0559e0878d Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 02:50:54 -0500 Subject: [PATCH 29/76] control: use namespace log in `down` --- src/warnet/control.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index 7fd12b4dd..e615764e1 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -20,12 +20,12 @@ BITCOINCORE_CONTAINER, COMMANDER_CHART, COMMANDER_CONTAINER, - LOGGING_NAMESPACE, ) from .k8s import ( delete_pod, get_default_namespace, get_mission, + get_namespaces, get_pod, get_pods, pod_log, @@ -118,8 +118,6 @@ def down(): """Bring down a running warnet quickly""" console.print("[bold yellow]Bringing down the warnet...[/bold yellow]") - namespaces = [get_default_namespace(), LOGGING_NAMESPACE] - def uninstall_release(namespace, release_name): cmd = f"helm uninstall {release_name} --namespace {namespace} --wait=false" subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -134,13 +132,15 @@ def delete_pod(pod_name, namespace): futures = [] # Uninstall Helm releases - for namespace in namespaces: - command = f"helm list --namespace {namespace} -o json" + for namespace in get_namespaces(): + command = f"helm list --namespace {namespace.metadata.name} -o json" result = run_command(command) if result: releases = json.loads(result) for release in releases: - futures.append(executor.submit(uninstall_release, namespace, release["name"])) + futures.append( + executor.submit(uninstall_release, namespace.metadata.name, release["name"]) + ) # Delete remaining pods pods = get_pods() From 76897d39e1d1c90fc61479a3670d0a40e1a1a74a Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 18:09:21 -0500 Subject: [PATCH 30/76] control & test base: prevent hasty `down`s This does a check to ensure users don't nuke the network by entering `warnet down`. It will prompt the users if there are multiple namespaces affected. Also adds a `--force` option to bypass this. --- src/warnet/control.py | 61 +++++++++++++++++++++++++++++++++++-------- test/test_base.py | 2 +- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index e615764e1..e6092c8e5 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -113,10 +113,15 @@ def stop_all_scenarios(scenarios): console.print("[bold green]All scenarios have been stopped.[/bold green]") +@click.option( + "--force", + is_flag=True, + default=False, + help="Skip confirmations", +) @click.command() -def down(): +def down(force): """Bring down a running warnet quickly""" - console.print("[bold yellow]Bringing down the warnet...[/bold yellow]") def uninstall_release(namespace, release_name): cmd = f"helm uninstall {release_name} --namespace {namespace} --wait=false" @@ -128,19 +133,53 @@ def delete_pod(pod_name, namespace): subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" + namespaces = get_namespaces() + print(namespaces) + release_list: list[dict[str, str]] = [] + for namespace in namespaces: + command = f"helm list --namespace {namespace.metadata.name} -o json" + result = run_command(command) + if result: + releases = json.loads(result) + for release in releases: + release_list.append({"namespace": namespace.metadata.name, "name": release["name"]}) + + if not force: + affected_namespaces = set([entry["namespace"] for entry in release_list]) + namespace_listing = "\n ".join(affected_namespaces) + confirmed = "confirmed" + click.secho("Preparing to bring down the running Warnet...", fg="yellow") + click.secho("The listed namespaces will be affected:", fg="yellow") + click.secho(f" {namespace_listing}", fg="blue") + + proj_answers = inquirer.prompt( + [ + inquirer.Confirm( + confirmed, + message=click.style( + "Do you want to bring down the running Warnet?", fg="yellow", bold=False + ), + default=False, + ), + ] + ) + if not proj_answers: + click.secho("Operation cancelled by user.", fg="yellow") + sys.exit(0) + if proj_answers[confirmed]: + click.secho("Bringing down the warnet...", fg="yellow") + else: + click.secho("Operation cancelled by user", fg="yellow") + sys.exit(0) + with ThreadPoolExecutor(max_workers=10) as executor: futures = [] # Uninstall Helm releases - for namespace in get_namespaces(): - command = f"helm list --namespace {namespace.metadata.name} -o json" - result = run_command(command) - if result: - releases = json.loads(result) - for release in releases: - futures.append( - executor.submit(uninstall_release, namespace.metadata.name, release["name"]) - ) + for release in release_list: + futures.append( + executor.submit(uninstall_release, release["namespace"], release["name"]) + ) # Delete remaining pods pods = get_pods() diff --git a/test/test_base.py b/test/test_base.py index 855e47c51..7f2fbe28a 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -45,7 +45,7 @@ def cleanup(self, signum=None, frame=None): try: self.log.info("Stopping network") if self.network: - self.warnet("down") + self.warnet("down --force") self.wait_for_all_tanks_status(target="stopped", timeout=60, interval=1) except Exception as e: self.log.error(f"Error bringing network down: {e}") From 159ac616be74a386c273723433f73b8b8682c1a9 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 30 Sep 2024 22:31:36 -0500 Subject: [PATCH 31/76] control: clean up `down` command --- src/warnet/control.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index e6092c8e5..def23567c 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -134,15 +134,15 @@ def delete_pod(pod_name, namespace): return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" namespaces = get_namespaces() - print(namespaces) release_list: list[dict[str, str]] = [] - for namespace in namespaces: - command = f"helm list --namespace {namespace.metadata.name} -o json" + for v1namespace in namespaces: + namespace = v1namespace.metadata.name + command = f"helm list --namespace {namespace} -o json" result = run_command(command) if result: releases = json.loads(result) for release in releases: - release_list.append({"namespace": namespace.metadata.name, "name": release["name"]}) + release_list.append({"namespace": namespace, "name": release["name"]}) if not force: affected_namespaces = set([entry["namespace"] for entry in release_list]) From 9da42aa7a385a1bcfa479533bed395c9b7f593dc Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:05:53 -0500 Subject: [PATCH 32/76] control: add ns to `logs` --- src/warnet/control.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index def23567c..d18a0d162 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -340,18 +340,20 @@ def filter(path): @click.command() @click.argument("pod_name", type=str, default="") @click.option("--follow", "-f", is_flag=True, default=False, help="Follow logs") -def logs(pod_name: str, follow: bool): +@click.option("--namespace", type=str, default="default", show_default=True) +def logs(pod_name: str, follow: bool, namespace: str): """Show the logs of a pod""" - return _logs(pod_name, follow) + return _logs(pod_name, follow, namespace) -def _logs(pod_name: str, follow: bool): - namespace = get_default_namespace() +def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): + if not namespace: + namespace = get_default_namespace() if pod_name == "": try: pods = get_pods() - pod_list = [item.metadata.name for item in pods] + pod_list = [f"{item.metadata.name}: {item.metadata.namespace}" for item in pods] except Exception as e: print(f"Could not fetch any pods in namespace {namespace}: {e}") return @@ -369,7 +371,7 @@ def _logs(pod_name: str, follow: bool): ] selected = inquirer.prompt(q, theme=GreenPassion()) if selected: - pod_name = selected["pod"] + pod_name, pod_namespace = selected["pod"].split(": ") else: return # cancelled by user From cd004784e8858b05c9dbd1b3e87523215fbb18bb Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:07:07 -0500 Subject: [PATCH 33/76] control: add namespace to `run` --- src/warnet/control.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index d18a0d162..5e59c0917 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -221,11 +221,21 @@ def get_active_network(namespace): "--source_dir", type=click.Path(exists=True, file_okay=False, dir_okay=True), required=False ) @click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -def run(scenario_file: str, debug: bool, source_dir, additional_args: tuple[str]): +@click.option("--namespace", default=None, show_default=True) +def run( + scenario_file: str, + debug: bool, + source_dir, + additional_args: tuple[str], + namespace: Optional[str], +): """ Run a scenario from a file. Pass `-- --help` to get individual scenario help """ + if not namespace: + namespace = get_default_namespace() + scenario_path = Path(scenario_file).resolve() scenario_dir = scenario_path.parent if not source_dir else Path(source_dir).resolve() scenario_name = scenario_path.stem @@ -235,7 +245,6 @@ def run(scenario_file: str, debug: bool, source_dir, additional_args: tuple[str] # Collect tank data for warnet.json name = f"commander-{scenario_name.replace('_', '')}-{int(time.time())}" - namespace = get_default_namespace() tankpods = get_mission("tank") tanks = [ { @@ -323,18 +332,20 @@ def filter(path): print(f"Error: {e.stderr}") # upload scenario files and network data to the init container - wait_for_init(name) + wait_for_init(name, namespace=namespace) if write_file_to_container( - name, "init", "/shared/warnet.json", warnet_data - ) and write_file_to_container(name, "init", "/shared/archive.pyz", archive_data): + name, "init", "/shared/warnet.json", warnet_data, namespace=namespace + ) and write_file_to_container( + name, "init", "/shared/archive.pyz", archive_data, namespace=namespace + ): print(f"Successfully uploaded scenario data to commander: {scenario_name}") if debug: print("Waiting for commander pod to start...") - wait_for_pod(name) - _logs(pod_name=name, follow=True) + wait_for_pod(name, namespace=namespace) + _logs(pod_name=name, follow=True, namespace=namespace) print("Deleting pod...") - delete_pod(name) + delete_pod(name, namespace=namespace) @click.command() From a05e0c7a04eeca077d5e20c11572c7132f447dff Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:07:52 -0500 Subject: [PATCH 34/76] control: add imports --- src/warnet/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/warnet/control.py b/src/warnet/control.py index 5e59c0917..be8994e47 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -7,6 +7,7 @@ import zipapp from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path +from typing import Optional import click import inquirer From f0b2d4b1039c16825f3e2fb89a83d1ea4cca1116 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 16:20:13 -0500 Subject: [PATCH 35/76] control: ignore logging namespaces --- src/warnet/control.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/warnet/control.py b/src/warnet/control.py index be8994e47..030c8b06d 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -21,6 +21,8 @@ BITCOINCORE_CONTAINER, COMMANDER_CHART, COMMANDER_CONTAINER, + INGRESS_NAMESPACE, + LOGGING_NAMESPACE, ) from .k8s import ( delete_pod, @@ -365,7 +367,11 @@ def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): if pod_name == "": try: pods = get_pods() - pod_list = [f"{item.metadata.name}: {item.metadata.namespace}" for item in pods] + pod_list = [ + f"{item.metadata.name}: {item.metadata.namespace}" + for item in pods + if item.metadata.namespace not in [LOGGING_NAMESPACE, INGRESS_NAMESPACE] + ] except Exception as e: print(f"Could not fetch any pods in namespace {namespace}: {e}") return From 5100b0a944c61ff5f2df3afeaac932a5fea789b0 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:08:16 -0500 Subject: [PATCH 36/76] bitcoin: add ns to `rpc` --- src/warnet/bitcoin.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index b0f5d0c66..84e0f160b 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -4,6 +4,7 @@ import sys from datetime import datetime from io import BytesIO +from typing import Optional import click from test_framework.messages import ser_uint256 @@ -24,23 +25,24 @@ 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): +@click.option("--namespace", default=None, show_default=True) +def rpc(tank: str, method: str, params: str, namespace: Optional[str]): """ Call bitcoin-cli [params] on """ try: - result = _rpc(tank, method, params) + result = _rpc(tank, method, params, namespace) except Exception as e: print(f"{e}") sys.exit(1) print(result) -def _rpc(tank: str, method: str, params: str): +def _rpc(tank: str, method: str, params: str, namespace: Optional[str] = None): # 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() - + if not namespace: + namespace = get_default_namespace() if params: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, params))}" else: From 59913cd10bcdb1aed97f50b8af3fe22e39fbaf13 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:08:58 -0500 Subject: [PATCH 37/76] bitcoin: add ns to `debug_log` and `grep_logs` --- src/warnet/bitcoin.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 84e0f160b..4266e7dd6 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -52,11 +52,14 @@ def _rpc(tank: str, method: str, params: str, namespace: Optional[str] = None): @bitcoin.command() @click.argument("tank", type=str, required=True) -def debug_log(tank: str): +@click.option("--namespace", default=None, show_default=True) +def debug_log(tank: str, namespace: Optional[str]): """ Fetch the Bitcoin Core debug log from """ - cmd = f"kubectl logs {tank}" + if not namespace: + namespace = get_default_namespace() + cmd = f"kubectl logs {tank} --namespace {namespace}" try: print(run_command(cmd)) except Exception as e: @@ -79,8 +82,12 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): sys.exit(1) matching_logs = [] + longest_namespace_len = 0 for tank in tanks: + if len(tank.metadata.namespace) > longest_namespace_len: + longest_namespace_len = len(tank.metadata.namespace) + pod_name = tank.metadata.name logs = pod_log(pod_name, BITCOINCORE_CONTAINER) @@ -89,7 +96,7 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): for line in logs: log_entry = line.decode("utf-8").rstrip() if re.search(pattern, log_entry): - matching_logs.append((log_entry, pod_name)) + matching_logs.append((log_entry, tank.metadata.namespace, pod_name)) except Exception as e: print(e) except KeyboardInterrupt: @@ -100,7 +107,7 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): matching_logs.sort(key=lambda x: x[0]) # Print matching logs - for log_entry, pod_name in matching_logs: + for log_entry, namespace, pod_name in matching_logs: try: # Split the log entry into Kubernetes timestamp, Bitcoin timestamp, and the rest of the log k8s_timestamp, rest = log_entry.split(" ", 1) @@ -108,9 +115,13 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): # Format the output based on the show_k8s_timestamps option if show_k8s_timestamps: - print(f"{pod_name}: {k8s_timestamp} {bitcoin_timestamp} {log_message}") + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {k8s_timestamp} {bitcoin_timestamp} {log_message}" + ) else: - print(f"{pod_name}: {bitcoin_timestamp} {log_message}") + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {bitcoin_timestamp} {log_message}" + ) except ValueError: # If we can't parse the timestamps, just print the original log entry print(f"{pod_name}: {log_entry}") From c94c7a36c0de1d8f3174cf58b0d34e1f88c91001 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 01:09:52 -0500 Subject: [PATCH 38/76] bitcoin: add ns to `messages` and `get_messages` --- src/warnet/bitcoin.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 4266e7dd6..938f9a150 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -133,16 +133,29 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): @click.argument("tank_a", type=str, required=True) @click.argument("tank_b", type=str, required=True) @click.option("--chain", default="regtest", show_default=True) -def messages(tank_a: str, tank_b: str, chain: str): +@click.option("--namespace_a", default=None, show_default=True) +@click.option("--namespace_b", default=None, show_default=True) +def messages( + tank_a: str, tank_b: str, chain: str, namespace_a: Optional[str], namespace_b: Optional[str] +): """ Fetch messages sent between and in [chain] """ try: + if not namespace_a: + namespace_a = get_default_namespace() + if not namespace_b: + namespace_b = get_default_namespace() + # Get the messages - messages = get_messages(tank_a, tank_b, chain) + messages = get_messages( + tank_a, tank_b, chain, namespace_a=namespace_a, namespace_b=namespace_b + ) if not messages: - print(f"No messages found between {tank_a} and {tank_b}") + print( + f"No messages found between {tank_a} ({namespace_a}) and {tank_b} ({namespace_b})" + ) return # Process and print messages @@ -167,7 +180,7 @@ def messages(tank_a: str, tank_b: str, chain: str): print(f"Error fetching messages between nodes {tank_a} and {tank_b}: {e}") -def get_messages(tank_a: str, tank_b: str, chain: str): +def get_messages(tank_a: str, tank_b: str, chain: str, namespace_a: str, namespace_b: str): """ Fetch messages from the message capture files """ @@ -175,15 +188,17 @@ def get_messages(tank_a: str, tank_b: str, chain: str): 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}}'" + cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}' --namespace {namespace_b}" tank_b_ip = run_command(cmd).strip() # Get the service IP of node_b - cmd = f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}'" + cmd = ( + f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}' --namespace {namespace_b}" + ) tank_b_service_ip = run_command(cmd).strip() # List directories in the message capture folder - cmd = f"kubectl exec {tank_a} -- ls {base_dir}" + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- ls {base_dir}" dirs = run_command(cmd).splitlines() @@ -194,7 +209,8 @@ 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}" + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- cat {file_path}" + import subprocess blob = subprocess.run( cmd, shell=True, capture_output=True, executable="bash" From 7acf45d5ee93059e39fdeee3dd97562713de68e8 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 22:20:45 -0500 Subject: [PATCH 39/76] deploy: enable deploying to all user namespaces --- src/warnet/deploy.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 88f281651..ef2cf0079 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -22,7 +22,12 @@ NETWORK_FILE, WARGAMES_NAMESPACE_PREFIX, ) -from .k8s import get_default_namespace, wait_for_ingress_controller, wait_for_pod_ready +from .k8s import ( + get_default_namespace, + get_namespaces_by_prefix, + wait_for_ingress_controller, + wait_for_pod_ready, +) from .process import stream_command @@ -44,11 +49,28 @@ def validate_directory(ctx, param, value): callback=validate_directory, ) @click.option("--debug", is_flag=True) -@click.option("--namespace", type=str) -def deploy(directory, debug, namespace): +@click.option("--namespace", type=str, help="Specify a namespace in which to deploy the network") +@click.option("--to-all-users", is_flag=True, help="Deploy network to all user namespaces") +def deploy(directory, debug, namespace, to_all_users): + """Deploy a warnet with topology loaded from """ + if to_all_users: + namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + for namespace in namespaces: + _deploy(directory, debug, namespace.metadata.name, False) + else: + _deploy(directory, debug, namespace, to_all_users) + + +def _deploy(directory, debug, namespace, to_all_users): """Deploy a warnet with topology loaded from """ directory = Path(directory) + if to_all_users: + namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + for namespace in namespaces: + deploy(directory, debug, namespace.metadata.name, False) + return + if (directory / NETWORK_FILE).exists(): dl = deploy_logging_stack(directory, debug) deploy_network(directory, debug, namespace=namespace) From ea1827c554a4f6f067c6b97f87f214ddec0d5564 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 00:54:38 -0500 Subject: [PATCH 40/76] test base: add namespace to get_pod_exit_status --- test/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_base.py b/test/test_base.py index 7f2fbe28a..2b024da64 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -131,7 +131,7 @@ def check_scenarios(): if len(scns) == 0: return True for s in scns: - exit_status = get_pod_exit_status(s["name"]) + exit_status = get_pod_exit_status(s["name"], s["namespace"]) self.log.debug(f"Scenario {s['name']} exited with code {exit_status}") if exit_status != 0: return False From 8b6e9dad65be0eda42f464ee26d635eaa74cf4d6 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 30 Sep 2024 01:45:29 -0500 Subject: [PATCH 41/76] testing: add e2e namespace/admin test --- .../namespace-defaults.yaml | 18 +++ .../two_namespaces_two_users/namespaces.yaml | 19 +++ .../networks/6_node_bitcoin/network.yaml | 34 +++++ .../6_node_bitcoin/node-defaults.yaml | 26 ++++ test/namespace_admin_test.py | 124 ++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml create mode 100644 test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml create mode 100644 test/data/admin/networks/6_node_bitcoin/network.yaml create mode 100644 test/data/admin/networks/6_node_bitcoin/node-defaults.yaml create mode 100755 test/namespace_admin_test.py diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml new file mode 100644 index 000000000..75cc8e42c --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml @@ -0,0 +1,18 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +# the pod-viewer and pod-manager roles are the default +# roles defined in values.yaml for the namespaces charts +# +# if you need a different set of roles for a particular namespaces +# deployment, you can override values.yaml by providing your own +# role definitions below +# +# roles: +# - name: my-custom-role +# rules: +# - apiGroups: "" +# resources: "" +# verbs: "" diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml new file mode 100644 index 000000000..542456ef6 --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml @@ -0,0 +1,19 @@ +namespaces: + - name: wargames-red-team + users: + - name: alice + roles: + - pod-viewer + - name: bob + roles: + - pod-viewer + - pod-manager + - name: wargames-blue-team + users: + - name: mallory + roles: + - pod-viewer + - name: carol + roles: + - pod-viewer + - pod-manager diff --git a/test/data/admin/networks/6_node_bitcoin/network.yaml b/test/data/admin/networks/6_node_bitcoin/network.yaml new file mode 100644 index 000000000..69a3125fc --- /dev/null +++ b/test/data/admin/networks/6_node_bitcoin/network.yaml @@ -0,0 +1,34 @@ +nodes: + - name: tank-0001 + image: + tag: "26.0" + connect: + - tank-0002.wargames-red-team.svc.cluster.local + - tank-0003.wargames-blue-team.svc.cluster.local + - name: tank-0002 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + connect: + - tank-0003.wargames-red-team.svc.cluster.local + - tank-0004.wargames-blue-team.svc.cluster.local + - name: tank-0003 + connect: + - tank-0004.wargames-red-team.svc.cluster.local + - tank-0005.wargames-blue-team.svc.cluster.local + - name: tank-0004 + connect: + - tank-0005.wargames-red-team.svc.cluster.local + - tank-0006.wargames-blue-team.svc.cluster.local + - name: tank-0005 + connect: + - tank-0006.wargames-red-team.svc.cluster.local + - name: tank-0006 +fork_observer: + enabled: false +caddy: + enabled: false diff --git a/test/data/admin/networks/6_node_bitcoin/node-defaults.yaml b/test/data/admin/networks/6_node_bitcoin/node-defaults.yaml new file mode 100644 index 000000000..325405e88 --- /dev/null +++ b/test/data/admin/networks/6_node_bitcoin/node-defaults.yaml @@ -0,0 +1,26 @@ +chain: regtest + +collectLogs: false +metricsExport: false + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "27.0" + +config: | + dns=1 + debug=rpc diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py new file mode 100755 index 000000000..edddce87c --- /dev/null +++ b/test/namespace_admin_test.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +from typing import Callable, Optional + +from test_base import TestBase + +from warnet.k8s import get_kubeconfig_value +from warnet.process import run_command + + +class NamespaceAdminTest(TestBase): + def __init__(self): + super().__init__() + self.namespace_dir = ( + Path(os.path.dirname(__file__)) + / "data" + / "admin" + / "namespaces" + / "two_namespaces_two_users" + ) + self.network_dir = ( + Path(os.path.dirname(__file__)) / "data" / "admin" / "networks" / "6_node_bitcoin" + ) + + def run_test(self): + try: + self.setup_namespaces() + self.setup_service_accounts() + self.deploy_network_in_team_namespaces() + self.authenticate_and_become_bob() + self.become_minikube_once_again() + finally: + self.cleanup() + + def become_minikube_once_again(self): + minikube = "minikube" + cmd = f"kubectl config use-context {minikube}" + self.log.info(run_command(cmd)) + self.wait_for_predicate(self.this_is_the_current_context(minikube)) + + def this_is_the_current_context(self, context: str) -> Callable[[], bool]: + cmd = "kubectl config current-context" + current_context = run_command(cmd).strip() + self.log.info(f"Current context: {current_context} {context == current_context}") + return lambda: current_context == context + + def setup_namespaces(self): + self.log.info("Setting up the namespaces") + self.log.info(self.warnet(f"deploy {self.namespace_dir}")) + self.wait_for_predicate(self.two_namespaces_are_validated) + self.log.info("Namespace setup complete") + + def setup_service_accounts(self): + self.log.info("Creating service accounts...") + self.log.info(self.warnet("admin create-kubeconfigs")) + self.wait_for_predicate(self.service_accounts_are_validated) + + def deploy_network_in_team_namespaces(self): + self.log.info("Deploy networks to team namespaces") + self.log.info(self.warnet(f"deploy {self.network_dir} --to-all-users")) + self.wait_for_all_tanks_status() + self.log.info("Waiting for all edges") + self.wait_for_all_edges() + + def authenticate_and_become_bob(self): + self.log.info("Authenticating and becoming bob...") + assert get_kubeconfig_value("{.current-context}") == "minikube" + self.log.info(self.warnet("auth kubeconfigs/bob-wargames-red-team-kubeconfig")) + assert get_kubeconfig_value("{.current-context}") == "bob-wargames-red-team" + + def get_service_accounts(self) -> Optional[dict[str, str]]: + self.log.info("Setting up service accounts") + resp = self.warnet("admin service-accounts list") + if resp == "Could not find any matching service accounts.": + return None + service_accounts: dict[str, [str]] = {} + current_namespace = "" + for line in resp.splitlines(): + if line.startswith("Service"): + current_namespace = line.split(": ")[1] + service_accounts[current_namespace] = [] + if line.startswith("- "): + sa = line.lstrip("- ") + service_accounts[current_namespace].append(sa) + self.log.info(f"Service accounts: {service_accounts}") + return service_accounts + + def service_accounts_are_validated(self) -> bool: + self.log.info("Checking service accounts") + maybe_service_accounts = self.get_service_accounts() + expected = { + "wargames-blue-team": ["carol", "default", "mallory"], + "wargames-red-team": ["alice", "bob", "default"], + } + return maybe_service_accounts == expected + + def get_namespaces(self) -> Optional[list[str]]: + self.log.info("Querying the namespaces...") + resp = self.warnet("admin namespaces list") + if resp == "No warnet namespaces found.": + return None + namespaces = [] + for line in resp.splitlines(): + if line.startswith("- "): + namespaces.append(line.lstrip("- ")) + self.log.info(f"Namespaces: {namespaces}") + return namespaces + + def two_namespaces_are_validated(self) -> bool: + maybe_namespaces = self.get_namespaces() + if maybe_namespaces is None: + return False + if len(maybe_namespaces) != 2: + return False + if "wargames-blue-team" not in maybe_namespaces: + return False + return "wargames-red-team" in maybe_namespaces + + +if __name__ == "__main__": + test = NamespaceAdminTest() + test.run_test() From 3f91d0b9cae76abc0ec2146cc6de39b659588e4b Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 23 Sep 2024 22:19:28 -0500 Subject: [PATCH 42/76] admin.md: add admin documentation --- docs/admin.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/admin.md diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 000000000..dbb74f582 --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,70 @@ +# Admin + +## Connect to your cluster + +Ensure you are connected to your cluster because Warnet will use your current configuration to generate configurations for your users. + +```shell +$ warnet status +``` + +Observe that the output of the command matches your cluster. + +## Create an *admin* directory + +```shell +$ mkdir admin +$ cd admin +$ warnet admin init +``` + +Observe that there are now two folders within the *admin* directory: *namespaces* and *networks* + +## The *namespaces* directory +This directory contains a Helm chart named *two_namespaces_two_users*. + +Modify this chart based on the number of teams and users you have. + +Deploy the *two_namespaces_two_users* chart. + +```shell +$ cd namespaces +$ warnet deploy two_namespaces_two_users +``` + +Observe that this creates service accounts and namespaces in the cluster: + +```shell +$ kubectl get ns +$ kubectl get sa -A +``` + +### Creating Warnet invites +A Warnet invite is a Kubernetes config file. + +Create invites for each of your users. + +```shell +$ warnet admin create-kubeconfigs +``` + +Observe the *kubeconfigs* directory. It holds invites for each user. + +### Using Warnet invites +Users can connect to your wargame using their invite. + +```shell +$ warnet auth alice-wargames-red-team-kubeconfig +``` + +### Set up a network for your users +Before letting the users into your cluster, make sure to create a network of tanks for them to view. + + +```shell +$ warnet admin deploy networks/mynet --to-all-users +``` + +Observe that the *wargames-red-team* namespace now has tanks in it. + +**TODO**: What's the logging approach here? From e1c277ca178bef6bf929d8145209b50ce4ae1f04 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 1 Oct 2024 13:33:41 -0500 Subject: [PATCH 43/76] k8s: add `continue` to `wait_for_init` When this function encounters a matching pod, it attempts to retrieve that pod's init_container_statuses. The problem seems to be that this metadata is not available (perhaps because the pod is not read?). This means that the metadata is set to None which is not iterable and therefore causes a crash. This fix allows the program to proceed to the next element in the for loop stream if the init_container_statuses metadata is not ready (aka is None). --- src/warnet/k8s.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 40dabe292..8a62f2a2e 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -301,6 +301,8 @@ def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None): ): 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} ({namespace}) is ready") From 2c15b6ccb4e9cd3c4052235c29755b585643880d Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 08:53:09 -0500 Subject: [PATCH 44/76] testing: remove mention of minikube --- test/namespace_admin_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py index edddce87c..d5d463d75 100755 --- a/test/namespace_admin_test.py +++ b/test/namespace_admin_test.py @@ -27,6 +27,7 @@ def __init__(self): def run_test(self): try: self.setup_namespaces() + self.current_context = get_kubeconfig_value("{.current-context}") self.setup_service_accounts() self.deploy_network_in_team_namespaces() self.authenticate_and_become_bob() @@ -66,7 +67,7 @@ def deploy_network_in_team_namespaces(self): def authenticate_and_become_bob(self): self.log.info("Authenticating and becoming bob...") - assert get_kubeconfig_value("{.current-context}") == "minikube" + assert get_kubeconfig_value("{.current-context}") == self.current_context self.log.info(self.warnet("auth kubeconfigs/bob-wargames-red-team-kubeconfig")) assert get_kubeconfig_value("{.current-context}") == "bob-wargames-red-team" From 670980596983b238e1eef76c6110c06724c54dea Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 09:03:40 -0500 Subject: [PATCH 45/76] tesing: use a temporary directory --- test/namespace_admin_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py index d5d463d75..5361b963c 100755 --- a/test/namespace_admin_test.py +++ b/test/namespace_admin_test.py @@ -26,6 +26,8 @@ def __init__(self): def run_test(self): try: + os.chdir(self.tmpdir) + self.log.info(f"Running test in: {self.tmpdir}") self.setup_namespaces() self.current_context = get_kubeconfig_value("{.current-context}") self.setup_service_accounts() From 70a736f6493f17d59665cc620783c0516bfeb4ea Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 09:44:10 -0500 Subject: [PATCH 46/76] admin: update create_kubeconfig description --- src/warnet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 5f2233cec..23dbb1b0c 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -52,7 +52,7 @@ def init(): help="Duration of the token in seconds (default: 48 hours)", ) def create_kubeconfigs(kubeconfig_dir, token_duration): - """Create kubeconfig files for all ServiceAccounts in warnet team namespaces starting with .""" + """Create kubeconfig files for ServiceAccounts""" kubeconfig_dir = os.path.expanduser(kubeconfig_dir) cluster_name = get_kubeconfig_value("{.clusters[0].name}") From 809eb5cf5d33d188556cebc1cf29c404c231e728 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 10:41:28 -0500 Subject: [PATCH 47/76] admin: spelling nit --- src/warnet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 23dbb1b0c..58d2c6862 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -62,7 +62,7 @@ def create_kubeconfigs(kubeconfig_dir, token_duration): os.makedirs(kubeconfig_dir, exist_ok=True) # Get all namespaces that start with prefix - # This assumes when deploying multiple namespacs for the purpose of team games, all namespaces start with a prefix, + # This assumes when deploying multiple namespaces for the purpose of team games, all namespaces start with a prefix, # e.g., tabconf-wargames-*. Currently, this is a bit brittle, but we can improve on this in the future # by automatically applying a TEAM_PREFIX when creating the get_warnet_namespaces # TODO: choose a prefix convention and have it managed by the helm charts instead of requiring the From 1feed7a09f07e539cf5e1b06ac079196a1c44a3e Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:03:59 -0500 Subject: [PATCH 48/76] DRY out the namespace check Replace the namespace check with this function: `get_default_namespace_or(namespace)` --- src/warnet/bitcoin.py | 14 +++++--------- src/warnet/control.py | 8 +++----- src/warnet/deploy.py | 7 +++---- src/warnet/k8s.py | 33 +++++++++++++++------------------ 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 938f9a150..63bc14c91 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -12,7 +12,7 @@ from urllib3.exceptions import MaxRetryError from .constants import BITCOINCORE_CONTAINER -from .k8s import get_default_namespace, get_mission, pod_log +from .k8s import get_default_namespace, get_mission, pod_log, get_default_namespace_or from .process import run_command @@ -41,8 +41,7 @@ def rpc(tank: str, method: str, params: str, namespace: Optional[str]): def _rpc(tank: str, method: str, params: str, namespace: Optional[str] = None): # bitcoin-cli should be able to read bitcoin.conf inside the container # so no extra args like port, chain, username or password are needed - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) if params: cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {' '.join(map(str, params))}" else: @@ -57,8 +56,7 @@ def debug_log(tank: str, namespace: Optional[str]): """ Fetch the Bitcoin Core debug log from """ - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) cmd = f"kubectl logs {tank} --namespace {namespace}" try: print(run_command(cmd)) @@ -142,10 +140,8 @@ def messages( Fetch messages sent between and in [chain] """ try: - if not namespace_a: - namespace_a = get_default_namespace() - if not namespace_b: - namespace_b = get_default_namespace() + namespace_a = get_default_namespace_or(namespace_a) + namespace_b = get_default_namespace_or(namespace_b) # Get the messages messages = get_messages( diff --git a/src/warnet/control.py b/src/warnet/control.py index 030c8b06d..fc3609d2a 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -35,7 +35,7 @@ snapshot_bitcoin_datadir, wait_for_init, wait_for_pod, - write_file_to_container, + write_file_to_container, get_default_namespace_or, ) from .process import run_command, stream_command @@ -236,8 +236,7 @@ def run( Run a scenario from a file. Pass `-- --help` to get individual scenario help """ - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) scenario_path = Path(scenario_file).resolve() scenario_dir = scenario_path.parent if not source_dir else Path(source_dir).resolve() @@ -361,8 +360,7 @@ def logs(pod_name: str, follow: bool, namespace: str): def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) if pod_name == "": try: diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index ef2cf0079..7fa4c5d3e 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -26,7 +26,7 @@ get_default_namespace, get_namespaces_by_prefix, wait_for_ingress_controller, - wait_for_pod_ready, + wait_for_pod_ready, get_default_namespace_or, ) from .process import stream_command @@ -218,12 +218,11 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str network_file_path = directory / NETWORK_FILE defaults_file_path = directory / DEFAULTS_FILE + namespace = get_default_namespace_or(namespace) + with network_file_path.open() as f: network_file = yaml.safe_load(f) - if not namespace: - namespace = get_default_namespace() - for node in network_file["nodes"]: click.echo(f"Deploying node: {node.get('name')}") try: diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 8a62f2a2e..67d5360dd 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -51,9 +51,8 @@ def get_pods() -> list[V1Pod]: def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: + namespace = get_default_namespace_or(namespace) sclient = get_static_client() - if not namespace: - namespace = get_default_namespace() return sclient.read_namespaced_pod(name=name, namespace=namespace) @@ -67,8 +66,7 @@ def get_mission(mission: str) -> list[V1Pod]: def get_pod_exit_status(pod_name, namespace: Optional[str] = None): - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) try: sclient = get_static_client() pod = sclient.read_namespaced_pod(name=pod_name, namespace=namespace) @@ -82,8 +80,7 @@ def get_pod_exit_status(pod_name, namespace: Optional[str] = None): def get_edges(namespace: Optional[str] = None) -> any: - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) sclient = get_static_client() configmap = sclient.read_namespaced_config_map(name="edges", namespace=namespace) return json.loads(configmap.data["data"]) @@ -138,8 +135,7 @@ def delete_namespace(namespace: str) -> bool: def delete_pod(pod_name: str, namespace: Optional[str] = None) -> bool: - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) command = f"kubectl -n {namespace} delete pod {pod_name}" return stream_command(command) @@ -159,6 +155,10 @@ def get_default_namespace() -> str: return kubectl_namespace if kubectl_namespace else DEFAULT_NAMESPACE +def get_default_namespace_or(namespace: Optional[str]) -> str: + return namespace if namespace else get_default_namespace() + + def snapshot_bitcoin_datadir( pod_name: str, chain: str, @@ -166,8 +166,7 @@ def snapshot_bitcoin_datadir( filters: list[str] = None, namespace: Optional[str] = None, ) -> None: - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) sclient = get_static_client() try: @@ -292,8 +291,7 @@ def wait_for_pod_ready(name, namespace, timeout=300): def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None): - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) sclient = get_static_client() w = watch.Watch() for event in w.stream( @@ -335,9 +333,9 @@ def get_ingress_ip_or_host(): def pod_log(pod_name, container_name=None, follow=False, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) sclient = get_static_client() - if not namespace: - namespace = get_default_namespace() + try: return sclient.read_namespaced_pod_log( name=pod_name, @@ -351,8 +349,7 @@ def pod_log(pod_name, container_name=None, follow=False, namespace: Optional[str def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) sclient = get_static_client() while timeout_seconds > 0: pod = sclient.read_namespaced_pod_status(name=pod_name, namespace=namespace) @@ -365,8 +362,7 @@ def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): def write_file_to_container( pod_name, container_name, dst_path, data, namespace: Optional[str] = None ): - if not namespace: - namespace = get_default_namespace() + namespace = get_default_namespace_or(namespace) sclient = get_static_client() exec_command = ["sh", "-c", f"cat > {dst_path}"] try: @@ -428,3 +424,4 @@ def get_service_accounts_in_namespace(namespace): # skip the default service account created by k8s service_accounts = run_command(command).split() return [sa for sa in service_accounts if sa != "default"] + From e40ea686c5d5b456abf127c164987a997afeff1b Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:07:41 -0500 Subject: [PATCH 49/76] testing: finish replacing minikube logic --- test/namespace_admin_test.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py index 5361b963c..5f2712778 100755 --- a/test/namespace_admin_test.py +++ b/test/namespace_admin_test.py @@ -29,19 +29,18 @@ def run_test(self): os.chdir(self.tmpdir) self.log.info(f"Running test in: {self.tmpdir}") self.setup_namespaces() - self.current_context = get_kubeconfig_value("{.current-context}") + self.initial_context = get_kubeconfig_value("{.current-context}") self.setup_service_accounts() self.deploy_network_in_team_namespaces() self.authenticate_and_become_bob() - self.become_minikube_once_again() + self.return_to_intial_context() finally: self.cleanup() - def become_minikube_once_again(self): - minikube = "minikube" - cmd = f"kubectl config use-context {minikube}" + def return_to_intial_context(self): + cmd = f"kubectl config use-context {self.initial_context}" self.log.info(run_command(cmd)) - self.wait_for_predicate(self.this_is_the_current_context(minikube)) + self.wait_for_predicate(self.this_is_the_current_context(self.initial_context)) def this_is_the_current_context(self, context: str) -> Callable[[], bool]: cmd = "kubectl config current-context" @@ -69,7 +68,7 @@ def deploy_network_in_team_namespaces(self): def authenticate_and_become_bob(self): self.log.info("Authenticating and becoming bob...") - assert get_kubeconfig_value("{.current-context}") == self.current_context + assert get_kubeconfig_value("{.current-context}") == self.initial_context self.log.info(self.warnet("auth kubeconfigs/bob-wargames-red-team-kubeconfig")) assert get_kubeconfig_value("{.current-context}") == "bob-wargames-red-team" From d47073cc67a99a3e5aaed0e3983965c9299201a8 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:39:01 -0500 Subject: [PATCH 50/76] ruff get_default_namespace_or --- src/warnet/bitcoin.py | 2 +- src/warnet/control.py | 3 ++- src/warnet/deploy.py | 3 ++- src/warnet/k8s.py | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 63bc14c91..d6b9effa9 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -12,7 +12,7 @@ from urllib3.exceptions import MaxRetryError from .constants import BITCOINCORE_CONTAINER -from .k8s import get_default_namespace, get_mission, pod_log, get_default_namespace_or +from .k8s import get_default_namespace_or, get_mission, pod_log from .process import run_command diff --git a/src/warnet/control.py b/src/warnet/control.py index fc3609d2a..fdf5c15fb 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -27,6 +27,7 @@ from .k8s import ( delete_pod, get_default_namespace, + get_default_namespace_or, get_mission, get_namespaces, get_pod, @@ -35,7 +36,7 @@ snapshot_bitcoin_datadir, wait_for_init, wait_for_pod, - write_file_to_container, get_default_namespace_or, + write_file_to_container, ) from .process import run_command, stream_command diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 7fa4c5d3e..cbad569e3 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -24,9 +24,10 @@ ) from .k8s import ( get_default_namespace, + get_default_namespace_or, get_namespaces_by_prefix, wait_for_ingress_controller, - wait_for_pod_ready, get_default_namespace_or, + wait_for_pod_ready, ) from .process import stream_command diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 67d5360dd..9db9ca88e 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -424,4 +424,3 @@ def get_service_accounts_in_namespace(namespace): # skip the default service account created by k8s service_accounts = run_command(command).split() return [sa for sa in service_accounts if sa != "default"] - From e208c0e89be97251c35b7d04f64f560d452aa02a Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:42:27 -0500 Subject: [PATCH 51/76] testing: bring service account checking "in house" --- test/namespace_admin_test.py | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py index 5f2712778..bc782e60f 100755 --- a/test/namespace_admin_test.py +++ b/test/namespace_admin_test.py @@ -6,7 +6,8 @@ from test_base import TestBase -from warnet.k8s import get_kubeconfig_value +from warnet.constants import WARGAMES_NAMESPACE_PREFIX +from warnet.k8s import get_kubeconfig_value, get_static_client from warnet.process import run_command @@ -72,30 +73,30 @@ def authenticate_and_become_bob(self): self.log.info(self.warnet("auth kubeconfigs/bob-wargames-red-team-kubeconfig")) assert get_kubeconfig_value("{.current-context}") == "bob-wargames-red-team" - def get_service_accounts(self) -> Optional[dict[str, str]]: - self.log.info("Setting up service accounts") - resp = self.warnet("admin service-accounts list") - if resp == "Could not find any matching service accounts.": - return None - service_accounts: dict[str, [str]] = {} - current_namespace = "" - for line in resp.splitlines(): - if line.startswith("Service"): - current_namespace = line.split(": ")[1] - service_accounts[current_namespace] = [] - if line.startswith("- "): - sa = line.lstrip("- ") - service_accounts[current_namespace].append(sa) - self.log.info(f"Service accounts: {service_accounts}") - return service_accounts - def service_accounts_are_validated(self) -> bool: self.log.info("Checking service accounts") - maybe_service_accounts = self.get_service_accounts() + sclient = get_static_client() + namespaces = sclient.list_namespace().items + + filtered_namespaces = [ + ns.metadata.name + for ns in namespaces + if ns.metadata.name.startswith(WARGAMES_NAMESPACE_PREFIX) + ] + assert len(filtered_namespaces) != 0 + + maybe_service_accounts = {} + + for namespace in filtered_namespaces: + service_accounts = sclient.list_namespaced_service_account(namespace=namespace).items + for sa in service_accounts: + maybe_service_accounts.setdefault(namespace, []).append(sa.metadata.name) + expected = { "wargames-blue-team": ["carol", "default", "mallory"], "wargames-red-team": ["alice", "bob", "default"], } + return maybe_service_accounts == expected def get_namespaces(self) -> Optional[list[str]]: From 713597332ae5770cb9e58af583782eedac31ba59 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:43:06 -0500 Subject: [PATCH 52/76] service_accounts: remove func from `admin` section --- src/warnet/admin.py | 2 -- src/warnet/service_accounts.py | 38 ---------------------------------- 2 files changed, 40 deletions(-) delete mode 100644 src/warnet/service_accounts.py diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 58d2c6862..8e0f7dde6 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -9,7 +9,6 @@ from .namespaces import copy_namespaces_defaults, namespaces from .network import copy_network_defaults from .process import run_command -from .service_accounts import service_accounts @click.group(name="admin", hidden=True) @@ -19,7 +18,6 @@ def admin(): admin.add_command(namespaces) -admin.add_command(service_accounts) @admin.command() diff --git a/src/warnet/service_accounts.py b/src/warnet/service_accounts.py deleted file mode 100644 index e32b4acb2..000000000 --- a/src/warnet/service_accounts.py +++ /dev/null @@ -1,38 +0,0 @@ -import click - -from .constants import ( - WARGAMES_NAMESPACE_PREFIX, -) -from .k8s import get_static_client - - -@click.group(name="service-accounts") -def service_accounts(): - """Service account commands""" - - -@service_accounts.command() -def list(): - """List all service accounts with 'wargames-' prefix""" - # Load the kubeconfig file - sclient = get_static_client() - namespaces = sclient.list_namespace().items - - filtered_namespaces = [ - ns.metadata.name - for ns in namespaces - if ns.metadata.name.startswith(WARGAMES_NAMESPACE_PREFIX) - ] - - if len(filtered_namespaces) == 0: - click.secho("Could not find any matching service accounts.", fg="yellow") - - for namespace in filtered_namespaces: - click.secho(f"Service accounts in namespace: {namespace}") - service_accounts = sclient.list_namespaced_service_account(namespace=namespace).items - - if len(service_accounts) == 0: - click.secho("...Could not find any matching service accounts", fg="yellow") - - for sa in service_accounts: - click.secho(f"- {sa.metadata.name}", fg="green") From 30af65a99cbfa5d048fce3d55f2f8acc160bd595 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 11:59:53 -0500 Subject: [PATCH 53/76] admin: make kubeconfig a dict And use yaml.dump --- src/warnet/admin.py | 46 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 8e0f7dde6..392349dca 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -2,6 +2,7 @@ from pathlib import Path import click +import yaml from rich import print as richprint from .constants import NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX @@ -92,27 +93,32 @@ def create_kubeconfigs(kubeconfig_dir, token_duration): # might not be worth it since we are just reading the yaml to then create a bunch of values and its not # actually used to deploy anything into the cluster # Then benefit would be making this code a bit cleaner and easy to follow, fwiw - kubeconfig_content = f"""apiVersion: v1 -kind: Config -clusters: -- name: {cluster_name} - cluster: - server: {cluster_server} - certificate-authority-data: {cluster_ca} -users: -- name: {sa} - user: - token: {token} -contexts: -- name: {sa}-{namespace} - context: - cluster: {cluster_name} - namespace: {namespace} - user: {sa} -current-context: {sa}-{namespace} -""" + + kubeconfig_dict = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [ + { + "name": cluster_name, + "cluster": { + "server": cluster_server, + "certificate-authority-data": cluster_ca, + }, + } + ], + "users": [{"name": sa, "user": {"token": token}}], + "contexts": [ + { + "name": f"{sa}-{namespace}", + "context": {"cluster": cluster_name, "namespace": namespace, "user": sa}, + } + ], + "current-context": f"{sa}-{namespace}", + } + + # Write to a YAML file with open(kubeconfig_file, "w") as f: - f.write(kubeconfig_content) + yaml.dump(kubeconfig_dict, f, default_flow_style=False) click.echo(f" Created kubeconfig file for {sa}: {kubeconfig_file}") From 518810810869c5652d7b4ddec97d77fd851780e2 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 12:28:58 -0500 Subject: [PATCH 54/76] constants: use labels to select pods for `log` --- src/warnet/constants.py | 3 +++ src/warnet/control.py | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 3ecddedb6..b1502e996 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -18,6 +18,9 @@ KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] HELM_COMMAND = "helm upgrade --install --create-namespace" +TANK_MISSION = "tank" +COMMANDER_MISSION = "commander" + BITCOINCORE_CONTAINER = "bitcoincore" COMMANDER_CONTAINER = "commander" diff --git a/src/warnet/control.py b/src/warnet/control.py index fdf5c15fb..1ee7f70d0 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -12,6 +12,7 @@ import click import inquirer from inquirer.themes import GreenPassion +from kubernetes.client.models import V1Pod from rich import print from rich.console import Console from rich.prompt import Confirm, Prompt @@ -21,8 +22,8 @@ BITCOINCORE_CONTAINER, COMMANDER_CHART, COMMANDER_CONTAINER, - INGRESS_NAMESPACE, - LOGGING_NAMESPACE, + COMMANDER_MISSION, + TANK_MISSION, ) from .k8s import ( delete_pod, @@ -363,20 +364,23 @@ def logs(pod_name: str, follow: bool, namespace: str): def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): namespace = get_default_namespace_or(namespace) + def format_pods(pods: list[V1Pod]) -> list[str]: + return [f"{pod.metadata.name}: {pod.metadata.namespace}" for pod in pods] + if pod_name == "": try: - pods = get_pods() - pod_list = [ - f"{item.metadata.name}: {item.metadata.namespace}" - for item in pods - if item.metadata.namespace not in [LOGGING_NAMESPACE, INGRESS_NAMESPACE] - ] + pod_list = [] + formatted_commanders = format_pods(get_mission(COMMANDER_MISSION)) + formatted_tanks = format_pods(get_mission(TANK_MISSION)) + pod_list.extend(formatted_commanders) + pod_list.extend(formatted_tanks) + except Exception as e: - print(f"Could not fetch any pods in namespace {namespace}: {e}") + print(f"Could not fetch any pods in namespace ({namespace}): {e}") return if not pod_list: - print(f"Could not fetch any pods in namespace {namespace}") + print(f"Could not fetch any pods in namespace ({namespace})") return q = [ From 2bb87ec7dea781549c6ff67adc46a6e33d1066e7 Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 12:34:47 -0500 Subject: [PATCH 55/76] rename to `get_namespaces_by_type` `get_namespaces_by_prefix` is now renamed to `get_namespaces_by_type` in anticipation of using labels in the future. --- src/warnet/admin.py | 4 ++-- src/warnet/deploy.py | 6 +++--- src/warnet/k8s.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 392349dca..13398486e 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -6,7 +6,7 @@ from rich import print as richprint from .constants import NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX -from .k8s import get_kubeconfig_value, get_namespaces_by_prefix, get_service_accounts_in_namespace +from .k8s import get_kubeconfig_value, get_namespaces_by_type, get_service_accounts_in_namespace from .namespaces import copy_namespaces_defaults, namespaces from .network import copy_network_defaults from .process import run_command @@ -67,7 +67,7 @@ def create_kubeconfigs(kubeconfig_dir, token_duration): # TODO: choose a prefix convention and have it managed by the helm charts instead of requiring the # admin user to pipe through the correct string in multiple places. Another would be to use # labels instead of namespace naming conventions - warnet_namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + warnet_namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) for v1namespace in warnet_namespaces: namespace = v1namespace.metadata.name diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index cbad569e3..63c888e30 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -25,7 +25,7 @@ from .k8s import ( get_default_namespace, get_default_namespace_or, - get_namespaces_by_prefix, + get_namespaces_by_type, wait_for_ingress_controller, wait_for_pod_ready, ) @@ -55,7 +55,7 @@ def validate_directory(ctx, param, value): def deploy(directory, debug, namespace, to_all_users): """Deploy a warnet with topology loaded from """ if to_all_users: - namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) for namespace in namespaces: _deploy(directory, debug, namespace.metadata.name, False) else: @@ -67,7 +67,7 @@ def _deploy(directory, debug, namespace, to_all_users): directory = Path(directory) if to_all_users: - namespaces = get_namespaces_by_prefix(WARGAMES_NAMESPACE_PREFIX) + namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) for namespace in namespaces: deploy(directory, debug, namespace.metadata.name, False) return diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 9db9ca88e..cb22dc7bc 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -408,12 +408,12 @@ def get_namespaces() -> list[V1Namespace]: return [] -def get_namespaces_by_prefix(prefix: str) -> list[V1Namespace]: +def get_namespaces_by_type(namespace_type: str) -> list[V1Namespace]: """ Get all namespaces beginning with `prefix`. Returns empty list of no namespaces with the specified prefix are found. """ namespaces = get_namespaces() - return [ns for ns in namespaces if ns.metadata.name.startswith(prefix)] + return [ns for ns in namespaces if ns.metadata.name.startswith(namespace_type)] def get_service_accounts_in_namespace(namespace): From 2cf829241823e4e79cb16f482b3305fcc383030c Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 12:46:10 -0500 Subject: [PATCH 56/76] k8s: add `can_delete_pods` function We use this to check if the current user can delete pods. --- src/warnet/k8s.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index cb22dc7bc..7f568be1d 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -424,3 +424,37 @@ def get_service_accounts_in_namespace(namespace): # skip the default service account created by k8s service_accounts = run_command(command).split() return [sa for sa in service_accounts if sa != "default"] + + +def can_delete_pods(namespace: Optional[str] = None) -> bool: + namespace = get_default_namespace_or(namespace) + + get_static_client() + auth_api = client.AuthorizationV1Api() + + # Define the SelfSubjectAccessReview request for deleting pods + access_review = client.V1SelfSubjectAccessReview( + spec=client.V1SelfSubjectAccessReviewSpec( + resource_attributes=client.V1ResourceAttributes( + namespace=namespace, + verb="delete", # Action: 'delete' + resource="pods", # Resource: 'pods' + ) + ) + ) + + try: + # Perform the SelfSubjectAccessReview check + review_response = auth_api.create_self_subject_access_review(body=access_review) + + # Check the result and return + if review_response.status.allowed: + print(f"Service account can delete pods in namespace '{namespace}'.") + return True + else: + print(f"Service account CANNOT delete pods in namespace '{namespace}'.") + return False + + except ApiException as e: + print(f"An error occurred: {e}") + return False From d6f4ed37a2a52164aefc625f961250a23ed8f9ab Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 12:46:45 -0500 Subject: [PATCH 57/76] control: update `down` with `can_delete_pods` We want to back out early if the user is not able to bring down the network --- src/warnet/control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/warnet/control.py b/src/warnet/control.py index 1ee7f70d0..cd8d51cdf 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -26,6 +26,7 @@ TANK_MISSION, ) from .k8s import ( + can_delete_pods, delete_pod, get_default_namespace, get_default_namespace_or, @@ -138,6 +139,10 @@ def delete_pod(pod_name, namespace): subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" + if not can_delete_pods(): + click.secho("You do not have permission to bring down the network.", fg="red") + return + namespaces = get_namespaces() release_list: list[dict[str, str]] = [] for v1namespace in namespaces: From bfc778b3f8dd0e649f8a6c49b80265353889c6ed Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 18:13:02 -0500 Subject: [PATCH 58/76] admin.md: update namespace deploy command --- docs/admin.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/admin.md b/docs/admin.md index dbb74f582..16b9a5a5d 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -28,8 +28,7 @@ Modify this chart based on the number of teams and users you have. Deploy the *two_namespaces_two_users* chart. ```shell -$ cd namespaces -$ warnet deploy two_namespaces_two_users +$ warnet deploy namespaces/two_namespaces_two_users ``` Observe that this creates service accounts and namespaces in the cluster: From ae19f288e87f081001e1e1ecbf0dd4ca35a2407c Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 3 Oct 2024 18:37:40 -0500 Subject: [PATCH 59/76] bitcoin: make ruff happy --- src/warnet/bitcoin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index d6b9effa9..1f478a6cc 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -1,6 +1,5 @@ import os import re -import subprocess import sys from datetime import datetime from io import BytesIO From e7dbaf14825cf5ee341eb323edae948f56ce70d5 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 11:57:33 -0500 Subject: [PATCH 60/76] k8s: add open/write kubeconfig fn; add K8sError The K8sError allows us to easily handle one single error that crops up from functions that life in k8s.py. Right now I just have it implemented in the open/write kubeconfig functions. --- src/warnet/k8s.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 7f568be1d..6cc458d64 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -25,6 +25,10 @@ from .process import run_command, stream_command +class K8sError(Exception): + pass + + def get_static_client() -> CoreV1Api: config.load_kube_config(config_file=KUBECONFIG) return client.CoreV1Api() @@ -458,3 +462,24 @@ def can_delete_pods(namespace: Optional[str] = None) -> bool: except ApiException as e: print(f"An error occurred: {e}") return False + + +def open_kubeconfig(kubeconfig_path: str) -> 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, kubeconfig_path: str) -> None: + dir_name = os.path.dirname(kubeconfig_path) + 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_path) + except Exception as e: + os.remove(temp_file.name) + raise K8sError(f"Error writing kubeconfig: {kubeconfig_path}") from e From 5a38e92666af80dafd0d90ef2d05cdff83f965b8 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:55:50 -0500 Subject: [PATCH 61/76] constants: remove --create-namespace --- src/warnet/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index b1502e996..adeae493e 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -16,7 +16,7 @@ INGRESS_NAMESPACE = "ingress" WARGAMES_NAMESPACE_PREFIX = "wargames-" KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] -HELM_COMMAND = "helm upgrade --install --create-namespace" +HELM_COMMAND = "helm upgrade --install" TANK_MISSION = "tank" COMMANDER_MISSION = "commander" From 73207584772c29cf1d969b3e9536ffe7d0e765a9 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 11:58:59 -0500 Subject: [PATCH 62/76] testing: specify `warnettest` in named items --- .../two_namespaces_two_users/namespaces.yaml | 12 ++++++------ .../admin/networks/6_node_bitcoin/network.yaml | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml index 542456ef6..413d3bcb7 100644 --- a/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml @@ -1,19 +1,19 @@ namespaces: - - name: wargames-red-team + - name: wargames-red-team-warnettest users: - - name: alice + - name: alice-warnettest roles: - pod-viewer - - name: bob + - name: bob-warnettest roles: - pod-viewer - pod-manager - - name: wargames-blue-team + - name: wargames-blue-team-warnettest users: - - name: mallory + - name: mallory-warnettest roles: - pod-viewer - - name: carol + - name: carol-warnettest roles: - pod-viewer - pod-manager diff --git a/test/data/admin/networks/6_node_bitcoin/network.yaml b/test/data/admin/networks/6_node_bitcoin/network.yaml index 69a3125fc..fed0ef640 100644 --- a/test/data/admin/networks/6_node_bitcoin/network.yaml +++ b/test/data/admin/networks/6_node_bitcoin/network.yaml @@ -3,8 +3,8 @@ nodes: image: tag: "26.0" connect: - - tank-0002.wargames-red-team.svc.cluster.local - - tank-0003.wargames-blue-team.svc.cluster.local + - tank-0002.wargames-red-team-warnettest.svc.cluster.local + - tank-0003.wargames-blue-team-warnettest.svc.cluster.local - name: tank-0002 resources: limits: @@ -14,19 +14,19 @@ nodes: cpu: 100m memory: 128Mi connect: - - tank-0003.wargames-red-team.svc.cluster.local - - tank-0004.wargames-blue-team.svc.cluster.local + - tank-0003.wargames-red-team-warnettest.svc.cluster.local + - tank-0004.wargames-blue-team-warnettest.svc.cluster.local - name: tank-0003 connect: - - tank-0004.wargames-red-team.svc.cluster.local - - tank-0005.wargames-blue-team.svc.cluster.local + - tank-0004.wargames-red-team-warnettest.svc.cluster.local + - tank-0005.wargames-blue-team-warnettest.svc.cluster.local - name: tank-0004 connect: - - tank-0005.wargames-red-team.svc.cluster.local - - tank-0006.wargames-blue-team.svc.cluster.local + - tank-0005.wargames-red-team-warnettest.svc.cluster.local + - tank-0006.wargames-blue-team-warnettest.svc.cluster.local - name: tank-0005 connect: - - tank-0006.wargames-red-team.svc.cluster.local + - tank-0006.wargames-red-team-warnettest.svc.cluster.local - name: tank-0006 fork_observer: enabled: false From 487641539cc83d08eabb1b141792397b2b3f7580 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:53:57 -0500 Subject: [PATCH 63/76] testing: update test to include cleanup --- test/namespace_admin_test.py | 74 ++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py index bc782e60f..c92040e1f 100755 --- a/test/namespace_admin_test.py +++ b/test/namespace_admin_test.py @@ -6,8 +6,14 @@ from test_base import TestBase -from warnet.constants import WARGAMES_NAMESPACE_PREFIX -from warnet.k8s import get_kubeconfig_value, get_static_client +from warnet.constants import KUBECONFIG, WARGAMES_NAMESPACE_PREFIX +from warnet.k8s import ( + K8sError, + get_kubeconfig_value, + get_static_client, + open_kubeconfig, + write_kubeconfig, +) from warnet.process import run_command @@ -29,15 +35,34 @@ def run_test(self): try: os.chdir(self.tmpdir) self.log.info(f"Running test in: {self.tmpdir}") + self.establish_initial_context() + self.establish_names() self.setup_namespaces() - self.initial_context = get_kubeconfig_value("{.current-context}") self.setup_service_accounts() self.deploy_network_in_team_namespaces() self.authenticate_and_become_bob() self.return_to_intial_context() finally: + try: + self.cleanup_kubeconfig() + except K8sError as e: + self.log.info(f"KUBECONFIG cleanup error: {e}") self.cleanup() + def establish_initial_context(self): + self.initial_context = get_kubeconfig_value("{.current-context}") + self.log.info(f"Initial context: {self.initial_context}") + + def establish_names(self): + self.bob_user = "bob-warnettest" + self.bob_auth_file = "bob-warnettest-wargames-red-team-warnettest-kubeconfig" + self.bob_context = "bob-warnettest-wargames-red-team-warnettest" + + self.blue_namespace = "wargames-blue-team-warnettest" + self.red_namespace = "wargames-red-team-warnettest" + self.blue_users = ["carol-warnettest", "default", "mallory-warnettest"] + self.red_users = ["alice-warnettest", self.bob_user, "default"] + def return_to_intial_context(self): cmd = f"kubectl config use-context {self.initial_context}" self.log.info(run_command(cmd)) @@ -59,6 +84,7 @@ def setup_service_accounts(self): self.log.info("Creating service accounts...") self.log.info(self.warnet("admin create-kubeconfigs")) self.wait_for_predicate(self.service_accounts_are_validated) + self.log.info("Service accounts have been set up and validated") def deploy_network_in_team_namespaces(self): self.log.info("Deploy networks to team namespaces") @@ -70,8 +96,8 @@ def deploy_network_in_team_namespaces(self): def authenticate_and_become_bob(self): self.log.info("Authenticating and becoming bob...") assert get_kubeconfig_value("{.current-context}") == self.initial_context - self.log.info(self.warnet("auth kubeconfigs/bob-wargames-red-team-kubeconfig")) - assert get_kubeconfig_value("{.current-context}") == "bob-wargames-red-team" + self.warnet(f"auth kubeconfigs/{self.bob_auth_file}") + assert get_kubeconfig_value("{.current-context}") == self.bob_context def service_accounts_are_validated(self) -> bool: self.log.info("Checking service accounts") @@ -93,8 +119,8 @@ def service_accounts_are_validated(self) -> bool: maybe_service_accounts.setdefault(namespace, []).append(sa.metadata.name) expected = { - "wargames-blue-team": ["carol", "default", "mallory"], - "wargames-red-team": ["alice", "bob", "default"], + self.blue_namespace: self.blue_users, + self.red_namespace: self.red_users, } return maybe_service_accounts == expected @@ -115,11 +141,37 @@ def two_namespaces_are_validated(self) -> bool: maybe_namespaces = self.get_namespaces() if maybe_namespaces is None: return False - if len(maybe_namespaces) != 2: - return False - if "wargames-blue-team" not in maybe_namespaces: + if self.blue_namespace not in maybe_namespaces: return False - return "wargames-red-team" in maybe_namespaces + return self.red_namespace in maybe_namespaces + + def cleanup_kubeconfig(self): + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + raise K8sError(f"Could not open KUBECONFIG: {KUBECONFIG}") from e + + kubeconfig_data = remove_user(kubeconfig_data, self.bob_user) + kubeconfig_data = remove_context(kubeconfig_data, self.bob_context) + + try: + write_kubeconfig(kubeconfig_data, KUBECONFIG) + except Exception as e: + raise K8sError(f"Could not write to KUBECONFIG: {KUBECONFIG}") from e + + +def remove_user(kubeconfig_data: dict, username: str) -> dict: + kubeconfig_data["users"] = [ + user for user in kubeconfig_data["users"] if user["name"] != username + ] + return kubeconfig_data + + +def remove_context(kubeconfig_data: dict, context_name: str) -> dict: + kubeconfig_data["contexts"] = [ + context for context in kubeconfig_data["contexts"] if context["name"] != context_name + ] + return kubeconfig_data if __name__ == "__main__": From 971afb9b11f27ee2da5a51f5eba2bd2ed9f8ae92 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 13:45:05 -0500 Subject: [PATCH 64/76] constants: does graph_test.py need --create-namespace? --- src/warnet/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index adeae493e..b1502e996 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -16,7 +16,7 @@ INGRESS_NAMESPACE = "ingress" WARGAMES_NAMESPACE_PREFIX = "wargames-" KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] -HELM_COMMAND = "helm upgrade --install" +HELM_COMMAND = "helm upgrade --install --create-namespace" TANK_MISSION = "tank" COMMANDER_MISSION = "commander" From 506bd58b5f0b71fd46b82bf15028be9da017574e Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:54:52 -0500 Subject: [PATCH 65/76] k8s: get raw config values from kubectl --- 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 6cc458d64..5322cfb8d 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -391,7 +391,7 @@ def write_file_to_container( def get_kubeconfig_value(jsonpath): - command = f"kubectl config view --minify -o jsonpath={jsonpath}" + command = f"kubectl config view --minify --raw -o jsonpath={jsonpath}" return run_command(command) From 02cf9e53bbe55b06f071191792cb406a007d9533 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:55:40 -0500 Subject: [PATCH 66/76] k8s: get cluster from current context --- src/warnet/k8s.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 5322cfb8d..c664f9c91 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -395,6 +395,48 @@ def get_kubeconfig_value(jsonpath): return run_command(command) +def get_cluster_of_current_context(kubeconfig_data: dict) -> dict: + # Get the current context name + current_context_name = kubeconfig_data.get("current-context") + + if not current_context_name: + raise K8sError("No current context found in kubeconfig.") + + # Find the context entry for the current context + context_entry = next( + ( + context + for context in kubeconfig_data.get("contexts", []) + if context["name"] == current_context_name + ), + None, + ) + + if not context_entry: + raise K8sError(f"Context '{current_context_name}' not found in kubeconfig.") + + # Get the cluster name from the context entry + cluster_name = context_entry.get("context", {}).get("cluster") + + if not cluster_name: + raise K8sError(f"Cluster not specified in context '{current_context_name}'.") + + # Find the cluster entry associated with the cluster name + cluster_entry = next( + ( + cluster + for cluster in kubeconfig_data.get("clusters", []) + if cluster["name"] == cluster_name + ), + None, + ) + + if not cluster_entry: + raise K8sError(f"Cluster '{cluster_name}' not found in kubeconfig.") + + return cluster_entry + + def get_namespaces() -> list[V1Namespace]: sclient = get_static_client() try: From a9239a7f3ec2a3943eda522c4bce83515b00e195 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:54:28 -0500 Subject: [PATCH 67/76] auth: update auth func to avoid flattening --- src/warnet/users.py | 157 +++++++++++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 52 deletions(-) diff --git a/src/warnet/users.py b/src/warnet/users.py index c85e53585..d061f08a9 100644 --- a/src/warnet/users.py +++ b/src/warnet/users.py @@ -1,70 +1,123 @@ +import difflib +import json import os -import subprocess import sys import click -import yaml + +from warnet.constants import KUBECONFIG +from warnet.k8s import K8sError, open_kubeconfig, write_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 - """ +@click.argument("auth_config", type=str) +def auth(auth_config): + """Authenticate with a Warnet cluster using a kubernetes 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 - ) - except subprocess.CalledProcessError as e: - click.secho("Error occurred while executing kubectl config view --flatten:", fg="red") - click.secho(e.stderr, fg="red") + 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) - 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) + is_first_config = False + if not os.path.exists(KUBECONFIG): + try: + write_kubeconfig(auth_config, KUBECONFIG) + 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) 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 - ) - 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") + 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) - with open(current_kubeconfig) as file: - contents = yaml.safe_load(file) + if not is_first_config: + for category in ["clusters", "users", "contexts"]: + if category in auth_config: + merge_entries(category, base_config, auth_config) - with open(current_kubeconfig, "w") as file: - contents["current-context"] = current_context - yaml.safe_dump(contents, file) + new_current_context = auth_config.get("current-context") + base_config["current-context"] = new_current_context - with open(current_kubeconfig) as file: - contents = yaml.safe_load(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"\nwarnet's current context is now set to: {contents['current-context']}", fg="green" + f"Warning: The context '{new_current_context}' does not have an explicit namespace.", + fg="yellow", ) + + try: + write_kubeconfig(base_config, KUBECONFIG) + 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: + 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) + + +def merge_entries(category, base_config, auth_config): + name = "name" + base_list = base_config.setdefault(category, []) + auth_list = auth_config[category] + base_entry_names = {entry[name] for entry in base_list} # Extract existing names + for auth_entry in auth_list: + if auth_entry[name] in base_entry_names: + existing_entry = next( + base_entry for base_entry in base_list if base_entry[name] == auth_entry[name] + ) + if existing_entry != auth_entry: + # Show diff between existing and new entry + existing_entry_str = json.dumps(existing_entry, indent=2, sort_keys=True) + auth_entry_str = json.dumps(auth_entry, indent=2, sort_keys=True) + diff = difflib.unified_diff( + existing_entry_str.splitlines(), + auth_entry_str.splitlines(), + fromfile="Existing Entry", + tofile="New Entry", + lineterm="", + ) + click.echo("Differences between existing and new entry:\n") + click.echo("\n".join(diff)) + + if click.confirm( + f"The '{category}' section key '{auth_entry[name]}' already exists and differs. Overwrite?", + default=False, + ): + # Find and replace the existing entry + base_list[:] = [ + base_entry if base_entry[name] != auth_entry[name] else auth_entry + for base_entry in base_list + ] + click.secho( + f"Overwrote '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Skipped '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Entry for '{category}' section key '{auth_entry[name]}' is identical. No changes made.", + fg="blue", + ) + else: + base_list.append(auth_entry) + click.secho(f"Added new '{category}' section key '{auth_entry[name]}'", fg="green") From ed5fb0014b1b18ea1f2d7a6a1c585eebba24a5f1 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 12:56:15 -0500 Subject: [PATCH 68/76] admin: get raw cluster for auth file --- src/warnet/admin.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index 13398486e..233a220e9 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -1,12 +1,19 @@ import os +import sys from pathlib import Path import click import yaml from rich import print as richprint -from .constants import NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX -from .k8s import get_kubeconfig_value, get_namespaces_by_type, get_service_accounts_in_namespace +from .constants import KUBECONFIG, NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX +from .k8s import ( + K8sError, + get_cluster_of_current_context, + get_namespaces_by_type, + get_service_accounts_in_namespace, + open_kubeconfig, +) from .namespaces import copy_namespaces_defaults, namespaces from .network import copy_network_defaults from .process import run_command @@ -54,9 +61,14 @@ def create_kubeconfigs(kubeconfig_dir, token_duration): """Create kubeconfig files for ServiceAccounts""" kubeconfig_dir = os.path.expanduser(kubeconfig_dir) - cluster_name = get_kubeconfig_value("{.clusters[0].name}") - cluster_server = get_kubeconfig_value("{.clusters[0].cluster.server}") - cluster_ca = get_kubeconfig_value("{.clusters[0].cluster.certificate-authority-data}") + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {KUBECONFIG}", fg="red") + sys.exit(1) + + cluster = get_cluster_of_current_context(kubeconfig_data) os.makedirs(kubeconfig_dir, exist_ok=True) @@ -93,24 +105,15 @@ def create_kubeconfigs(kubeconfig_dir, token_duration): # might not be worth it since we are just reading the yaml to then create a bunch of values and its not # actually used to deploy anything into the cluster # Then benefit would be making this code a bit cleaner and easy to follow, fwiw - kubeconfig_dict = { "apiVersion": "v1", "kind": "Config", - "clusters": [ - { - "name": cluster_name, - "cluster": { - "server": cluster_server, - "certificate-authority-data": cluster_ca, - }, - } - ], + "clusters": [cluster], "users": [{"name": sa, "user": {"token": token}}], "contexts": [ { "name": f"{sa}-{namespace}", - "context": {"cluster": cluster_name, "namespace": namespace, "user": sa}, + "context": {"cluster": cluster["name"], "namespace": namespace, "user": sa}, } ], "current-context": f"{sa}-{namespace}", From 6ecc017f64327aa559833475246aa5e75b473d9a Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 4 Oct 2024 17:26:28 -0500 Subject: [PATCH 69/76] bitcoin: update `message` to take tank-a.namespace --- src/warnet/bitcoin.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 1f478a6cc..9d0c54f50 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -130,14 +130,31 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): @click.argument("tank_a", type=str, required=True) @click.argument("tank_b", type=str, required=True) @click.option("--chain", default="regtest", show_default=True) -@click.option("--namespace_a", default=None, show_default=True) -@click.option("--namespace_b", default=None, show_default=True) -def messages( - tank_a: str, tank_b: str, chain: str, namespace_a: Optional[str], namespace_b: Optional[str] -): +def messages(tank_a: str, tank_b: str, chain: str): """ Fetch messages sent between and in [chain] + + Optionally, include a namespace like so: tank-name.namespace """ + + def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: + tank_split = tank.split(".") + try: + namespace = tank_split[1] + except IndexError: + namespace = None + return tank_split[0], namespace + + tank_a_split = tank_a.split(".") + tank_b_split = tank_b.split(".") + if len(tank_a_split) > 2 or len(tank_b_split) > 2: + click.secho("Accepted formats: tank-name OR tank-name.namespace") + click.secho(f"Foramts found: {tank_a} {tank_b}") + sys.exit(1) + + tank_a, namespace_a = parse_name_and_namespace(tank_a) + tank_b, namespace_b = parse_name_and_namespace(tank_b) + try: namespace_a = get_default_namespace_or(namespace_a) namespace_b = get_default_namespace_or(namespace_b) From 21815f09f43ae2b3255ff98d1d8118c971a5a6f7 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 7 Oct 2024 08:31:18 -0500 Subject: [PATCH 70/76] deploy: fix override path --- src/warnet/deploy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 63c888e30..f32156c41 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -273,7 +273,7 @@ def deploy_namespaces(directory: Path): for namespace in namespaces_file["namespaces"]: click.echo(f"Deploying namespace: {namespace.get('name')}") try: - temp_override_file_path = Path() + temp_override_file_path = "" namespace_name = namespace.get("name") namespace_config_override = {k: v for k, v in namespace.items() if k != "name"} @@ -294,7 +294,7 @@ def deploy_namespaces(directory: Path): click.echo(f"Error: {e}") return finally: - if temp_override_file_path.exists(): + if temp_override_file_path: temp_override_file_path.unlink() From 1024bcf185a32afe6a93f39edc232042cfaba667 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 7 Oct 2024 08:32:00 -0500 Subject: [PATCH 71/76] `admin.md`: reword deploy documentation --- docs/admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin.md b/docs/admin.md index 16b9a5a5d..b888de3d8 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -61,7 +61,7 @@ Before letting the users into your cluster, make sure to create a network of tan ```shell -$ warnet admin deploy networks/mynet --to-all-users +$ warnet deploy networks/mynet --to-all-users ``` Observe that the *wargames-red-team* namespace now has tanks in it. From 8024236b911b83da9665e0a3381137b910203e0f Mon Sep 17 00:00:00 2001 From: Max Edwards Date: Mon, 7 Oct 2024 15:52:02 +0100 Subject: [PATCH 72/76] removing --create-namespace from constants --- src/warnet/constants.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index b1502e996..25b583352 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -16,7 +16,7 @@ INGRESS_NAMESPACE = "ingress" WARGAMES_NAMESPACE_PREFIX = "wargames-" KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] -HELM_COMMAND = "helm upgrade --install --create-namespace" +HELM_COMMAND = "helm upgrade --install" TANK_MISSION = "tank" COMMANDER_MISSION = "commander" @@ -103,10 +103,10 @@ "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", "helm repo update", f"helm upgrade --install --namespace warnet-logging --create-namespace --values {MANIFESTS_DIR}/loki_values.yaml loki grafana/loki --version 5.47.2", - "helm upgrade --install --namespace warnet-logging promtail grafana/promtail", - "helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --set grafana.enabled=false", - f"helm upgrade --install grafana-dashboards {CHARTS_DIR}/grafana-dashboards --namespace warnet-logging", - f"helm upgrade --install --namespace warnet-logging loki-grafana grafana/grafana --values {MANIFESTS_DIR}/grafana_values.yaml", + "helm upgrade --install --namespace warnet-logging promtail grafana/promtail --create-namespace", + "helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --create-namespace --set grafana.enabled=false", + f"helm upgrade --install grafana-dashboards {CHARTS_DIR}/grafana-dashboards --namespace warnet-logging --create-namespace", + f"helm upgrade --install --namespace warnet-logging --create-namespace loki-grafana grafana/grafana --values {MANIFESTS_DIR}/grafana_values.yaml", ] From 82cf6873ab9962c3d5533e9595945deee74e7173 Mon Sep 17 00:00:00 2001 From: Max Edwards Date: Mon, 7 Oct 2024 15:52:02 +0100 Subject: [PATCH 73/76] removing --create-namespace from constants --- src/warnet/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index f32156c41..35648e72f 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -173,7 +173,7 @@ def deploy_fork_observer(directory: Path, debug: bool) -> bool: default_namespace = get_default_namespace() namespace = LOGGING_NAMESPACE - cmd = f"{HELM_COMMAND} 'fork-observer' {FORK_OBSERVER_CHART} --namespace {namespace}" + cmd = f"{HELM_COMMAND} 'fork-observer' {FORK_OBSERVER_CHART} --namespace {namespace} --create-namespace" if debug: cmd += " --debug" From d9b3e1d7c697dd7248ea638a8ba8d5ca3280e438 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 7 Oct 2024 16:01:40 -0500 Subject: [PATCH 74/76] gitignore: add kubeconfigs dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c42f6ba7f..f4b5d0076 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ warnet.egg-info .env dist/ build/ +**/kubeconfigs/ From 6f18eb802bbfc4c31a3cbe7404fd8583d406dcf3 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 7 Oct 2024 18:12:31 -0500 Subject: [PATCH 75/76] k8s: use mv to prevent scenario getting cut off We use this following to check for the scenario file: while [ ! -f /shared/archive.pyz ]; do echo "Waiting for /shared/archive.pyz to exist..." sleep 2 done I suspect this may cut off longer-running copy commands. Using mv we make sure all the data is copied over, and then we `mv` the file into place. --- src/warnet/k8s.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index c664f9c91..416d31ae3 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -368,7 +368,7 @@ def write_file_to_container( ): namespace = get_default_namespace_or(namespace) sclient = get_static_client() - exec_command = ["sh", "-c", f"cat > {dst_path}"] + exec_command = ["sh", "-c", f"cat > {dst_path}.tmp"] try: res = stream( sclient.connect_get_namespaced_pod_exec, @@ -384,6 +384,18 @@ def write_file_to_container( ) res.write_stdin(data) res.close() + rename_command = ["sh", "-c", f"mv {dst_path}.tmp {dst_path}"] + stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=rename_command, + container=container_name, + stdin=False, + stderr=True, + stdout=True, + tty=False, + ) print(f"Successfully copied data to {pod_name}({container_name}):{dst_path}") return True except Exception as e: From 424eba8b7a75d590026f189edaaf75b1eca6dc92 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 7 Oct 2024 18:26:04 -0500 Subject: [PATCH 76/76] k8s: add `sync` to make sure the data is written --- 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 416d31ae3..9354eb903 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -368,7 +368,7 @@ def write_file_to_container( ): namespace = get_default_namespace_or(namespace) sclient = get_static_client() - exec_command = ["sh", "-c", f"cat > {dst_path}.tmp"] + exec_command = ["sh", "-c", f"cat > {dst_path}.tmp && sync"] try: res = stream( sclient.connect_get_namespaced_pod_exec,