""" Kubernetes Actuator Manager

	This module implements a simple Actuator Manager for Kubernetes.
	It discovers Kubernetes resources by invoking its APIs. 

	The documentation of the Kubernetes API is here: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md.


	The actuator-specific configuration includes:
		
		- ``auth``:

			- ``api_key``: A token generated by the Kubernetes cluster for the service account used for context discovery.
			- ``host``: DNS name or IP address of the Kubernetes endpoint (including port number)

		- ``config``:
		
			- ``dns``: DNS suffix for this cluster (needed to create unique names for resources)
			- ``cacert``: Location of the CA certificate used to sign the endpoint HTTPS certificate (if not installed in the local host).
			- ``cni``: CNI configuration. It must include specific address ranges if the original manifest file is not available. The ``file`` stanza is alternative to the address ranges (if supplied, it takes precedence).
			
				- ``cluster-cidr``: the network address used for cluster communication (inter-pod).
				- ``service-cluster-ip-range``: the network address used for services.
				- ``file``: Location of the Kube controller manager manifest file that can be parsed.

 			- ``namespaces``: list of namespace names to query (all namespace if not given).

"""

import socket
import subprocess
import json
import yaml
import os
import logging
import sys
import ipaddress
import uuid

from otupy.profiles import slpf
from otupy.profiles.ctxd.data.application import Application
from otupy.types.data.ipv4_addr import IPv4Addr
from otupy.types.data.uri  import URI

from kubernetes import config, client
from kubernetes.client.rest import ApiException

from urllib.parse import urlparse

from otupy.actuators.ctxd.ctxd_actuator import CTXDActuator

from otupy.profiles.ctxd import *

from otupy.types.data.hostname import Hostname
from otupy.types.data.l4_protocol import L4Protocol



from otupy import ArrayOf, Nsid, Version,Actions, Response, StatusCode, StatusCodeDescription, Features, ResponseType, Feature, actuator_implementation
import otupy.profiles.ctxd as ctxd

from otupy.profiles.ctxd.data.name import Name
from otupy.profiles.ctxd.data.service import Service, SId
from otupy.profiles.ctxd.data.link import Link
from otupy.profiles.ctxd.data.execution_environment_type import ExecutionEnvironmentType
from otupy.profiles.ctxd.data.host_type import HostType
from otupy.profiles.ctxd.data.host import  Host
from otupy.profiles.ctxd.data.network_node import NetworkNode
from otupy.profiles.ctxd.data.network_interface import NetworkInterface
from otupy.profiles.ctxd.data.ip_network import IPNetwork

logger = logging.getLogger(__name__)

# TODO: Add to configuration items (or read from file
K8S_APISERVER_NAME="kube-apiserver"
K8S_APISERVER_LABEL="apiserver"
K8S_CLUSTER_NETWORK="k8s-pod-network"
K8S_ADVERTISE_ADDRESS='advertise-address'

@actuator_implementation("ctxd-kubernetes")
class CTXDActuator_kubernetes(CTXDActuator):
	""" Kubernetes Actuator Manager

		Extend the base `CTDXActuator` to retrieve services and links for a Kubernetes cluster. Currently discovery is mostly limited to pods,
		nodes, and containers. It should be extended in future releases with additional resources (e.g., services and file systems).

	"""

	namespace = [] #it contains only the name of the namespaces
	""" Namespaces list"""
	api_client : any = None #api client for kubernetes
	config_file : any = None #configuration file path
	kube_context : any = None #kubernetes context

	def __init__(self, auth, **kwargs):
		""" Initialization

			The necessary parameters are:
			- auth: Authentication information to connect to a Kubernetes cluster
			Optional parameters:
			- namespaces: List of namespaces which resources are to be discovered. If not provided, resources from all namespaces will be discovered.
			- dns: DNS name for the kubernets cloud
		"""
		kwargs['auth']=auth
		super().__init__(**kwargs)

		self.namespaces = kwargs['config']['namespaces'] if 'config' in kwargs and 'namespaces' in kwargs['config'] else None
		self.dns = kwargs['config']['dns'] if 'config' in kwargs and 'dns' in kwargs['config'] else None
		self.cniconfig = kwargs['config']['cni'] if 'config' in kwargs and 'cni' in kwargs['config'] else None
		self.domain=self.dns

		self._k8s_nodes = {} # Keep a list of nodes and their sids
		self._k8s_apiserver = None # Keep the SId of the apiserver container (if any)

		self.nats = {} # Keep a map of NAT associated to each pod
		self.nodes = {} # Keep a map of nodes hosting each pod
		self.networks = {} # Keep a map of networks for each pod
		self.pods = {} # Keep a map of pods associated to each container
		self.net_on_node = {} # Keep a map of where networks are expected to be implemented

		self._setup_cni()
		self._connect_to_kubernetes()

	def discover_context(self):
		""" Discover services and links

			Implements the base class interface to update services and links
		"""
		self.discover_services()
		self.discover_links()


	def discover_services(self):
		""" Discovers K8S services

			Implements the required method to discover services. Currently it discover:
			- K8S base services (e.g., dns)
			- Pods in selected namespaces
			- Nodes
			- Namespaces available
		"""
		self._discover_k8s_cloud()
		self._discover_k8s_services()
		self._discover_k8s_pods()
		self._discover_k8s_namespaces()		
		self._discover_k8s_networks()
		self._discover_pod_networks()
