From 5730de845191cd6a53cf00e0e7eb5818d5699ba8 Mon Sep 17 00:00:00 2001 From: shireenf-ibm Date: Thu, 10 Nov 2022 09:45:32 +0200 Subject: [PATCH 1/4] try loading from live cluster silently unless k8s/istio/calico is provided explicitly by user Signed-off-by: shireenf-ibm --- docs/CommonQueryPatterns.md | 6 ++- nca/NetworkConfig/ResourcesHandler.py | 50 ++++++++++++++++--- nca/Utils/CmdlineRunner.py | 17 +++++-- .../demo_short/demo2-scheme.yaml | 5 +- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/docs/CommonQueryPatterns.md b/docs/CommonQueryPatterns.md index a7324c60c..03dbe4a06 100644 --- a/docs/CommonQueryPatterns.md +++ b/docs/CommonQueryPatterns.md @@ -14,7 +14,8 @@ Patterns describing how to combine specific switches (global: `-- , `-- --resource_list --base_resource_list ` [see example here](../tests/k8s_cmdline_tests.yaml#L293-L302) ##### Handling missing resources: -- When running without any switch (i.e. `--`), all resources will be loaded from k8s live cluster. +- When running without any switch (i.e. `--`), nca checks if a communication with k8s live cluster is available, if yes - resources will be loaded from k8s live cluster, +otherwise, the query runs on empty resources (empty query result) Running with switches: - When running with specific topology switches only (using only `pod_list` and `ns_list`) without providing networkPolicy path, policies will be loaded from k8s live cluster @@ -47,7 +48,8 @@ Running with switches: `resourceList: [list of networkPolicies, namespaces and pods paths]` [see example here ](../tests/k8s_testcases/example_policies/resourcelist-one-path-example/resource-path-scheme.yaml#L3-L7) ##### Handling missing resources: -- If global scope does not exist, topology objects will be loaded from k8s live cluster. +- If global scope does not exist, nca checks if a communication with k8s live cluster is available, if yes - topology resources will be loaded from k8s live cluster, +otherwise, an empty peer container is created - If `networkPolicyList` is not used and `resourceList` does not refer to any policy, a query reading this considers empty network-policies list. - If global pods are missing (i.e. `podList` is not used and `resourceList` does not refer to any pod), global cluster will have 0 endpoints. - If config's pods are missing, global pods will be used diff --git a/nca/NetworkConfig/ResourcesHandler.py b/nca/NetworkConfig/ResourcesHandler.py index 00d1b2f9e..93faf8d30 100644 --- a/nca/NetworkConfig/ResourcesHandler.py +++ b/nca/NetworkConfig/ResourcesHandler.py @@ -5,6 +5,7 @@ import copy from enum import Enum from nca.FileScanners.GenericTreeScanner import TreeScannerFactory +from nca.Utils.CmdlineRunner import CmdlineRunner from .NetworkConfig import NetworkConfig from .PoliciesFinder import PoliciesFinder from .TopologyObjectsFinder import PodsFinder, NamespacesFinder, ServicesFinder @@ -83,14 +84,14 @@ def _set_config_peer_container(self, ns_list, pod_list, resource_list, config_na peer_container = copy.deepcopy(self.global_peer_container) else: # the specific networkConfig has no topology input resources (not private, neither global) # this case is reachable when: - # 1. no input paths are provided at all, i.e. user intended to get resources from live cluster + # 1. no input paths are provided at all, then try to load from live cluster silently + # if communication fails then build an empty peer container # 2. paths are provided only using resourceList flag, but no topology objects found; # in this case we will not load topology from live cluster - keeping peer container empty if resource_list is None: # getting here means ns_list and pod_list are None too - print('loading topology objects from k8s live cluster') - resources_parser.load_resources_from_k8s_live_cluster([ResourceType.Namespaces, ResourceType.Pods]) + resources_parser.try_to_load_topology_from_live_cluster([ResourceType.Namespaces, ResourceType.Pods], + config_name) peer_container = resources_parser.build_peer_container(config_name) - if save_flag: # if called from scheme with global topology or cmdline with 2 configs query self.global_peer_container = peer_container self.global_ns_finder = resources_parser.ns_finder @@ -214,9 +215,8 @@ def parse_lists_for_policies(self, np_list, resource_list, peer_container): elif resource_list: self._parse_resources_path(resource_list, [ResourceType.Policies]) config_name = resource_list[0] - else: # running without any input flags means running on k8s live cluster - print('loading policies from k8s live cluster') - self.load_resources_from_k8s_live_cluster([ResourceType.Policies]) + else: # running without any input flags - try to load from k8s live cluster silently + self.try_to_load_topology_from_live_cluster([ResourceType.Policies]) return config_name @@ -257,7 +257,17 @@ def _parse_resources_path(self, resource_list, resource_flags): self.policies_finder.parse_policies_in_parse_queue() - def load_resources_from_k8s_live_cluster(self, resource_flags): + def load_resources_from_k8s_live_cluster(self, resource_flags, run_silently=False): + """ + attempt to load the resources in resource_flags from k8s live cluster + :param list resource_flags: resource types to load from k8s live cluster + :param bool run_silently: indicates if this attempt should run silently, i.e. ignore errors if fails to + communicate. + """ + # Setting a flag in the CmdlineRunner to indicate if we are trying to load resources silently + # from the live cluster (e.g. global resources are missing) + CmdlineRunner.ignore_live_cluster_err = run_silently + if ResourceType.Namespaces in resource_flags: self.ns_finder.load_ns_from_live_cluster() if ResourceType.Pods in resource_flags: @@ -268,6 +278,30 @@ def load_resources_from_k8s_live_cluster(self, resource_flags): if ResourceType.Policies in resource_flags: self.policies_finder.load_policies_from_k8s_cluster() + def try_to_load_topology_from_live_cluster(self, resources_flags, config_name='global'): + """ + an attempt to load resources from k8s live cluster silently. + in this case, communication with a k8s live cluster is not a must. + so the attempt occurs silently, if succeed to connect and load resources then a relevant message will be printed + otherwise, a warning message of not found resources will be printed + :param list resources_flags: resource types to load from k8s live cluster + :param str config_name: configuration name + """ + try: + self.load_resources_from_k8s_live_cluster(resources_flags, run_silently=True) + if ResourceType.Policies in resources_flags: + success = self.policies_finder.policies_container.policies + else: + success = self.ns_finder.namespaces or self.pods_finder.peer_set + except FileNotFoundError: # in case that kube-config file is not found + success = False # ignore the exception since this is a silent try + + resource_names = ' and '.join(str(resource).split('.')[1].lower() for resource in resources_flags) + if success: # we got resources from live cluster + print(f'{config_name}: loading {resource_names} from k8s live cluster') + else: + print(f'Warning: {config_name} - {resource_names} were not found') + def _handle_calico_inputs(self, resource_flags): if ResourceType.Namespaces in resource_flags: self.ns_finder.load_ns_from_live_cluster() diff --git a/nca/Utils/CmdlineRunner.py b/nca/Utils/CmdlineRunner.py index 55d2d7e7f..5ffdb50ae 100644 --- a/nca/Utils/CmdlineRunner.py +++ b/nca/Utils/CmdlineRunner.py @@ -9,6 +9,7 @@ import subprocess import os +import sys from fnmatch import fnmatch @@ -16,16 +17,24 @@ class CmdlineRunner: """ A stateless class with only static functions to easily get k8s and calico resources using kubectl and calicoctl """ + # a static variable to indicate if we want to ignore errors from running executable command + ignore_live_cluster_err = False + @staticmethod def run_and_get_output(cmdline_list): """ Run an executable with specific arguments and return its output to stdout + if a communicate error occurs, it will be ignored in case this is a silent try to communicate with live cluster, + otherwise, will be printed to stderr :param list[str] cmdline_list: A list of arguments, the first of which is the executable path - :return: The executable's output to stdout + :return: The executable's output to stdout ( a list-resources on success, otherwise empty value) :rtype: str """ - cmdline_process = subprocess.Popen(cmdline_list, stdout=subprocess.PIPE) - return cmdline_process.communicate()[0] + cmdline_process = subprocess.Popen(cmdline_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = cmdline_process.communicate() + if err and not CmdlineRunner.ignore_live_cluster_err: + print(err.decode().strip('\n'), file=sys.stderr) + return out @staticmethod def search_file_in_path(filename, search_path): @@ -72,7 +81,7 @@ def locate_kube_config_file(): kube_config_file = os.path.join(home_dir, file) os.environ['KUBECONFIG'] = kube_config_file return - raise Exception('Failed to locate Kubernetes configuration files') + raise FileNotFoundError('Failed to locate Kubernetes configuration files') @staticmethod def get_k8s_resources(resource): diff --git a/tests/k8s_testcases/example_policies/demo_short/demo2-scheme.yaml b/tests/k8s_testcases/example_policies/demo_short/demo2-scheme.yaml index 327955cda..847120a01 100644 --- a/tests/k8s_testcases/example_policies/demo_short/demo2-scheme.yaml +++ b/tests/k8s_testcases/example_policies/demo_short/demo2-scheme.yaml @@ -1,8 +1,7 @@ -namespaceList: ../../example_podlist/ns_list.json -podList: ../../example_podlist/pods_list.json - networkConfigList: - name: sanity_np2 + namespaceList: ../../example_podlist/ns_list.json + podList: ../../example_podlist/pods_list.json networkPolicyList: - sanity2-networkpolicy.yaml expectedWarnings: 0 From 618a098621e0eb9df1b24ec851d48f9361eb1647 Mon Sep 17 00:00:00 2001 From: shireenf-ibm Date: Thu, 10 Nov 2022 12:09:55 +0200 Subject: [PATCH 2/4] helm case Signed-off-by: shireenf-ibm --- nca/Utils/CmdlineRunner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nca/Utils/CmdlineRunner.py b/nca/Utils/CmdlineRunner.py index 5ffdb50ae..97a2cc8af 100644 --- a/nca/Utils/CmdlineRunner.py +++ b/nca/Utils/CmdlineRunner.py @@ -21,18 +21,19 @@ class CmdlineRunner: ignore_live_cluster_err = False @staticmethod - def run_and_get_output(cmdline_list): + def run_and_get_output(cmdline_list, helm_flag=False): """ Run an executable with specific arguments and return its output to stdout if a communicate error occurs, it will be ignored in case this is a silent try to communicate with live cluster, otherwise, will be printed to stderr :param list[str] cmdline_list: A list of arguments, the first of which is the executable path + :param helm_flag: indicates if the executable is helm - communicate errors are always considered for helm :return: The executable's output to stdout ( a list-resources on success, otherwise empty value) :rtype: str """ cmdline_process = subprocess.Popen(cmdline_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = cmdline_process.communicate() - if err and not CmdlineRunner.ignore_live_cluster_err: + if err and (not CmdlineRunner.ignore_live_cluster_err or helm_flag): print(err.decode().strip('\n'), file=sys.stderr) return out @@ -104,4 +105,4 @@ def resolve_helm_chart(chart_dir): :return: The resolved yaml files generated from the chart file """ cmdline_list = ['helm', 'template', 'nca-extract', chart_dir] - return CmdlineRunner.run_and_get_output(cmdline_list) + return CmdlineRunner.run_and_get_output(cmdline_list, helm_flag=True) From 5b8d42f0b2818c8f594d98a07bf834a8a845bf5c Mon Sep 17 00:00:00 2001 From: shireenf-ibm Date: Thu, 10 Nov 2022 12:38:33 +0200 Subject: [PATCH 3/4] general case Signed-off-by: shireenf-ibm --- nca/Utils/CmdlineRunner.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nca/Utils/CmdlineRunner.py b/nca/Utils/CmdlineRunner.py index 97a2cc8af..834ec5b74 100644 --- a/nca/Utils/CmdlineRunner.py +++ b/nca/Utils/CmdlineRunner.py @@ -17,23 +17,26 @@ class CmdlineRunner: """ A stateless class with only static functions to easily get k8s and calico resources using kubectl and calicoctl """ - # a static variable to indicate if we want to ignore errors from running executable command + # a static variable to indicate if we want to ignore errors from running executable command - i.e. run silently ignore_live_cluster_err = False @staticmethod - def run_and_get_output(cmdline_list, helm_flag=False): + def run_and_get_output(cmdline_list, check_for_silent_exec=False): """ Run an executable with specific arguments and return its output to stdout if a communicate error occurs, it will be ignored in case this is a silent try to communicate with live cluster, otherwise, will be printed to stderr :param list[str] cmdline_list: A list of arguments, the first of which is the executable path - :param helm_flag: indicates if the executable is helm - communicate errors are always considered for helm + :param check_for_silent_exec: when true consider the static variable that indicates whether to ignore errors + or not :return: The executable's output to stdout ( a list-resources on success, otherwise empty value) :rtype: str """ cmdline_process = subprocess.Popen(cmdline_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = cmdline_process.communicate() - if err and (not CmdlineRunner.ignore_live_cluster_err or helm_flag): + print_err_flag = not check_for_silent_exec or \ + (check_for_silent_exec and not CmdlineRunner.ignore_live_cluster_err) + if err and print_err_flag: print(err.decode().strip('\n'), file=sys.stderr) return out @@ -95,7 +98,7 @@ def get_k8s_resources(resource): cmdline_list = ['kubectl', 'get', resource, '-o=json'] if resource in ['networkPolicy', 'authorizationPolicy', 'pod', 'ingress', 'Gateway', 'VirtualService', 'sidecar']: cmdline_list.append('--all-namespaces') - return CmdlineRunner.run_and_get_output(cmdline_list) + return CmdlineRunner.run_and_get_output(cmdline_list, check_for_silent_exec=True) @staticmethod def resolve_helm_chart(chart_dir): @@ -105,4 +108,4 @@ def resolve_helm_chart(chart_dir): :return: The resolved yaml files generated from the chart file """ cmdline_list = ['helm', 'template', 'nca-extract', chart_dir] - return CmdlineRunner.run_and_get_output(cmdline_list, helm_flag=True) + return CmdlineRunner.run_and_get_output(cmdline_list) From 36c8f3149b4b665dcd5789ebe17cb736f2e831e7 Mon Sep 17 00:00:00 2001 From: shireenf-ibm Date: Thu, 10 Nov 2022 12:45:54 +0200 Subject: [PATCH 4/4] lint fix Signed-off-by: shireenf-ibm --- nca/Utils/CmdlineRunner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nca/Utils/CmdlineRunner.py b/nca/Utils/CmdlineRunner.py index 834ec5b74..eda05a7e1 100644 --- a/nca/Utils/CmdlineRunner.py +++ b/nca/Utils/CmdlineRunner.py @@ -34,8 +34,8 @@ def run_and_get_output(cmdline_list, check_for_silent_exec=False): """ cmdline_process = subprocess.Popen(cmdline_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = cmdline_process.communicate() - print_err_flag = not check_for_silent_exec or \ - (check_for_silent_exec and not CmdlineRunner.ignore_live_cluster_err) + print_err_flag = \ + not check_for_silent_exec or (check_for_silent_exec and not CmdlineRunner.ignore_live_cluster_err) if err and print_err_flag: print(err.decode().strip('\n'), file=sys.stderr) return out