from typing import Optional, Dict, List, Any from phi.k8s.app.base import ( K8sApp, AppVolumeType, # noqa: F401 ContainerContext, # noqa: F401 ServiceType, RestartPolicy, # noqa: F401 ImagePullPolicy, # noqa: F401 LoadBalancerProvider, # noqa: F401 ) from phi.k8s.app.traefik.crds import ingressroute_crd, middleware_crd from phi.utils.log import logger class TraefikRouter(K8sApp): # -*- App Name name: str = "traefik" # -*- Image Configuration image_name: str = "traefik" image_tag: str = "v2.10" # -*- RBAC Configuration # Create a ServiceAccount, ClusterRole, and ClusterRoleBinding create_rbac: bool = True # -*- Install traefik CRDs # See: https://doc.traefik.io/traefik/providers/kubernetes-crd/#configuration-requirements install_crds: bool = False # -*- Traefik Configuration domain_name: Optional[str] = None # Enable Access Logs access_logs: bool = True # Traefik config file on the host traefik_config_file: Optional[str] = None # Traefik config file on the container traefik_config_file_container_path: str = "/etc/traefik/traefik.yaml" # -*- HTTP Configuration http_enabled: bool = False http_routes: Optional[List[dict]] = None http_container_port: int = 80 http_service_port: int = 80 http_node_port: Optional[int] = None http_key: str = "http" http_ingress_name: str = "http-ingress" forward_http_to_https: bool = False enable_http_proxy_protocol: bool = False enable_http_forward_headers: bool = False # -*- HTTPS Configuration https_enabled: bool = False https_routes: Optional[List[dict]] = None https_container_port: int = 443 https_service_port: int = 443 https_node_port: Optional[int] = None https_key: str = "https" https_ingress_name: str = "https-ingress" enable_https_proxy_protocol: bool = False enable_https_forward_headers: bool = False add_headers: Optional[Dict[str, dict]] = None # -*- Dashboard Configuration dashboard_enabled: bool = False dashboard_routes: Optional[List[dict]] = None dashboard_container_port: int = 8080 dashboard_service_port: int = 8080 dashboard_node_port: Optional[int] = None dashboard_key: str = "dashboard" dashboard_ingress_name: str = "dashboard-ingress" # The dashboard is gated behind a user:password, which is generated using # htpasswd -nb user password # You can provide the "users:password" list as a dashboard_auth_users param # or as DASHBOARD_AUTH_USERS in the secrets_file # Using the secrets_file is recommended dashboard_auth_users: Optional[str] = None insecure_api_access: bool = False # -*- Service Configuration create_service: bool = True def get_dashboard_auth_users(self) -> Optional[str]: return self.dashboard_auth_users or self.get_secret_from_file("DASHBOARD_AUTH_USERS") def get_ingress_rules(self) -> List[Any]: from kubernetes.client.models.v1_ingress_rule import V1IngressRule from kubernetes.client.models.v1_ingress_backend import V1IngressBackend from kubernetes.client.models.v1_ingress_service_backend import V1IngressServiceBackend from kubernetes.client.models.v1_http_ingress_path import V1HTTPIngressPath from kubernetes.client.models.v1_http_ingress_rule_value import V1HTTPIngressRuleValue from kubernetes.client.models.v1_service_port import V1ServicePort ingress_rules = [ V1IngressRule( http=V1HTTPIngressRuleValue( paths=[ V1HTTPIngressPath( path="/", path_type="Prefix", backend=V1IngressBackend( service=V1IngressServiceBackend( name=self.get_service_name(), port=V1ServicePort( name=self.https_key if self.https_enabled else self.http_key, port=self.https_service_port if self.https_enabled else self.http_service_port, ), ) ), ), ] ), ) ] if self.dashboard_enabled: ingress_rules[0].http.paths.append( V1HTTPIngressPath( path="/", path_type="Prefix", backend=V1IngressBackend( service=V1IngressServiceBackend( name=self.get_service_name(), port=V1ServicePort( name=self.dashboard_key, port=self.dashboard_service_port, ), ) ), ) ) return ingress_rules def get_cr_policy_rules(self) -> List[Any]: from phi.k8s.create.rbac_authorization_k8s_io.v1.cluster_role import ( PolicyRule, ) return [ PolicyRule( api_groups=[""], resources=["services", "endpoints", "secrets"], verbs=["get", "list", "watch"], ), PolicyRule( api_groups=["extensions", "networking.k8s.io"], resources=["ingresses", "ingressclasses"], verbs=["get", "list", "watch"], ), PolicyRule( api_groups=["extensions", "networking.k8s.io"], resources=["ingresses/status"], verbs=["update"], ), PolicyRule( api_groups=["traefik.io", "traefik.containo.us"], resources=[ "middlewares", "middlewaretcps", "ingressroutes", "traefikservices", "ingressroutetcps", "ingressrouteudps", "tlsoptions", "tlsstores", "serverstransports", ], verbs=["get", "list", "watch"], ), ] def get_container_args(self) -> Optional[List[str]]: if self.command is not None: if isinstance(self.command, str): return self.command.strip().split(" ") return self.command container_args = ["--providers.kubernetescrd"] if self.access_logs: container_args.append("--accesslog") if self.http_enabled: container_args.append(f"--entrypoints.{self.http_key}.Address=:{self.http_service_port}") if self.enable_http_proxy_protocol: container_args.append(f"--entrypoints.{self.http_key}.proxyProtocol.insecure=true") if self.enable_http_forward_headers: container_args.append(f"--entrypoints.{self.http_key}.forwardedHeaders.insecure=true") if self.https_enabled: container_args.append(f"--entrypoints.{self.https_key}.Address=:{self.https_service_port}") if self.enable_https_proxy_protocol: container_args.append(f"--entrypoints.{self.https_key}.proxyProtocol.insecure=true") if self.enable_https_forward_headers: container_args.append(f"--entrypoints.{self.https_key}.forwardedHeaders.insecure=true") if self.forward_http_to_https: container_args.extend( [ f"--entrypoints.{self.http_key}.http.redirections.entryPoint.to={self.https_key}", f"--entrypoints.{self.http_key}.http.redirections.entryPoint.scheme=https", ] ) if self.dashboard_enabled: container_args.append("--api=true") container_args.append("--api.dashboard=true") if self.insecure_api_access: container_args.append("--api.insecure") return container_args def get_secrets(self) -> List[Any]: return self.add_secrets or [] def get_ports(self) -> List[Any]: from phi.k8s.create.common.port import CreatePort ports: List[CreatePort] = self.add_ports or [] if self.http_enabled: web_port = CreatePort( name=self.http_key, container_port=self.http_container_port, service_port=self.http_service_port, target_port=self.http_key, ) if ( self.service_type in (ServiceType.NODE_PORT, ServiceType.LOAD_BALANCER) and self.http_node_port is not None ): web_port.node_port = self.http_node_port ports.append(web_port) if self.https_enabled: websecure_port = CreatePort( name=self.https_key, container_port=self.https_container_port, service_port=self.https_service_port, target_port=self.https_key, ) if ( self.service_type in (ServiceType.NODE_PORT, ServiceType.LOAD_BALANCER) and self.https_node_port is not None ): websecure_port.node_port = self.https_node_port ports.append(websecure_port) if self.dashboard_enabled: dashboard_port = CreatePort( name=self.dashboard_key, container_port=self.dashboard_container_port, service_port=self.dashboard_service_port, target_port=self.dashboard_key, ) if ( self.service_type in (ServiceType.NODE_PORT, ServiceType.LOAD_BALANCER) and self.dashboard_node_port is not None ): dashboard_port.node_port = self.dashboard_node_port ports.append(dashboard_port) return ports def add_app_resources(self, namespace: str, service_account_name: Optional[str]) -> List[Any]: from phi.k8s.create.apiextensions_k8s_io.v1.custom_object import CreateCustomObject app_resources = self.add_resources or [] if self.http_enabled: http_ingressroute = CreateCustomObject( name=self.http_ingress_name, crd=ingressroute_crd, spec={ "entryPoints": [self.http_key], "routes": self.http_routes, }, app_name=self.get_app_name(), namespace=namespace, ) app_resources.append(http_ingressroute) logger.debug(f"Added IngressRoute: {http_ingressroute.name}") if self.https_enabled: https_ingressroute = CreateCustomObject( name=self.https_ingress_name, crd=ingressroute_crd, spec={ "entryPoints": [self.https_key], "routes": self.https_routes, }, app_name=self.get_app_name(), namespace=namespace, ) app_resources.append(https_ingressroute) logger.debug(f"Added IngressRoute: {https_ingressroute.name}") if self.add_headers: headers_middleware = CreateCustomObject( name="header-middleware", crd=middleware_crd, spec={ "headers": self.add_headers, }, app_name=self.get_app_name(), namespace=namespace, ) app_resources.append(headers_middleware) logger.debug(f"Added Middleware: {headers_middleware.name}") if self.dashboard_enabled: # create dashboard_auth_middleware if auth provided # ref: https://doc.traefik.io/traefik/operations/api/#configuration dashboard_auth_middleware = None dashboard_auth_users = self.get_dashboard_auth_users() if dashboard_auth_users is not None: from phi.k8s.create.core.v1.secret import CreateSecret dashboard_auth_secret = CreateSecret( secret_name="dashboard-auth-secret", app_name=self.get_app_name(), namespace=namespace, string_data={"users": dashboard_auth_users}, ) app_resources.append(dashboard_auth_secret) logger.debug(f"Added Secret: {dashboard_auth_secret.secret_name}") dashboard_auth_middleware = CreateCustomObject( name="dashboard-auth-middleware", crd=middleware_crd, spec={"basicAuth": {"secret": dashboard_auth_secret.secret_name}}, app_name=self.get_app_name(), namespace=namespace, ) app_resources.append(dashboard_auth_middleware) logger.debug(f"Added Middleware: {dashboard_auth_middleware.name}") dashboard_routes = self.dashboard_routes # use default dashboard routes if dashboard_routes is None: # domain must be provided if self.domain_name is not None: dashboard_routes = [ { "kind": "Rule", "match": f"Host(`traefik.{self.domain_name}`)", "middlewares": [ { "name": dashboard_auth_middleware.name, "namespace": namespace, }, ] if dashboard_auth_middleware is not None else [], "services": [ { "kind": "TraefikService", "name": "api@internal", } ], }, ] dashboard_ingressroute = CreateCustomObject( name=self.dashboard_ingress_name, crd=ingressroute_crd, spec={ "routes": dashboard_routes, }, app_name=self.get_app_name(), namespace=namespace, ) app_resources.append(dashboard_ingressroute) logger.debug(f"Added IngressRoute: {dashboard_ingressroute.name}") if self.install_crds: from phi.k8s.resource.yaml import YamlResource if self.yaml_resources is None: self.yaml_resources = [] self.yaml_resources.append( YamlResource( name="traefik-crds", url="https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml", ) ) logger.debug("Added CRD yaml") return app_resources