#self._discover_k8s_nodes() We don't create services for nodes, since these should be created by another Actuator

	def discover_links(self):
		""" Automatically discover links between Kubernetes components

			The current implementation discovers links between:
			- Kubernetes and nodes
			- Kubernetes  and pods 
			- Pods and containers
			- Network Policy firewall and pods
		"""
		self._discover_k8s_links_nodes()
		self._discover_k8s_links_pods()
		self._discover_networkfunctions_links_nodes()
		self._discover_pod_links_nodes()
		self._discover_network_links_nodes()
		self._discover_pod_links_containers()
		self._discover_pod_links_networks()
		self._discover_k8s_links_np()
		self._discover_np_links_pods()


	def _discover_k8s_namespaces(self):
		namespaces = self._k8s_namespaces_list()

		# There might be a null configuration
		if namespaces is []:
			return

		# WARNING: Namespaces are not converted to services so far!!!


	def _k8s_namespaces_list(self):
		namespace_list = []
		try:
			namespace_list = self.api_client.list_namespace(field_selector="status.phase=Active").items
		except ApiException as e: # Fails without cluster-wide scope
			logger.warn("Unable to retrieve namespace list (do you have cluster-wide access?)")
			logger.debug("Reason: %s", e)
			return []

		if self.namespaces is None: # Retrieve all namespaces
			return namespace_list
		else:
			return [ ns for ns in namespace_list if ns.metadata.name in self.namespaces ]

		
	def _discover_k8s_cloud(self):
		# The root service: K8S cluster
		# --------------------------------------------------
		# K8S is considered PaaS when managed by an external service provider
		name=self.dns if self.dns is not None  else "kubernetes"
		k8s = Cloud(description='Kubernetes cloud', id=None, name=name, type='k8s')
		k8s_subservices = ArrayOf(SId)()
		# TODO: Fill in with k8s version/release

		# Discover kubelet names
		k8s_nodes = self._k8s_node_list()


		for n in k8s_nodes:
			# A Kubernetes node is an execution environment and hosts a kubelet
			node = ExecutionEnvironment(name=n.metadata.name, id=n.metadata.uid, version=n.status.node_info.kernel_version,
						description="Kubernetes node "+n.status.node_info.container_runtime_version,
                  type= ExecutionEnvironmentType( OS(family=n.status.node_info.operating_system, arch=n.status.node_info.architecture,
							version=n.status.node_info.kernel_version)) )

			self._k8s_nodes[n.metadata.name]=SId.create_from_service_type(node)

			kubelet = Application(name="kubelet", description="Kubernetes node agent",
					version=n.status.node_info.kubelet_version , owner=self.owner, app_type="kubelet")
			k8s_subservices.append(SId.create_from_service_type(kubelet, domain=n.metadata.name))

		# Discover kube-api server
		# This is a container discovered later on
#		label_selector= "component=kube-apiserver"
#		master_list = self._k8s_pod_list(label_selector=label_selector)
#		for master in master_list:
#			try:
#				for c in master.spec.containers:
#					if c.name == "kube-apiserver":
#						version = c.image.split(':')[1]
#						apiserver = Application(name="kube-apiserver", description="Kubernetes API server",
#								version=version, owner=self.owner, app_type='kubelet')
#						k8s_subservices.append(SId.create_from_service_type(apiserver, domain=self.domain,
#								namespace=master.metadata.namespace))
#			except Exception as e:
#				logger.warn("Unable to find kube-apiserver: %s", e)
#				apiserver = None

		self.services.append(Service(name=Name(k8s.name),
				sid=SId.create_from_service_type(k8s, domain=self.domain),
				type=ServiceType(k8s), 
				subservices=k8s_subservices, owner=self.owner, release=None))

	def _create_node_sid(self, node_name):
		""" Create a node sid for nodes discovered while scanning pods 

			This assumes the actuator has not access to node apis"""
		return SId(type=ServiceType.get_type_name(ExecutionEnvironment), 
						subtype=ExecutionEnvironmentType.get_type_name(OS), 
						name=node_name)


	def _discover_k8s_services(self):
		""" Discover Kubernetes services 

			Loops through provided namespaces and returns the services found.
			Services are NATs in front of Pods.

			Rules are currently saved as strings. TODO: create data structures for NAT rules.
		"""

		# Kubernetes services
		# ---------------------------------
		cloud_services = self._k8s_service_list(namespaces=self.namespaces)
		kube_api_found = False
		self.nats = {}

		def _update_endpoints(endpoints, ip, port):
			if len(endpoints) > 0:
				endpoints = endpoints + ", "
			return endpoints + str(ip) + "/" + str(port)

		k8s_services = ArrayOf(Name)()
		for service in cloud_services:
			rules = ArrayOf(str)()
			if service.spec.selector is not None:
				for k,v in service.spec.selector.items():
					# Assume only 1 field selector is present
					label_selector= k + "=" + v
				pods = self._k8s_pod_list(label_selector=label_selector)
			elif service.metadata.labels is not None:
				# Workaround: the internal NAT to translate between service CIDR and public API IP has not selector. It uses the label: "component=kube-apiserver"
				if "component" in service.metadata.labels and service.metadata.labels['component'] == K8S_APISERVER_LABEL:
					label_selector= "component="+K8S_APISERVER_NAME
					pods = self._k8s_pod_list(label_selector=label_selector)
			else:
				pods = None
			match service.spec.type:
				case "ClusterIP":
					for ip in ( [] if service.spec.cluster_i_ps is None else service.spec.cluster_i_ps ) +  ( [] if service.spec.external_i_ps is None else service.spec.external_i_ps):
						for port in service.spec.ports:
							endpoints=""
							if pods is not None:
								for p in pods:
									if p.metadata.namespace == service.metadata.namespace: 
										endpoints = _update_endpoints(endpoints, p.status.pod_ip,  str(port.target_port))
							else:
								try:
									eps = self._k8s_endpoint_list(namespace=service.metadata.namespace, service=service.metadata.name)
									for e in eps.items:
										for s in e.subsets:
											for a in s.addresses:
												for p in s.ports:
													endpoints = _update_endpoints(endpoints, a.ip, str(p.port))
								except:
									logger.warn("Unable to retrieve endpoint for %s", service.metadata.name)
							rules.append( "DNAT: " + ip + ":" + str(port.port) + " (" + port.name + ") -> " + endpoints )
				case "NodePort":
				  	# Retrieve node address
					for port in service.spec.ports:
						endpoints=""
						for p in pods:
							endpoints = _update_endpoints(endpoints, p.status.host_ip ,str(port.target_port))
						rules.append( "DNAT: " + ip + ":" + str(port.node_port) + " (" + port.name + ") -> " + endpoints )
				case _:
					# TODO: Retrieve the IP address of the nodes hosting the pod implementing the service
					logger.warn("Unmanaged service type %s for for %s",service.spec.type, service.metadata.name)

			try:
				# Try to detect the kubeapi service (if cluster-wide visibility is available)
				if ( service.metadata.labels['component'] == K8S_APISERVER_LABEL and
						service.metadata.labels['provider'] == "kubernetes" ):
					# This is the apiserver for Kubernetes; subservices are kubelets
					description="Kubernetes API Service"
					kube_api_found=True
			except:
					description="Kubernetes service " + service.spec.type
			
