From 542fab1607f4fe2530a8dda42dbdd074a5aa9be0 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 15 Dec 2025 21:11:02 -0500 Subject: [PATCH] Add ingress status update functionality to support various deployment modes - Introduced `ingressStatus` configuration in Helm values to enable status updates and customize deployment modes (auto, daemonset, nodeport, clusterip). - Implemented logic in the processor to detect deployment mode, retrieve ingress addresses, and update ingress status dynamically. - Updated Kubernetes manifests to grant required permissions for `ingresses/status` operations and pass necessary environment variables for configuration. - Adjusted Helm templates and associated Kubernetes examples to reflect the new ingress status update capability. --- deploy/kubernetes/easyhaproxy-clusterip.yml | 27 ++- deploy/kubernetes/easyhaproxy-daemonset.yml | 27 ++- deploy/kubernetes/easyhaproxy-nodeport.yml | 27 ++- helm/easyhaproxy/templates/clusterrole.yaml | 17 +- helm/easyhaproxy/templates/deployment.yaml | 16 +- helm/easyhaproxy/values.yaml | 11 + src/functions/__init__.py | 6 + src/processor/__init__.py | 218 ++++++++++++++++++++ 8 files changed, 312 insertions(+), 37 deletions(-) diff --git a/deploy/kubernetes/easyhaproxy-clusterip.yml b/deploy/kubernetes/easyhaproxy-clusterip.yml index bf1da88..df23850 100644 --- a/deploy/kubernetes/easyhaproxy-clusterip.yml +++ b/deploy/kubernetes/easyhaproxy-clusterip.yml @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -44,19 +44,18 @@ rules: - "networking.k8s.io" resources: - ingresses - # - ingresses/status + - ingresses/status - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -195,6 +194,16 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" --- # Source: easyhaproxy/templates/ingressclass.yaml apiVersion: networking.k8s.io/v1 diff --git a/deploy/kubernetes/easyhaproxy-daemonset.yml b/deploy/kubernetes/easyhaproxy-daemonset.yml index 190f865..f1616e4 100644 --- a/deploy/kubernetes/easyhaproxy-daemonset.yml +++ b/deploy/kubernetes/easyhaproxy-daemonset.yml @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -44,19 +44,18 @@ rules: - "networking.k8s.io" resources: - ingresses - # - ingresses/status + - ingresses/status - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -170,6 +169,16 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" --- # Source: easyhaproxy/templates/ingressclass.yaml apiVersion: networking.k8s.io/v1 diff --git a/deploy/kubernetes/easyhaproxy-nodeport.yml b/deploy/kubernetes/easyhaproxy-nodeport.yml index 8cb1fb6..06c3430 100644 --- a/deploy/kubernetes/easyhaproxy-nodeport.yml +++ b/deploy/kubernetes/easyhaproxy-nodeport.yml @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -44,19 +44,18 @@ rules: - "networking.k8s.io" resources: - ingresses - # - ingresses/status + - ingresses/status - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -195,6 +194,16 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" --- # Source: easyhaproxy/templates/ingressclass.yaml apiVersion: networking.k8s.io/v1 diff --git a/helm/easyhaproxy/templates/clusterrole.yaml b/helm/easyhaproxy/templates/clusterrole.yaml index ca784ab..6968b91 100644 --- a/helm/easyhaproxy/templates/clusterrole.yaml +++ b/helm/easyhaproxy/templates/clusterrole.yaml @@ -17,7 +17,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -31,19 +31,18 @@ rules: - "networking.k8s.io" resources: - ingresses - # - ingresses/status + - ingresses/status - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: diff --git a/helm/easyhaproxy/templates/deployment.yaml b/helm/easyhaproxy/templates/deployment.yaml index 75e14e5..bdbfadf 100644 --- a/helm/easyhaproxy/templates/deployment.yaml +++ b/helm/easyhaproxy/templates/deployment.yaml @@ -77,4 +77,18 @@ spec: {{- if .Values.easyhaproxy.certbot.email }} - name: EASYHAPROXY_CERTBOT_EMAIL value: {{ .Values.easyhaproxy.certbot.email }} - {{ end }} + {{- end }} + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: {{ .Values.ingressStatus.enabled | quote }} + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: {{ .Values.ingressStatus.deploymentMode | quote }} + {{- if .Values.ingressStatus.externalHostname }} + - name: EASYHAPROXY_EXTERNAL_HOSTNAME + value: {{ .Values.ingressStatus.externalHostname | quote }} + {{- end }} + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: {{ .Values.ingressStatus.updateInterval | quote }} diff --git a/helm/easyhaproxy/values.yaml b/helm/easyhaproxy/values.yaml index cf85445..bf4906b 100644 --- a/helm/easyhaproxy/values.yaml +++ b/helm/easyhaproxy/values.yaml @@ -38,6 +38,17 @@ ingressClass: # Additional annotations for the IngressClass annotations: {} +# Ingress status update configuration +ingressStatus: + # Enable updating ingress status with load balancer IPs + enabled: true + # Deployment mode: auto (detect), daemonset, nodeport, or clusterip + deploymentMode: auto + # External hostname override (for ClusterIP mode without LoadBalancer) + externalHostname: "" + # How often to update status (seconds) + updateInterval: 30 + podAnnotations: {} podSecurityContext: {} diff --git a/src/functions/__init__.py b/src/functions/__init__.py index 979eb42..51f22d1 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -105,6 +105,12 @@ class ContainerEnv: env_vars["plugins"]["config"].setdefault(plugin_name, {}) env_vars["plugins"]["config"][plugin_name][config_key] = value + # Ingress status update configuration + env_vars["update_ingress_status"] = os.getenv("EASYHAPROXY_UPDATE_INGRESS_STATUS", "true").lower() == "true" + env_vars["deployment_mode"] = os.getenv("EASYHAPROXY_DEPLOYMENT_MODE", "auto") + env_vars["external_hostname"] = os.getenv("EASYHAPROXY_EXTERNAL_HOSTNAME", "") + env_vars["ingress_status_update_interval"] = int(os.getenv("EASYHAPROXY_STATUS_UPDATE_INTERVAL", "30")) + return env_vars diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 9ba6248..3e363a3 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -226,8 +226,214 @@ class Kubernetes(ProcessorInterface): self.api_instance = client.CoreV1Api() self.v1 = client.NetworkingV1Api() self.cert_cache = {} + self.deployment_mode_cache = None + self.ingress_addresses_cache = None + self.addresses_cache_time = 0 super().__init__() + def _detect_deployment_mode(self): + """ + Detect the deployment mode (daemonset, nodeport, clusterip). + Returns: tuple (mode: str, service: V1Service or None) + """ + import os + import time + + # Return cached if available + if self.deployment_mode_cache: + return self.deployment_mode_cache + + env_config = ContainerEnv.read() + + # Check for manual override + if env_config['deployment_mode'] != 'auto': + loggerEasyHaproxy.info(f"Using manual deployment mode: {env_config['deployment_mode']}") + service = self._get_easyhaproxy_service() if env_config['deployment_mode'] in ['nodeport', 'clusterip'] else None + self.deployment_mode_cache = (env_config['deployment_mode'], service) + return self.deployment_mode_cache + + try: + # Get current pod name from hostname + pod_name = socket.gethostname() + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + + # Read current pod + pod = self.api_instance.read_namespaced_pod(pod_name, namespace) + + # Check owner references to determine if DaemonSet or Deployment + if pod.metadata.owner_references: + owner_kind = pod.metadata.owner_references[0].kind + + if owner_kind == 'DaemonSet': + loggerEasyHaproxy.info("Detected deployment mode: daemonset") + self.deployment_mode_cache = ('daemonset', None) + return self.deployment_mode_cache + elif owner_kind in ['ReplicaSet', 'Deployment']: + # Check if Service exists + service = self._get_easyhaproxy_service() + if service: + if service.spec.type == 'NodePort': + loggerEasyHaproxy.info("Detected deployment mode: nodeport") + self.deployment_mode_cache = ('nodeport', service) + return self.deployment_mode_cache + else: + loggerEasyHaproxy.info("Detected deployment mode: clusterip") + self.deployment_mode_cache = ('clusterip', service) + return self.deployment_mode_cache + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to detect deployment mode: {e}, defaulting to daemonset") + + self.deployment_mode_cache = ('daemonset', None) + return self.deployment_mode_cache + + def _get_easyhaproxy_service(self): + """Get the EasyHAProxy service if it exists.""" + import os + + try: + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + # Try common service names + service_names = ['easyhaproxy', 'ingress-easyhaproxy'] + + for service_name in service_names: + try: + service = self.api_instance.read_namespaced_service(service_name, namespace) + return service + except: + continue + return None + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to get EasyHAProxy service: {e}") + return None + + def _get_ingress_addresses(self, mode, service): + """ + Get IP addresses or hostnames to report in ingress status. + + Args: + mode: Deployment mode (daemonset, nodeport, clusterip) + service: V1Service object (for nodeport/clusterip modes) + + Returns: + List of dicts: [{"ip": "..."}, {"hostname": "..."}] + """ + import os + import time + + env_config = ContainerEnv.read() + cache_ttl = env_config.get('ingress_status_update_interval', 30) + + # Return cached if still valid + if self.ingress_addresses_cache and (time.time() - self.addresses_cache_time) < cache_ttl: + return self.ingress_addresses_cache + + addresses = [] + + try: + if mode == 'daemonset': + # Get nodes where DaemonSet pods are running + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + label_selector = "app.kubernetes.io/name=easyhaproxy" + + pods = self.api_instance.list_namespaced_pod(namespace, label_selector=label_selector) + node_names = set(pod.spec.node_name for pod in pods.items if pod.spec.node_name) + + # Get external IPs from these nodes + for node_name in node_names: + node = self.api_instance.read_node(node_name) + for addr in node.status.addresses: + if addr.type == 'ExternalIP': + addresses.append({"ip": addr.address}) + break + else: + # Fallback to InternalIP if no ExternalIP + for addr in node.status.addresses: + if addr.type == 'InternalIP': + addresses.append({"ip": addr.address}) + break + + elif mode == 'nodeport': + # Get all node IPs (traffic can reach any node via NodePort) + nodes = self.api_instance.list_node() + for node in nodes.items: + for addr in node.status.addresses: + if addr.type == 'ExternalIP': + addresses.append({"ip": addr.address}) + break + else: + # Fallback to InternalIP + for addr in node.status.addresses: + if addr.type == 'InternalIP': + addresses.append({"ip": addr.address}) + break + + elif mode == 'clusterip': + # Check if LoadBalancer status is available + if service and service.status and service.status.load_balancer: + lb_ingress = service.status.load_balancer.ingress or [] + for ing in lb_ingress: + if ing.ip: + addresses.append({"ip": ing.ip}) + if ing.hostname: + addresses.append({"hostname": ing.hostname}) + + # If no LoadBalancer, check for external hostname override + if not addresses and env_config['external_hostname']: + addresses.append({"hostname": env_config['external_hostname']}) + + # Fallback to ClusterIP + if not addresses and service: + addresses.append({"ip": service.spec.cluster_ip}) + + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to get ingress addresses: {e}") + + # Cache the result + self.ingress_addresses_cache = addresses + self.addresses_cache_time = time.time() + + return addresses + + def _update_ingress_status(self, ingress, addresses): + """ + Update the status of an ingress resource. + + Args: + ingress: V1Ingress object + addresses: List of address dicts [{"ip": "..."}, {"hostname": "..."}] + """ + if not addresses: + return + + try: + # Create status patch + status_body = { + "status": { + "loadBalancer": { + "ingress": addresses + } + } + } + + # Update status using patch (not replace) + self.v1.patch_namespaced_ingress_status( + name=ingress.metadata.name, + namespace=ingress.metadata.namespace, + body=status_body, + field_manager="easyhaproxy" + ) + + loggerEasyHaproxy.debug( + f"Updated ingress {ingress.metadata.namespace}/{ingress.metadata.name} " + f"status with {len(addresses)} address(es)" + ) + + except Exception as e: + loggerEasyHaproxy.warn( + f"Failed to update status for ingress " + f"{ingress.metadata.namespace}/{ingress.metadata.name}: {e}" + ) + def _check_annotation(self, annotations, key, default=None): if key not in annotations: return default @@ -237,6 +443,14 @@ class Kubernetes(ProcessorInterface): ret = self.v1.list_ingress_for_all_namespaces(watch=False) + # Detect deployment mode once per cycle for ingress status updates + env_config = ContainerEnv.read() + if env_config['update_ingress_status']: + deployment_mode, service = self._detect_deployment_mode() + ingress_addresses = self._get_ingress_addresses(deployment_mode, service) + else: + ingress_addresses = [] + self.parsed_object = {} for ingress in ret.items: # Support both new spec.ingressClassName and deprecated annotation for backward compatibility @@ -340,3 +554,7 @@ class Kubernetes(ProcessorInterface): if cluster_ip not in self.parsed_object.keys(): self.parsed_object[cluster_ip] = data self.parsed_object[cluster_ip].update(rule_data) + + # Update ingress status if enabled + if env_config['update_ingress_status'] and ingress_addresses: + self._update_ingress_status(ingress, ingress_addresses)