From fa75fb56a11cf578fd66e5589e1e06847fcac13b Mon Sep 17 00:00:00 2001 From: josibake Date: Fri, 13 Sep 2024 13:08:31 +0200 Subject: [PATCH 1/5] 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 48629bf1ecdf86313f586051dc238df771ee6a36 Mon Sep 17 00:00:00 2001 From: Grant Date: Sat, 7 Sep 2024 01:52:06 -0500 Subject: [PATCH 2/5] add create-kubeconfigs to `warnet admin` Replace the setup_contexts.sh script into a proper warnet command. Co-authored-by: mplsgrant <58152638+mplsgrant@users.noreply.github.com> --- src/warnet/admin.py | 92 +++++++++++++++++++++++++++++++++++++++++++++ src/warnet/k8s.py | 24 ++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/warnet/admin.py b/src/warnet/admin.py index f194e16bd..a60f66753 100644 --- a/src/warnet/admin.py +++ b/src/warnet/admin.py @@ -5,8 +5,10 @@ from rich import print as richprint from .constants import NETWORK_DIR +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.argument("prefix", type=str, required=True) +@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(prefix: str, 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(prefix) + + for namespace in warnet_namespaces: + 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.") diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 37f5d38f1..2073872ef 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -282,3 +282,27 @@ def get_ingress_ip_or_host(): except Exception as e: print(f"Error getting ingress IP: {e}") return None + + +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 acc63edc216443c42d76e175357a4264dcadd3e7 Mon Sep 17 00:00:00 2001 From: josibake Date: Fri, 13 Sep 2024 14:14:10 +0200 Subject: [PATCH 3/5] remove hardcoded warnet- prefix from namespaces deploy --- src/warnet/deploy.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index c175dd6d9..9448ffdf9 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -235,14 +235,6 @@ def deploy_namespaces(directory: Path): with namespaces_file_path.open() as f: namespaces_file = yaml.safe_load(f) - 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." - ) - return - for namespace in namespaces_file["namespaces"]: click.echo(f"Deploying namespace: {namespace.get('name')}") try: From e3f517e429dd9a844725721d5200f311cb085025 Mon Sep 17 00:00:00 2001 From: josibake Date: Mon, 16 Sep 2024 11:05:34 +0200 Subject: [PATCH 4/5] add --namespace flag to deploy allow the user to specify a namespace to deploy into (overrides kubectl default namespace) --- src/warnet/deploy.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py index 9448ffdf9..3cfbb9fc3 100644 --- a/src/warnet/deploy.py +++ b/src/warnet/deploy.py @@ -42,19 +42,23 @@ def validate_directory(ctx, param, value): callback=validate_directory, ) @click.option("--debug", is_flag=True) -def deploy(directory, debug): +@click.option("--namespace", "-n", type=str, help="Namespace to deploy network into (overrides the current namespace in kubectl)") +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, namespace, debug) df = deploy_fork_observer(directory, debug) if dl | df: deploy_ingress(debug) deploy_caddy(directory, debug) elif (directory / NAMESPACES_FILE).exists(): - deploy_namespaces(directory) + if namespace: + click.echo("Cannot specify a --namespace when deploying a namespaces chart.") + else: + deploy_namespaces(directory) else: click.echo( "Error: Neither network.yaml nor namespaces.yaml found in the specified directory." @@ -189,14 +193,14 @@ def deploy_fork_observer(directory: Path, debug: bool) -> bool: return True -def deploy_network(directory: Path, debug: bool = False): +def deploy_network(directory: Path, namespace_override: str, debug: bool = False): 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() + namespace = namespace_override if namespace_override else get_default_namespace() for node in network_file["nodes"]: click.echo(f"Deploying node: {node.get('name')}") From f84777afeff6bf0c2a942be0317dd5cbc465fad6 Mon Sep 17 00:00:00 2001 From: josie Date: Thu, 26 Sep 2024 16:54:11 +0200 Subject: [PATCH 5/5] operate on a namespace group --- .../namespaces/templates/namespace.yaml | 2 ++ resources/charts/namespaces/values.yaml | 3 +- src/warnet/constants.py | 1 + src/warnet/control.py | 12 ++++---- src/warnet/k8s.py | 30 +++++++++++++------ 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/resources/charts/namespaces/templates/namespace.yaml b/resources/charts/namespaces/templates/namespace.yaml index 5e01eebed..5709cf499 100644 --- a/resources/charts/namespaces/templates/namespace.yaml +++ b/resources/charts/namespaces/templates/namespace.yaml @@ -2,3 +2,5 @@ apiVersion: v1 kind: Namespace metadata: name: {{ .Values.namespaceName | default .Release.Name }} + labels: + type: {{ .Values.type }} diff --git a/resources/charts/namespaces/values.yaml b/resources/charts/namespaces/values.yaml index 61f946879..99d411363 100644 --- a/resources/charts/namespaces/values.yaml +++ b/resources/charts/namespaces/values.yaml @@ -1,3 +1,4 @@ +type: "assets" users: - name: warnet-user roles: @@ -37,4 +38,4 @@ roles: verbs: ["get", "list"] - apiGroups: [""] resources: ["events"] - verbs: ["get"] \ No newline at end of file + verbs: ["get"] diff --git a/src/warnet/constants.py b/src/warnet/constants.py index c01e8c2b4..f3b36f71d 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -15,6 +15,7 @@ LOGGING_NAMESPACE = "warnet-logging" INGRESS_NAMESPACE = "ingress" HELM_COMMAND = "helm upgrade --install --create-namespace" +WARNET_ASSETS = "assets" # Directories and files for non-python assets, e.g., helm charts, example scenarios, default configs SRC_DIR = files("warnet") diff --git a/src/warnet/control.py b/src/warnet/control.py index 782764cd9..ccc9c186e 100644 --- a/src/warnet/control.py +++ b/src/warnet/control.py @@ -132,8 +132,9 @@ def delete_pod(pod_name, namespace): # Delete remaining pods pods = get_pods() - for pod in pods.items: - futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) + for pod_list in pods: + for pod in pod_list.items: + futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) # Wait for all tasks to complete and print results for future in as_completed(futures): @@ -159,9 +160,10 @@ def get_active_network(namespace): @click.command(context_settings={"ignore_unknown_options": True}) +@click.option("--namespace", "-n", type=str, help="Namespace to run scenario in (overrides the current namespace in kubectl)") @click.argument("scenario_file", type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -def run(scenario_file: str, additional_args: tuple[str]): +def run(namespace: str, scenario_file: str, additional_args: tuple[str]): """ Run a scenario from a file. Pass `-- --help` to get individual scenario help @@ -173,7 +175,7 @@ def run(scenario_file: str, additional_args: tuple[str]): scenario_data = base64.b64encode(file.read()).decode() name = f"commander-{scenario_name.replace('_', '')}-{int(time.time())}" - namespace = get_default_namespace() + ns = namespace if namespace else get_default_namespace() tankpods = get_mission("tank") tanks = [ { @@ -198,7 +200,7 @@ def run(scenario_file: str, additional_args: tuple[str]): "upgrade", "--install", "--namespace", - namespace, + ns, "--set", f"fullnameOverride={name}", "--set", diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py index 2073872ef..3f365e287 100644 --- a/src/warnet/k8s.py +++ b/src/warnet/k8s.py @@ -6,7 +6,8 @@ import yaml from kubernetes import client, config, watch -from kubernetes.client.models import CoreV1Event, V1PodList +from kubernetes.client.models import CoreV1Event, V1PodList, V1NamespaceList, V1Pod +from kubernetes.client.rest import ApiException from kubernetes.dynamic import DynamicClient from kubernetes.stream import stream @@ -16,6 +17,7 @@ INGRESS_NAMESPACE, KUBECONFIG, LOGGING_NAMESPACE, + WARNET_ASSETS, ) from .process import run_command, stream_command @@ -32,19 +34,23 @@ def get_dynamic_client() -> DynamicClient: def get_pods() -> V1PodList: 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 = [] + namespaces: V1NamespaceList = get_namespaces_by_warnet_type(WARNET_ASSETS) + for ns in namespaces.items: + try: + pods.append(sclient.list_namespaced_pod(ns.metadata.name)) + except Exception as e: + raise e + return pods def get_mission(mission: str) -> list[V1PodList]: pods = get_pods() crew = [] - for pod in pods.items: - if "mission" in pod.metadata.labels and pod.metadata.labels["mission"] == mission: - crew.append(pod) + for pod_list in pods: + for pod in pod_list.items: + if "mission" in pod.metadata.labels and pod.metadata.labels["mission"] == mission: + crew.append(pod) return crew @@ -306,3 +312,9 @@ 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 get_namespaces_by_warnet_type(warnet_type: str) -> list[V1NamespaceList]: + sclient = get_static_client() + namespaces = sclient.list_namespace(label_selector=f"type={warnet_type}") + return namespaces