#			# Retrieve the list of pods served by this namespace
#			ep = self._k8s_endpoint_list(namespace=service.metadata.namespace, service=service.metadata.name)
#			subservices = ArrayOf(Name)()
#			for e in ep.items:
#				for s in e.subsets:
#					try:
#						for a in s.addresses:
#							subservices.append(self._k8s_dns_name(name=a.target_ref.name, 
#									namespace=service.metadata.namespace,
#									resource_type="pod"))
#					except:
#						pass

			nat = NetworkFunction(name=service.metadata.name,
					description=description,
					id=service.metadata.uid,
					type=NetworkFunctionType(NAT(rules=rules)))
			name=Name(Hostname(self._k8s_dns_name(nat.name, service.metadata.namespace, "svc")))
			nat_service = Service(name=name, type=ServiceType(nat), 
						sid=SId.create_from_service_type(nat, domain=self.domain, namespace=service.metadata.namespace),
						subservices=None, owner=self.owner, release=None)
			self.services.append(nat_service)
			if pods is not None:
				for p in pods:
#					pod_ref=str(Name(Hostname(self._k8s_dns_name(p.metadata.name, p.metadata.namespace, "pod"))))
					pod_sid=SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod), 
										domain=self.domain, namespace=p.metadata.namespace, 
										name=p.metadata.name)
					self.nats[str(pod_sid)]=nat_service


# Not sure this is a good idea, once I understood K8S svc are NATs
#		# Kube API service
#		# -----------------
#		# Add the API service if not found automatically
#		u = urlparse(self.auth['host'])
#		app = (API(description="Kubernetes API Service",
#					name=self._k8s_dns_name(name="kubernetes",namespace="default",resource_type="svc"),
#					endpoints=ArrayOf(Endpoint)([Endpoint(description=u.scheme, endpoint_type="kubeapi", 
#						  									transport="TCP", transfer=u.scheme, encoding=None,
#															uri=self.auth['host'])]),
#					id=None, provider=self.owner ))
#		logger.debug("Found k8s service: %s", str(app.name))
#		# TODO: Add software release (maybe with its SBOM)
#		# TODO: What are subservices of a Service? Pods? 
#		name=Name(Hostname(self._k8s_dns_name(app.name, service.metadata.namespace, "svc")))
#		self.services.append(Service(name=name, type=ServiceType(app), #links=ArrayOf(Link)(),
#					namespace=service.metadata.namespace,
#					subservices=subservices, owner=self.owner, release=None))
	

	def _discover_k8s_networks(self):
		""" Create cluster networks

			This function artifically creates two cluster networks based on configuration
			information, since there is no API to query the cni.
		"""

		# Cluster network
		ipnets = ArrayOf(IPNetAddress)([IPNetAddress(self.cni_cluster_cidr), IPNetAddress(self.cni_service_cidr)])
		eth = IPNetwork({'nets': ipnets})
		net = Network(name=K8S_CLUSTER_NETWORK, description="Kubernetes cluster network",
				id=None, type=NetworkType(eth))
	
		subservices=ArrayOf(SId)()

		self.services.append(Service(name=Name(Hostname(self._k8s_dns_name(name=net.name, 
								namespace=None, resource_type="network"))) , 
								sid=SId.create_from_service_type(net, domain=self.domain),
								domain=self.domain,
								type=ServiceType(net),
								subservices=subservices, owner=self.owner, release=None))

		net_service = Service(name=Name(Hostname(self._k8s_dns_name(name=net.name, 
								namespace=None, resource_type="network"))) , 
								sid=SId.create_from_service_type(net, domain=self.domain),
								type=ServiceType(net),
								subservices=subservices, owner=self.owner, release=None)

	def _add_k8s_node(self, net_service, node_sid):
		if str(node_sid) in self.net_on_node:
			if str(net_service.sid) not in self.net_on_node[str(node_sid)]:
				self.net_on_node[str(node_sid)].append(str(net_service.sid))
		else:
			self.net_on_node[str(node_sid)]=[str(net_service.sid)]


	def _discover_pod_networks(self):
		# Look for additional multus networks.
		# I could not understand whether there is function in the kubernetes library
		# to use the k8s.cni.cncf.io/v1 for retrieving network-attachment-definition 
		# In any case, since in such configuration there is no network address, I parse
		# pod annotations

		self.networks = {}
		self.net_on_node = {}
		k8s_node_dict = self._k8s_node_dict()

		for pod in self._k8s_pod_list():
			podname=str(Name(Hostname(self._k8s_dns_name(name=pod.metadata.name, namespace=pod.metadata.namespace,
								resource_type="pod"))))
			self.networks[podname] = []
			if pod.metadata.annotations is not None and 'k8s.v1.cni.cncf.io/networks' in pod.metadata.annotations:
				for net in json.loads(pod.metadata.annotations['k8s.v1.cni.cncf.io/networks']):
					if 'name' in net and net['name'] != K8S_CLUSTER_NETWORK:
						netname=Name(Hostname(self._k8s_dns_name(name=net['name'], 
																			namespace=pod.metadata.namespace, 
																			resource_type="network")))
						net_services = self.get_services(name=netname, namespace=pod.metadata.namespace, filter=Network)
						my_net_services = self.get_services(filter=Network)
