Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Docker image: add support for SYNAPSE_DATA_DIR parameter #5563

Merged
merged 8 commits into from
Jun 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5561.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update Docker image to deprecate the use of environment variables for configuration, and make the use of a static configuration the default.
1 change: 1 addition & 0 deletions changelog.d/5563.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Docker: Use a sensible location for data files when generating a config file.
11 changes: 11 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,14 @@ docker run -d --name synapse \
-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
matrixdotorg/synapse:latest
```

The following environment variables are supported in this mode:

* `SYNAPSE_SERVER_NAME` (mandatory): the server public hostname.
* `SYNAPSE_REPORT_STATS` (mandatory, `yes` or `no`): whether to enable
anonymous statistics reporting.
* `SYNAPSE_CONFIG_PATH` (mandatory): path to the file to be generated.
* `SYNAPSE_DATA_DIR`: where the generated config will put persistent data
such as the datatase and media store. Defaults to `/data`.
* `UID`, `GID`: the user id and group id to use for creating the data
directories. Defaults to `991`, `991`.
188 changes: 126 additions & 62 deletions docker/start.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,162 @@
#!/usr/local/bin/python

import jinja2
import codecs
import glob
import os
import sys
import subprocess
import glob
import codecs
import sys

import jinja2


# Utility functions
convert = lambda src, dst, environ: open(dst, "w").write(
jinja2.Template(open(src).read()).render(**environ)
)
def log(txt):
print(txt, file=sys.stderr)


def error(txt):
log(txt)
sys.exit(2)


def convert(src, dst, environ):
"""Generate a file from a template

Args:
src (str): path to input file
dst (str): path to file to write
environ (dict): environment dictionary, for replacement mappings.
"""
with open(src) as infile:
template = infile.read()
rendered = jinja2.Template(template).render(**environ)
with open(dst, "w") as outfile:
outfile.write(rendered)

def check_arguments(environ, args):
for argument in args:
if argument not in environ:
print("Environment variable %s is mandatory, exiting." % argument)
sys.exit(2)

def generate_config_from_template(environ, ownership):
"""Generate a homeserver.yaml from environment variables

Args:
environ (dict): environment dictionary
ownership (str): "<user>:<group>" string which will be used to set
ownership of the generated configs

Returns:
path to generated config file
"""
for v in ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS"):
if v not in environ:
error(
"Environment variable '%s' is mandatory when generating a config "
"file on-the-fly." % (v,)
)

# populate some params from data files (if they exist, else create new ones)
environ = environ.copy()
secrets = {
"registration": "SYNAPSE_REGISTRATION_SHARED_SECRET",
"macaroon": "SYNAPSE_MACAROON_SECRET_KEY",
}

def generate_secrets(environ, secrets):
for name, secret in secrets.items():
if secret not in environ:
filename = "/data/%s.%s.key" % (environ["SYNAPSE_SERVER_NAME"], name)

# if the file already exists, load in the existing value; otherwise,
# generate a new secret and write it to a file

if os.path.exists(filename):
with open(filename) as handle:
value = handle.read()
else:
print("Generating a random secret for {}".format(name))
log("Generating a random secret for {}".format(name))
value = codecs.encode(os.urandom(32), "hex").decode()
with open(filename, "w") as handle:
handle.write(value)
environ[secret] = value

environ["SYNAPSE_APPSERVICES"] = glob.glob("/data/appservices/*.yaml")
if not os.path.exists("/compiled"):
os.mkdir("/compiled")

config_path = "/compiled/homeserver.yaml"

# Convert SYNAPSE_NO_TLS to boolean if exists
if "SYNAPSE_NO_TLS" in environ:
tlsanswerstring = str.lower(environ["SYNAPSE_NO_TLS"])
if tlsanswerstring in ("true", "on", "1", "yes"):
environ["SYNAPSE_NO_TLS"] = True
else:
if tlsanswerstring in ("false", "off", "0", "no"):
environ["SYNAPSE_NO_TLS"] = False
else:
error(
'Environment variable "SYNAPSE_NO_TLS" found but value "'
+ tlsanswerstring
+ '" unrecognized; exiting.'
)

convert("/conf/homeserver.yaml", config_path, environ)
convert("/conf/log.config", "/compiled/log.config", environ)
subprocess.check_output(["chown", "-R", ownership, "/data"])
return config_path


def run_generate_config(environ, ownership):
"""Run synapse with a --generate-config param to generate a template config file

Args:
environ (dict): env var dict
ownership (str): "userid:groupid" arg for chmod

Never returns.
"""
for v in ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS", "SYNAPSE_CONFIG_PATH"):
if v not in environ:
error("Environment variable '%s' is mandatory in `generate` mode." % (v,))

# Prepare the configuration
mode = sys.argv[1] if len(sys.argv) > 1 else None
environ = os.environ.copy()
ownership = "{}:{}".format(environ.get("UID", 991), environ.get("GID", 991))
args = ["python", "-m", "synapse.app.homeserver"]
data_dir = environ.get("SYNAPSE_DATA_DIR", "/data")

# In generate mode, generate a configuration, missing keys, then exit
if mode == "generate":
check_arguments(
environ, ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS", "SYNAPSE_CONFIG_PATH")
)
args += [
# make sure that synapse has perms to write to the data dir.
subprocess.check_output(["chown", ownership, data_dir])

args = [
"python",
"-m",
"synapse.app.homeserver",
"--server-name",
environ["SYNAPSE_SERVER_NAME"],
"--report-stats",
environ["SYNAPSE_REPORT_STATS"],
"--config-path",
environ["SYNAPSE_CONFIG_PATH"],
"--data-directory",
data_dir,
"--generate-config",
]
# log("running %s" % (args, ))
os.execv("/usr/local/bin/python", args)

# In normal mode, generate missing keys if any, then run synapse
else:

def main(args, environ):
mode = args[1] if len(args) > 1 else None
ownership = "{}:{}".format(environ.get("UID", 991), environ.get("GID", 991))

# In generate mode, generate a configuration and missing keys, then exit
if mode == "generate":
return run_generate_config(environ, ownership)

# In normal mode, generate missing keys if any, then run synapse
if "SYNAPSE_CONFIG_PATH" in environ:
config_path = environ["SYNAPSE_CONFIG_PATH"]
else:
check_arguments(environ, ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS"))
generate_secrets(
environ,
{
"registration": "SYNAPSE_REGISTRATION_SHARED_SECRET",
"macaroon": "SYNAPSE_MACAROON_SECRET_KEY",
},
)
environ["SYNAPSE_APPSERVICES"] = glob.glob("/data/appservices/*.yaml")
if not os.path.exists("/compiled"):
os.mkdir("/compiled")

config_path = "/compiled/homeserver.yaml"

# Convert SYNAPSE_NO_TLS to boolean if exists
if "SYNAPSE_NO_TLS" in environ:
tlsanswerstring = str.lower(environ["SYNAPSE_NO_TLS"])
if tlsanswerstring in ("true", "on", "1", "yes"):
environ["SYNAPSE_NO_TLS"] = True
else:
if tlsanswerstring in ("false", "off", "0", "no"):
environ["SYNAPSE_NO_TLS"] = False
else:
print(
'Environment variable "SYNAPSE_NO_TLS" found but value "'
+ tlsanswerstring
+ '" unrecognized; exiting.'
)
sys.exit(2)

convert("/conf/homeserver.yaml", config_path, environ)
convert("/conf/log.config", "/compiled/log.config", environ)
subprocess.check_output(["chown", "-R", ownership, "/data"])

args += [
config_path = generate_config_from_template(environ, ownership)

args = [
"python",
"-m",
"synapse.app.homeserver",
"--config-path",
config_path,
# tell synapse to put any generated keys in /data rather than /compiled
Expand All @@ -107,3 +167,7 @@ def generate_secrets(environ, secrets):
# Generate missing keys and start synapse
subprocess.check_output(args + ["--generate-keys"])
os.execv("/sbin/su-exec", ["su-exec", ownership] + args)


if __name__ == "__main__":
main(sys.argv, os.environ)