Files
the-littlest-jupyterhub/tljh/traefik.py

134 lines
4.4 KiB
Python
Raw Normal View History

"""Traefik installation and setup"""
import hashlib
import os
2020-05-29 17:59:20 +03:00
from glob import glob
2018-08-01 17:06:52 +02:00
from jinja2 import Template
from passlib.apache import HtpasswdFile
import backoff
2019-05-24 14:37:42 +03:00
import requests
2020-05-29 17:59:20 +03:00
import toml
2020-05-29 17:59:20 +03:00
from .config import CONFIG_DIR
from tljh.configurer import load_config
# FIXME: support more than one platform here
plat = "linux-amd64"
2019-10-01 11:00:28 +03:00
traefik_version = "1.7.18"
# record sha256 hashes for supported platforms here
checksums = {
2019-10-01 11:00:28 +03:00
"linux-amd64": "3c2d153d80890b6fc8875af9f8ced32c4d684e1eb5a46d9815337cb343dfd92e"
}
2020-05-29 17:59:20 +03:00
traefik_extra_config_dir = "traefik_config.d"
def checksum_file(path):
"""Compute the sha256 checksum of a path"""
hasher = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
2019-05-24 14:37:42 +03:00
def fatal_error(e):
# Retry only when connection is reset or we think we didn't download entire file
return str(e) != "ContentTooShort" and not isinstance(e, ConnectionResetError)
@backoff.on_exception(
backoff.expo,
2019-05-24 14:37:42 +03:00
Exception,
max_tries=2,
giveup=fatal_error
)
def ensure_traefik_binary(prefix):
"""Download and install the traefik binary"""
traefik_bin = os.path.join(prefix, "bin", "traefik")
if os.path.exists(traefik_bin):
checksum = checksum_file(traefik_bin)
if checksum == checksums[plat]:
# already have the right binary
# ensure permissions and we're done
os.chmod(traefik_bin, 0o755)
return
else:
print(f"checksum mismatch on {traefik_bin}")
os.remove(traefik_bin)
traefik_url = (
"https://github.com/containous/traefik/releases"
f"/download/v{traefik_version}/traefik_{plat}"
)
print(f"Downloading traefik {traefik_version}...")
# download the file
2019-05-24 14:37:42 +03:00
response = requests.get(traefik_url)
if response.status_code == 206:
raise Exception("ContentTooShort")
with open(traefik_bin, 'wb') as f:
f.write(response.content)
os.chmod(traefik_bin, 0o755)
# verify that we got what we expected
checksum = checksum_file(traefik_bin)
if checksum != checksums[plat]:
raise IOError(f"Checksum failed {traefik_bin}: {checksum} != {checksums[plat]}")
def compute_basic_auth(username, password):
"""Generate hashed HTTP basic auth from traefik_api username+password"""
ht = HtpasswdFile()
# generate htpassword
ht.set_password(username, password)
hashed_password = str(ht.to_string()).split(":")[1][:-3]
return username + ":" + hashed_password
2020-05-29 17:59:20 +03:00
def load_extra_config(std_toml):
extra_configs = sorted(glob(os.path.join(CONFIG_DIR, traefik_extra_config_dir, '*.py')))
config = toml.load(extra_configs + [std_toml])
return config
def ensure_traefik_config(state_dir):
"""Render the traefik.toml config file"""
config = load_config()
config['traefik_api']['basic_auth'] = compute_basic_auth(
config['traefik_api']['username'],
config['traefik_api']['password'],
)
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
template = Template(f.read())
new_toml = template.render(config)
https = config["https"]
letsencrypt = https["letsencrypt"]
tls = https["tls"]
# validate https config
if https["enabled"]:
if not tls["cert"] and not letsencrypt["email"]:
raise ValueError(
"To enable https, you must set tls.cert+key or letsencrypt.email+domains"
)
if (letsencrypt["email"] and not letsencrypt["domains"]) or (
letsencrypt["domains"] and not letsencrypt["email"]
):
raise ValueError("Both email and domains must be set for letsencrypt")
2020-05-29 17:59:20 +03:00
# Ensure extra config dir exists and is private
for path in [CONFIG_DIR, os.path.join(CONFIG_DIR, traefik_extra_config_dir)]:
os.makedirs(path, mode=0o700, exist_ok=True)
traefik_toml = load_extra_config(new_toml)
print(f"Writing traefik config {traefik_toml}...")
with open(os.path.join(state_dir, "traefik.toml"), "w") as f:
2019-02-11 09:24:16 +02:00
os.fchmod(f.fileno(), 0o600)
2020-05-29 17:59:20 +03:00
f.write(traefik_toml)
2019-01-22 16:24:38 +02:00
with open(os.path.join(state_dir, "rules.toml"), "w") as f:
2019-02-11 09:24:16 +02:00
os.fchmod(f.fileno(), 0o600)
2019-01-22 16:24:38 +02:00
# ensure acme.json exists and is private
with open(os.path.join(state_dir, "acme.json"), "a") as f:
os.fchmod(f.fileno(), 0o600)