#						net_services_by_sid = self.get_services_by_sid(SId(name=net['name'], namespace=pod.metadata.namespace, type=ServiceType.get_type_name(Network)))
						assert len(net_services) < 2 # This is necessary because in the else branch I assume a single element to be returned
						if len(net_services) == 0: # Not registered yet
							ipnets = ArrayOf(IPNetAddress)()
							if 'ips' in net:
								for ip in net['ips']:
									ipnets.append(ipaddress.ip_network(ip, strict=False))	
							# TODO: This is an over-simplifcation, since multiple types of network can be present
							eth = IPNetwork({'nets': ipnets})
							net = Network(name=net['name'], description="Kubernetes network " + str(netname),
								id=None, type=NetworkType(eth))
	
							net_service = Service(
										name=netname,
										sid=SId.create_from_service_type(net, domain=self.domain, namespace=pod.metadata.namespace),
										namespace=pod.metadata.namespace,
										type=ServiceType(net),
										subservices=ArrayOf(SId)(), owner=self.owner, release=None)
							self.services.append(net_service)
						else:
							for mynet in net_services:
								net_service = mynet
								# Check if ips are already registered
								# TODO: Manage additional network types
								for ip1 in mynet.type.getObj().type.getObj()['nets']:
									if 'ips' in net:
										for ip2 in net['ips']:
											found = False
											if str(ip1) == str(ipaddress.ip_network(ip2,strict=False)):
												found=True
											if not found:
												 mynet.type.getObj().type.getObj()['nets'].append(ip2)
						self.networks[podname].append(net_service)
						self._add_k8s_node(net_service, self._k8s_nodes[pod.spec.node_name]) 
									
			if pod.metadata.labels is not None and 'component' in pod.metadata.labels and  pod.metadata.labels['component']==K8S_APISERVER_NAME:
				# Workaround: kubeapiserver listen on public network
				if pod.status.host_ip is not None:
					ipnets=ArrayOf(IPNetAddress)()
#					for ip in pod.status.host_i_ps:
#						ipnets.append(ipaddress.ip_network(ip, strict=False))
					ipnets.append(ipaddress.ip_network(pod.status.host_ip, strict=False))
					eth = IPNetwork({'nets': ipnets})
					netname=self._k8s_dns_name(name=K8S_ADVERTISE_ADDRESS, namespace=None, resource_type="network")
					net = Network(name=K8S_ADVERTISE_ADDRESS,
							description="Kube API server public address",
							id=None, type=NetworkType(eth))
					net_service = Service(
								name=Name(Hostname(netname)),
								sid=SId.create_from_service_type(net, domain=self.domain),
								type=ServiceType(net),
								subservices=ArrayOf(SId)(), owner=self.owner, release=None)
					self.services.append(net_service)
					self.networks[podname].append(net_service)
					self._add_k8s_node(net_service, self._k8s_nodes[pod.spec.node_name])

