File size: 15,439 Bytes
105b369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
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