2023-08-18 17:54:34 +01:00
|
|
|
from typing import NamedTuple
|
|
|
|
import re
|
2023-11-02 22:26:26 +00:00
|
|
|
from io import StringIO
|
2023-08-24 21:49:26 +01:00
|
|
|
import sys
|
2023-10-31 18:22:31 +00:00
|
|
|
import tomllib
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import TypedDict
|
2023-10-31 19:15:26 +00:00
|
|
|
from collections import defaultdict
|
2023-11-01 21:45:14 +00:00
|
|
|
from urllib.request import urlopen
|
2023-11-02 21:59:49 +00:00
|
|
|
import argparse
|
2023-11-01 21:45:14 +00:00
|
|
|
import json
|
2023-11-02 22:08:51 +00:00
|
|
|
import subprocess
|
2023-08-18 17:54:34 +01:00
|
|
|
|
|
|
|
TRAEFIK_HOST_RE = re.compile(r"Host\(`([a-z0-9\.-]+)`\)")
|
|
|
|
|
2023-10-31 18:22:31 +00:00
|
|
|
|
2023-08-18 17:54:34 +01:00
|
|
|
class Route(NamedTuple):
|
|
|
|
name: str
|
|
|
|
destination: str
|
|
|
|
hostname: str
|
|
|
|
|
|
|
|
|
2023-10-31 18:22:31 +00:00
|
|
|
class Source(TypedDict):
|
|
|
|
type: str
|
|
|
|
url: str
|
|
|
|
destination: str
|
|
|
|
|
|
|
|
|
|
|
|
class Config(TypedDict):
|
|
|
|
source: list[Source]
|
|
|
|
|
|
|
|
|
2023-08-24 21:49:26 +01:00
|
|
|
def parse_traefik_rule(rule: str) -> list[str]:
|
|
|
|
if "Host(" not in rule:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return TRAEFIK_HOST_RE.findall(rule)
|
|
|
|
|
|
|
|
|
2023-08-18 17:54:34 +01:00
|
|
|
def get_traefik_routes(traefik_host: str, traefik_route: str):
|
2023-11-01 21:45:14 +00:00
|
|
|
with urlopen(f"{traefik_host}/api/http/routers") as response:
|
|
|
|
api_response = json.load(response)
|
|
|
|
|
2023-08-18 17:54:34 +01:00
|
|
|
routes = set()
|
|
|
|
|
|
|
|
for router in api_response:
|
2023-08-24 21:49:26 +01:00
|
|
|
hosts = parse_traefik_rule(router["rule"])
|
2023-08-18 17:54:34 +01:00
|
|
|
|
|
|
|
if not hosts:
|
2023-08-24 21:49:26 +01:00
|
|
|
print(f"Failed to find host for {router['rule']}", file=sys.stderr)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if len(hosts) > 1:
|
|
|
|
print(f"WARNING: Found multiple hosts for rule: {router['rule']}", file=sys.stderr)
|
2023-08-18 17:54:34 +01:00
|
|
|
|
|
|
|
routes.add(Route(
|
|
|
|
router["service"],
|
|
|
|
traefik_route,
|
|
|
|
hosts[0]
|
|
|
|
))
|
|
|
|
|
|
|
|
return routes
|
|
|
|
|
|
|
|
def get_dokku_routes(dokku_exporter_url: str, dokku_route: str):
|
2023-11-01 21:45:14 +00:00
|
|
|
with urlopen(dokku_exporter_url) as response:
|
|
|
|
api_response = json.load(response)
|
2023-08-18 17:54:34 +01:00
|
|
|
|
|
|
|
routes = set()
|
|
|
|
|
|
|
|
for app in api_response:
|
|
|
|
for vhost in app["vhosts"]:
|
|
|
|
routes.add(Route(
|
|
|
|
app["app"],
|
|
|
|
dokku_route,
|
|
|
|
vhost
|
|
|
|
))
|
|
|
|
|
|
|
|
return routes
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2023-11-02 21:59:49 +00:00
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
|
|
|
parser.add_argument("--config", "-c", default="./config.toml", type=Path, help="Config file. Default: %(default)s")
|
|
|
|
parser.add_argument("--output", "-o", type=Path, default="./map.txt", help="Output file. Default: %(default)s")
|
2023-11-02 22:08:51 +00:00
|
|
|
parser.add_argument("--reload", action="store_true", help="Reload nginx on completion")
|
2023-11-02 21:59:49 +00:00
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
config: Config = tomllib.loads(args.config.read_text())
|
2023-10-31 18:22:31 +00:00
|
|
|
|
2023-08-18 17:54:34 +01:00
|
|
|
routes = []
|
2023-10-31 18:22:31 +00:00
|
|
|
for source in config["source"]:
|
|
|
|
match source["type"]:
|
|
|
|
case "traefik":
|
|
|
|
routes.extend(get_traefik_routes(source["url"], source["destination"]))
|
|
|
|
case "dokku":
|
|
|
|
routes.extend(get_dokku_routes(source["url"], source["destination"]))
|
2023-08-18 17:54:34 +01:00
|
|
|
|
2023-10-31 19:10:35 +00:00
|
|
|
if len(routes) != len(set(routes)):
|
|
|
|
raise ValueError("Conflict found!")
|
|
|
|
|
2023-10-31 19:15:26 +00:00
|
|
|
grouped_routes = defaultdict(set)
|
|
|
|
for route in routes:
|
|
|
|
grouped_routes[route.hostname, route.destination].add(route.name)
|
2023-08-18 17:54:34 +01:00
|
|
|
|
2023-10-31 19:15:26 +00:00
|
|
|
print("Found", len(routes), "routes, grouped into", len(grouped_routes), "groups")
|
2023-10-31 19:10:35 +00:00
|
|
|
|
2023-11-02 22:26:26 +00:00
|
|
|
output = StringIO()
|
|
|
|
for (hostname, destination), names in sorted(grouped_routes.items()):
|
|
|
|
output.write(f"{hostname}\t{destination}; # {', '.join(names)}\n")
|
|
|
|
|
|
|
|
if args.output.is_file():
|
|
|
|
current_output = args.output.read_text()
|
|
|
|
|
|
|
|
if current_output == output.getvalue():
|
|
|
|
print("Output identical - aborting")
|
|
|
|
return
|
|
|
|
|
|
|
|
args.output.write_text(output.getvalue())
|
|
|
|
|
2023-11-02 22:08:51 +00:00
|
|
|
if args.reload:
|
|
|
|
print("Reloading nginx")
|
|
|
|
subprocess.check_call(
|
|
|
|
[
|
|
|
|
"nginx",
|
|
|
|
"-s",
|
|
|
|
"reload"
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2023-08-18 17:54:34 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|