#			net_services = self.get_services(name=Name(K8S_CLUSTER_NETWORK), filter=Network)
			net_services = self.get_services_by_sid(SId(name=K8S_CLUSTER_NETWORK, domain=self.domain, type=ServiceType.get_type_name(Network)))
			if len(net_services) > 0:
				self._add_k8s_node(net_services[0], self._k8s_nodes[pod.spec.node_name])


	def _k8s_service_list(self, namespaces=None):
		""" Discover k8s services in the given namespace

			:param namespace: the namespace name
		"""
		try:
			if namespaces is None:
				return self.api_client.list_service_for_all_namespaces().items 
		except ApiException as e:
			logger.warn("Unable to retrieve service list for all namespaces")
			logger.debug("Reason: %s",e)
			return []

		try:
			service_list=[]
			for n in namespaces:
				# This works even without cluster scope
				service_list+=self.api_client.list_namespaced_service(namespace=n).items
		except ApiException as e:
			logger.warn("Unable to retrieve service list for namespace %s: %s",n, e)
		
		return service_list	

	def _k8s_endpoint_list(self, namespace, service=None):
		if service is not None:
			fs = "metadata.name=="+service
		else:
			fs = None

		try:
			return self.api_client.list_namespaced_endpoints(namespace=namespace, field_selector=fs)
		except:
			logger.warn("Unable to retrieve endpoints for namespace: %s", namespace)
			return []

	
	def _k8s_pod_node(self, pod):
		pods = self.api_client.list_namespaced_pod(namespace=pod.namespace, field_selector="metadata.name="+str(pod.name))
		for p in pods.items:
			# Just one item will be present because the pod name is unique in the same namespace
			return p.spec.node_name
			

	def _discover_pod_links_nodes(self):
		""" Add links between pods and nodes where they are hosted 
		"""
		k8s_pods = self.get_services_by_sid(SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod)))

		# If nodes were discovered, I can safely assume to have cluster-wide visibility
		for p in k8s_pods:
			node=self.nodes[str(p.sid)]
			consumer=self.get_consumer(sid=node)

			# We assume the node name is the same as the service created by its actuator
			# We know kubernetes nodes are implemented by plain operating systems (does not matter where they are hosted)
			peer = Peer(service_name=Name(Hostname(node.name)), sid=node,
						role=PeerRole.host, # Node hosts pods
						consumer=consumer)
			description="Pod "+str(p.name)+" hosted on "+str(peer.service_name)
			self.links.append(Link(name=p.name, sid=p.sid, description=description, role=PeerRole.guest,
						link_type=LinkType.hosting, peers=ArrayOf(Peer)([peer])))

	def _discover_network_links_nodes(self):
		""" Add links between virtual networks and the nodes that implement them
		"""

		for node, nets in self.net_on_node.items():
			sid=SId.from_str(node)
			consumer=self.get_consumer(sid=sid)
			peer = Peer(service_name=Name(Hostname(sid.name)), 
					sid=sid,
					role=PeerRole.host, # Node hodes network implementation
					consumer=consumer)
			for net in nets:
				# TODO: Check if some networks are present only on specific nodes (where the pod is running
				description="Network "+str(net)+" implemented in " + str(peer.sid)
				net_sid = SId.from_str(net)
				self.links.append(Link(name=Name(Hostname(net_sid.name)), sid=net_sid, description=description,
							role=PeerRole.guest, link_type=LinkType.hosting, peers=ArrayOf(Peer)([peer])))


	def _discover_pod_links_containers(self):
		""" Add links between pods and containers they contain
		"""
		k8s_pods = self.get_services_by_sid(SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod)))

		# This link might be removed, if we keep containers as subservices of pods
		for p in k8s_pods:
			for c in p.subservices:
				peer = Peer(service_name=p.name, sid=p.sid, role=PeerRole.contained, consumer=None)
				description="Container "+str(c)+" in pod "+str(p.sid)
				self.links.append(Link(name=None, sid=c, description=description, role=PeerRole.guest,
							link_type=LinkType.containing, peers=ArrayOf(Peer)([peer])))
				
	def _discover_pod_links_networks(self):
		""" Add links between pods and the cluster network

			If a service for the pod is present, we also add a NAT function in between.
			TODO: may we also add a Firewall function in between?

			(Container) --- (NAT) --- (k8s-pod-network )
		"""
		k8s_pods = self.get_services_by_sid(SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod)))
		nets = self.get_services_by_sid(SId(name=K8S_CLUSTER_NETWORK, domain=self.domain, type=ServiceType.get_type_name(Network)))

		for p in k8s_pods:
			for net in nets:
				pod_sid = str(p.sid) 
				if pod_sid in self.nats:
					nat = self.nats[pod_sid]
					# Add NAT between pod and cluster network
					peer = Peer(service_name=nat.name, sid=nat.sid, role=PeerRole.forwarding, consumer=None)
					description="Pod "+str(p.sid)+" attached to NAT " + str(nat.sid)
					self.links.append(Link(name=p.name, sid=p.sid, description=description, role=PeerRole.endpoint,
								link_type=LinkType.packet_flow, peers=ArrayOf(Peer)([peer])))
					peer = Peer(service_name=net.name, sid=net.sid, role=PeerRole.forwarding, consumer=None)
					description="NAT "+str(nat)+" attached to cluster network"
					self.links.append(Link(name=nat, sid=nat.sid, description=description, role=PeerRole.forwarding,
								link_type=LinkType.packet_flow, peers=ArrayOf(Peer)([peer])))
				else: # No NAT: pod attached directly to cluster network
					peer = Peer(service_name=net.name, sid=net.sid, role=PeerRole.forwarding, consumer=None)
					description="Pod "+str(p.name)+" attached to cluster network"
					self.links.append(Link(name=p.name, sid=p.sid, description=description, role=PeerRole.endpoint,
								link_type=LinkType.packet_flow, peers=ArrayOf(Peer)([peer])))
			# Other networks:
			pod_name=str(p.name)
			if pod_name in self.networks:
				for net_service in self.networks[str(pod_name)]:
					peer = Peer(service_name=net_service.name, sid=net_service.sid, role=PeerRole.forwarding, consumer=None)
					description="Pod "+str(p.name)+" attached to network " + str(net_service.name)
					self.links.append(Link(name=p.name, sid=p.sid, description=description, role=PeerRole.endpoint,
								link_type=LinkType.packet_flow, peers=ArrayOf(Peer)([peer])))



	def _discover_k8s_links_nodes(self):
		""" Discover K8S links to nodes

			Kube-api is hosted on master (but it is a pod, so we don't need to deal with it now)
			Kubelets are hosted on nodes
			
			For now, a simplification is used and only Kubernetes hosted on master is reported
		"""
		cloud_services=self.get_services(filter=Cloud)

		for cloud in cloud_services:
			for sub in cloud.subservices:
				if sub.type == ServiceType.get_type_name(Application):
					node = self._k8s_nodes[sub.domain]
					peer = Peer(service_name=node.name, sid=node, role=PeerRole.host,
										consumer=self.get_consumer(sid=node))
					self.links.append( Link(name=sub.name, sid=sub, description="Kubelet "+sub.name+" hosted on "+str(node),
							role=PeerRole.guest, link_type=LinkType.hosting, peers=ArrayOf(Peer)([peer])) )

			
	def _discover_networkfunctions_links_nodes(self):
		""" Assign network functions to nodes 
		"""
		nfs = self.get_services(filter=NetworkFunction)
#		nfs_by_id = self.get_services_by_sid(SId(type=ServiceType.get_type_name(NetworkFunction)))

		peersid=None
		for nf in nfs:
			if ( nf.sid.subtype == NetworkFunctionType.get_type_name(NAT) ):
				for pod_sid, nat in self.nats.items():
					if nat.sid == nf.sid:
						try:
							peersid=self.nodes[str(pod_sid)]
							description="NAT " + str(nf.sid) + " hosted on "+ str(peersid)
						except Exception as e:
							peersid=None
			else:
				peersid=None

			if peersid is not None:
				consumer = self.get_consumer(sid=peersid)
				peer=Peer(service_name=Name(peersid.name), sid=peersid,
						role=PeerRole.host,
						consumer=consumer)
				self.links.append(Link(name=nf.name, sid=nf.sid, description=description, role=PeerRole.guest,
							link_type=LinkType.hosting, peers=ArrayOf(Peer)([peer])))




	def _discover_k8s_links_pods(self):
		""" Discover K8S links to pods

			kude-apiserver controls kubelets, 
			and kubelets controls pods. 
		"""
		cloud_services=self.get_services(filter=Cloud)
		cloud_pods=self.get_services_by_sid(SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod)))

		for cloud in cloud_services:
			for sub in cloud.subservices:
				# Kube-apiserver controls kubelets
				if sub.type == ServiceType.get_type_name(Application):
					peer = Peer(service_name=sub.name, sid=sub, role=PeerRole.controlled,
										consumer=self.get_consumer(sid=sub))
					if self._k8s_apiserver is not None:
						self.links.append( Link(name=K8S_APISERVER_NAME, sid=self._k8s_apiserver, 
											description="Kube-apiserver controls kubelets",
											link_type=LinkType.controlling, role=PeerRole.control, 
											peers=ArrayOf(Peer)([peer])))
					peer.role=PeerRole.control
					for pod in cloud_pods:
						if self.nodes[str(pod.sid)].name == sub.domain:
							self.links.append( Link(name=pod.name, sid=pod.sid, 
															description="Kubelet controls pod "+str(pod.name),
															link_type=LinkType.controlling, role=PeerRole.controlled,
															peers=ArrayOf(Peer)([peer])))



		




#
#		k8s_service = self.get_services(name=Name(Hostname(self._k8s_dns_name(name='kubernetes',
#																								namespace="default",
#																								resource_type="svc"))),
#													filter=API)
##		k8s_service_by_id = self.get_services_by_sid(SId(name='kubernetes', namespace='default', type=ServiceType.get_type_name(API)))
#
#		k8s_containers = self.get_services_by_sid(SId(type=ServiceType.get_type_name(ExecutionEnvironment), subtype=ExecutionEnvironmentType.get_type_name(Container)))
##		k8s_containers_by_id = self.get_services_by_sid(SId(type=ServiceType.get_type_name(Container)))
#
#		for s in k8s_service:
#			for c in k8s_containers:
#				peer = Peer(service_name=c.name, sid=c.sid,
#							role=PeerRole.controlled, # Kubernetes controls its containers
#							consumer=None)
#				description="Kubernetes controls container "+str(c.name.getObj())
#				self.links.append(Link(name=s.name, sid=s.sid, description=description,
#							link_type=LinkType.controlling, peers=ArrayOf(Peer)([peer])))
#


	def _discover_k8s_nodes(self):
		""" Discover kubernetes working nodes

			Nodes are the physical servers/vms that host pods. It is questionable if such service 
			should be reported, since the ExecutionEnvironment subsystem should have its own actuator describing 
			the full stack of services/software hosted.
		"""
		nodes = self._k8s_node_list()

		for n in nodes:
			# A Kubernetes node is an execution environment and hosts a kubelet
			node = ExecutionEnvironment(name=n.metadata.name, id=n.metadata.uid,
						description="Kubernetes node"+n.status.node_info.container_runtime_version,
                  type= ExecutionEnvironmentType( OS(family=n.status.node_info.operating_system, name=n.status.node_info.os_image)))
			logger.debug("Found node: %s", str(node))

			self.services.append(Service(name=Name(Hostname(self._k8s_dns_name(name=n.metadata.name, resource_type="node"))), 
						sid=SId.create_from_service_type(node, domain=self.domain),
						type=ServiceType(node), 
						subservices=None, owner=self.owner, release=n.metadata.resource_version))

			kubelet = Application(name="kubelet", description="Kubernetes worker node",
					version=n.status.node_info.kubelet_version , owner=self.owner, app_type="kubelet")

			self.services.append(Service(name=Name(Hostname(kubelet.name+"@"+str(node.hostname))),
						sid=SId.create_from_service_type(kubelet, domain=self.domain),
						type=ServiceType(kubelet), 
						subservices=None, owner=self.owner ))

	def _k8s_node_list(self, field_selector=None, label_selector=None):
		try:
			return self.api_client.list_node(field_selector=field_selector, label_selector=label_selector).items
		except ApiException as e:
			logger.warn("Unable to retrieve node list (do you have cluster-wide access?)")
			logger.debug("Reaseon: %s", e)
			return []

	def _k8s_node_dict(self,  field_selector=None, label_selector=None):
		""" Discover k8s nodes and create a dictionary indexed by node name """
		k8s_nodes_list = self._k8s_node_list(field_selector=field_selector, label_selector=label_selector)
		k8s_nodes_dict = {}

		for n in k8s_nodes_list:
			k8s_nodes_dict[n.metadata.name]=n

		return k8s_nodes_dict
	
	def _discover_k8s_links_np(self):
		""" Add link between Kubernetes and Network Policies

			Network Policies implement sort of a slpf firewall, hence they are a security function. However, they are not
			standalone software, and they are implemented by kubernetes..
		"""
		if self._k8s_apiserver is not None:
			consumer = self.get_consumer(sid=self._get_networkpolicies_sid())
			peer = Peer(service_name=Name("kubernetes-networkpolicies"), 
							sid=self._get_networkpolicies_sid(),
							role=PeerRole.controlled, # Kubernetes controls its Network Policies (indeed, they are controlled by a CNI)
							consumer=consumer)
			description="Kubernetes API controls Network Policies"
			self.links.append(Link(name=Name(K8S_APISERVER_NAME), sid=self._k8s_apiserver, description=description,
							link_type=LinkType.controlling, peers=ArrayOf(Peer)([peer])))
	
 
	
	def _discover_k8s_pods(self):
		""" Discovers pods

			Discovers pods and describes them including network interfaces and hosted containers.
		"""
		self.nodes = {}
		controller = None

		k8s_nodes_dict = self._k8s_node_dict()
			
		pods = self._k8s_pod_list()
		k8s_subservices = ArrayOf(SId)()

		# Get the Containers (init and main) used by this pod
		def _get_container_status(c):
			state='unknown'
			if c.state.running is not None:
				state='running'
			if c.state.waiting is not None:
				state='waiting'
			if c.state.terminated is not None:
				state='terminated'
			return state

		def _get_container_list(containers, description=None):
			container_list = []
			try:
				for c in containers:
					state=_get_container_status(c)
					# TODO: parse command and create Application service with link to container 
					container_list.append( ExecutionEnvironment(name=c.name+"."+pod.metadata.name, id=c.container_id, 
							description=description, type=ExecutionEnvironmentType(Container(namespace=pod.metadata.namespace, image=c.image, status=state))))
			except:	# no containers
				pass
			return container_list

		for pod in pods:
			pod_subservices_list = ArrayOf(SId)()
			# pod.status.pod_ip and pod.status.pod_i_ps return wrong values (host ip) and
			# do not include multus networks
			if pod.metadata.annotations is not None and 'k8s.v1.cni.cncf.io/network-status' in pod.metadata.annotations:
				ports = json.loads(pod.metadata.annotations['k8s.v1.cni.cncf.io/network-status'])
				port_list = ArrayOf(NetworkInterface)()
				for p in ports:
					name = p['name'] if 'name' in p else None
					iface = p['iface'] if 'iface' in p else None
					ips = []
					if 'ips' in p:
						for ip in p['ips']:
							ips.append(IPInfo(ip=IPAddress(ip)))
					port_list.append( Port(id=name, description="Pod network interfaces",  iface=iface, ips=ips) )
			
			
			node_type = NetworkNode(description="Pod network ports", id=pod.metadata.uid,
					name=pod.metadata.name, ifaces=port_list)
			nodename=Name(self._k8s_dns_name(pod.metadata.name, pod.metadata.namespace, "ports"))
			nodesid=SId.create_from_service_type(node_type, domain=self.domain, namespace=pod.metadata.namespace)
			self.services.append(Service(name=nodename, 
						sid=nodesid,
						type=ServiceType(node_type), 
					subservices=ArrayOf(SId)(), release=None, owner=None))
			pod_subservices_list.append(nodesid)
			

			pod_type = Host(description="Kubernetes pod", id=pod.metadata.uid,
				  name=pod.metadata.name, type=HostType(Pod(namespace=pod.metadata.namespace)))

			# Since there is not a clear understanding of the different types of controllers,
			# we create the objects here and update them if we found more pods
			# Update: same reasoning as for the pod. A StatefulSet/Deployment controls the pod number and replication, but it is not
			# really a service. It is a Kubernetes abstraction to configure lifecycle management. So we don't expose them anymore as
			# services.
			if pod.metadata.owner_references is not None:
				for owner in pod.metadata.owner_references:
					if owner.controller:
						controllername = Name(Hostname(self._k8s_dns_name(owner.name, pod.metadata.namespace, owner.kind)))
#controller_list = self.get_services(name=controllername, namespace=pod.metadata.namespace, filter=Application)
#assert len(controller_list) < 2 # Hopefully, only 1 result will be returned
						# The owner maybe a StatefulSet/DaemonSet/... resource belonging to a namespace or even a kubernetes node,
						# which does not belong to any namespace. The only reliable approach is to look for the name in any
						# namespace, and then select the correct one by its id
						controller_list = self.get_services(name=controllername, filter=Application)
						controller = None
						for ctr in controller_list:
							if ctr.type.getObj().id == owner.uid:
								controller = ctr
#						controller_list_by_id = self.get_services_by_sid(SId(name=owner.name, namespace=pod.metadata.namespace, type=ServiceType.get_type_name(Application)))
						if controller is None: # Create a new Application for this controller
							app = Application(name=owner.name, description=owner.kind,
									id=owner.uid, app_type=owner.kind)
							# This is a trick, but for now it is enough
							if owner.kind == 'Node':
								owner_namespace = None
							else:
								owner_namespace = pod.metadata.namespace
							controller = Service(name=controllername, 
									sid=SId.create_from_service_type(app, namespace=owner_namespace, domain=self.domain),
									namespace=owner_namespace,
									type=ServiceType(app), 
									subservices=ArrayOf(SId)(), release=owner.api_version)
							self.services.append(controller)
					if pod.metadata.namespace == "kube-system" and controller.sid not in k8s_subservices:
						k8s_subservices.append(controller.sid)


			container_list = _get_container_list(pod.status.container_statuses, "Kubernetes container") + _get_container_list(pod.status.init_container_statuses, "Kubernetes init container")

			# Create services for each container
			# A container is an execution environment, which we model as hosted in a Pod. Indeed,
		   # in Kubernetes a pod is a control unit for one or more container, but the concrete sandbox is partially defined
			# by the pod and partially by the container. However, in containerd the pod is a network namespace, and a network
			# namespaces host pids. hence it is part of the container virtual environment. Because of this, we consider the 
			# container as the main object (ExecutionEnvironment), and the network namespace as the hosting hardware (Host).
			# Containers in the same pod shares the same network namespace, hence they will be hosted in the same Host but
			# remains separate execution environments.
			podname=Name(self._k8s_dns_name(pod.metadata.name, pod.metadata.namespace, "pod"))
			for c in container_list:
				container_name=Name(Hostname(self._k8s_dns_name(c.name, pod.metadata.namespace,pod.metadata.name+".container")))
				self.pods[str(container_name)]=podname
				container_sid=SId.create_from_service_type(c, domain=self.domain, namespace=pod.metadata.namespace)
				self.services.append(Service(name=container_name,
						sid=container_sid,
						type=ServiceType(c), 
                  subservices=ArrayOf(SId)(), owner=self.owner, release=None))
				pod_subservices_list.append(container_sid)

				# Look for Kubernetes API server
				if c.name.startswith(K8S_APISERVER_NAME):
					cloud_list = self.get_services(filter=Cloud)
					self._k8s_apiserver = container_sid
					for cloud in cloud_list:
						cloud.subservices.append(container_sid)


				# Add subservice to existing instance
				# --- Currently replaced by pod (see below)
#				if controller is not None:
#					controller.subservices.append(container_sid)


			# NO -> Create the pod service with containers as subservices 
			# The pod has a double role in Kubernets. It is the network namespace for its containers and
			# it controls the lifecycle of its containers. I don't see the need to identify a link for the second role,
			# since the concrete software that controls the container is something else (kubelet? containerd?)
			# In our abstraction, the Pod is the Host of an Execution Environment, and Execution Environments are subservices
			# of them.
#			podname=Name(Hostname(self._k8s_dns_name(pod.metadata.name, pod.metadata.namespace, "pod"))) 
			pod_sid=SId.create_from_service_type(pod_type, domain=self.domain, namespace=pod.metadata.namespace)
			self.services.append(Service(name= podname, sid=pod_sid,
						type=ServiceType(pod_type), #links= ArrayOf(Name)([]),
						subservices=pod_subservices_list,
						owner=self.owner, release=pod.metadata.resource_version))

			# Add pod as subservice of a controller (StatefulSet, Deployment, ...)
			if controller is not None:
				controller.subservices.append(pod_sid)

			if pod.spec.node_name != "" and pod.spec.node_name is not None:
				if pod.spec.node_name not in self._k8s_nodes:
					# This happens if we do not have access to nodes apis
					# We generate a simpler sid for the node (and we'll use it again in the following)
					self._k8s_nodes[pod.spec.node_name] = self._create_node_sid(pod.spec.node_name)
				self.nodes[str(pod_sid)] = self._k8s_nodes[pod.spec.node_name]
			else:
				self.nodes[str(pod_sid)]=None
						

	def _k8s_pod_list(self, label_selector=""):
		""" Retrieve the list of pods from the K8S API. """
		array_container = ArrayOf(Service)()

		try:
			if self.namespaces is None:
				return self.api_client.list_pod_for_all_namespaces(label_selector=label_selector).items
		except ApiException as e:
			logger.warn("Unable to retrieve pod list for all namespaces")
			logger.debug("Reason: %s", e)
			return []
		
		try:
			pods = []
			for ns in self.namespaces:
				pods += self.api_client.list_namespaced_pod(namespace=ns, label_selector=label_selector).items 
		except ApiException as e:
			logger.warn("Unable to retrieve pod list for namespace %s", ns)
			logger.warn("Reason: %s", ns , e)

		return pods


	def _discover_np_links_pods(self):
		""" Add links from Network Policies to pods

			Automatically add a link from Network Policies to pods
			Network policies are modelled as a security function implemented by an external actuator.
			They protect all pods hosted in Kubernetes..
		"""
		k8s_service = self.get_services_by_sid(SId(type=ServiceType.get_type_name(Host), subtype=HostType.get_type_name(Pod)))

		for s in k8s_service:
			consumer = self.get_consumer(sid=self._get_networkpolicies_sid())
			peer = Peer(service_name=Name("kubernetes-networkpolicies"), 
							sid=self._get_networkpolicies_sid(),
							role=PeerRole.protect, # Kubernetes controls its Network Policies (indeed, they are controlled by a CNI)
							consumer=consumer)
			description="Kubernetes Network Policies protect "+str(s.name)
			self.links.append(Link(name=s.name, sid=s.sid, description=description, role=PeerRole.protected,
							link_type=LinkType.protecting, peers=ArrayOf(Peer)([peer])))


	def _connect_to_kubernetes(self):
		""" Connects to Kubernetes

			Use the available configuration to carry out authentication to a Kubernetes cluster.
		"""
			
		# The config module loads configuration from .kube (or provided file) into the _default member of the client.Configuration
		# class (which is a class member!). After invoking one of the config method to load these values, the client module
		# has access to such configuration through the Configuration. Note that certificates provided as values in the 
		# configuration file are then stored as files by the Configuration class.

		try:
			configuration = client.Configuration()
			# Configure API key authorization: BearerToken
			configuration.api_key['authorization'] = self.auth['api_key']
			# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
			configuration.api_key_prefix['authorization'] = 'Bearer'
			# Defining host is optional and default to http://localhost
			configuration.host = self.auth['host']
			# Set the cluster CA	certificate or disable certificate validation
			if self.config is not None and 'cacert' in self.config:
				configuration.ssl_ca_cert=self.config['cacert']
			else:
				configuration.verify_ssl=False
	
			# Create an API client
			self.api_client = client.CoreV1Api(client.ApiClient(configuration))
		except Exception as e:
			logger.error("Failed to connect to kubernetes: ", e)
			return Exception("Failed to connect to kubernetes")
		
	def _get_networkpolicies_sid(self):
		return SId(name='kubernetes-networkpolicies', domain=self.domain, type=ServiceType.get_type_name(NetworkFunction), subtype=NetworkFunctionType.get_type_name(Firewall))
		

	def _k8s_dns_name(self, name: str, namespace: str = None, resource_type: str = None):
		""" Create dns name to identify kubernetes resources

			The name will be the same as the dns name
		"""
		dnsname=name
		if( namespace is not None ):
			dnsname=name+"."+namespace
		if resource_type is not None:
			dnsname+="."+resource_type
		if self.dns is not None:
			dnsname+="."+self.dns

		return dnsname

	def _setup_cni(self):

		if self.cniconfig is None:
			self.cni_cluster_cidr = "0.0.0.0/0"
			self.cni_service_cidr = "0.0.0.0/0"
			return

		self.cni_cluster_cidr = None
		self.cni_service_cidr = None
		try:
			if 'file' in self.cniconfig:
				with open(self.cniconfig['file']) as f:
					config = yaml.safe_load(f)
					for cmds in  config['spec']['containers']:
						if 'command' in cmds:
							for cmd in cmds['command']:
								pair=cmd.replace("--",'',1)
								l=pair.split('=',1)
								if l[0] == "cluster-cidr":
									self.cni_cluster_cidr = l[1]
								if l[0] == "service-cluster-ip-range":
									self.cni_service_cidr = l[1]
		except Exception as e:
			logger.error("Unable to parse cni config file: %s", e)

		try:
			if self.cni_cluster_cidr == None or self.cni_service_cidr == None:
				self.cni_cluster_cidr = self.cniconfig['cluster-cidr']
				self.cni_service_cidr = self.cniconfig['service-cluster-ip-range']
		except Exception as e:
			logger.error("Unable to get cluster IP addresses from configuration: %s", e)

	
