Skip to content

Commit 662e6bf

Browse files
authored
Merge pull request #13 from vimc/VIMC-7467
Add support for certificates from Let's Encrypt.
2 parents f8fe27a + 0371e65 commit 662e6bf

17 files changed

+452
-76
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ jobs:
4949
docker pull vimc/montagu-db:master
5050
docker pull vimc/montagu-migrate:master
5151
docker pull vimc/montagu-reverse-proxy:master
52-
docker pull vimc/montagu-reverse-proxy:vimc-7152
5352
docker pull vimc/orderly-web-user-cli:master
5453
docker pull vimc/orderly-web:master
5554
docker pull vimc/orderly.server:master
5655
docker pull vimc/orderlyweb-migrate:master
5756
docker pull vimc/task-queue-worker:master
57+
docker pull ghcr.io/letsencrypt/pebble:latest
5858
- name: Test
5959
env:
6060
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}

README.md

+1-22
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,7 @@ pip install montagu-deploy
1616
## Usage
1717

1818
```
19-
$ montagu --help
20-
Usage:
21-
montagu --version
22-
montagu start <path> [--extra=PATH] [--option=OPTION]... [--pull]
23-
montagu status <path>
24-
montagu stop <path> [--volumes] [--network] [--kill] [--force]
25-
[--extra=PATH] [--option=OPTION]...
26-
27-
Options:
28-
--extra=PATH Path, relative to <path>, of yml file of additional
29-
configuration
30-
--option=OPTION Additional configuration options, in the form key=value
31-
Use dots in key for hierarchical structure, e.g., a.b=value
32-
This argument may be repeated to provide multiple arguments
33-
--pull Pull images before starting
34-
--volumes Remove volumes (WARNING: irreversible data loss)
35-
--network Remove network
36-
--kill Kill the containers (faster, but possible db corruption)
37-
--force Force stop even if containers are corrupted and cannot
38-
signal their running configuration, or if config cannot be
39-
parsed. Use with extra and/or option to force stop with
40-
configuration options.
19+
$ montagu start <path>
4120
```
4221

4322
Here `<path>` is the path to a directory that contains a configuration file `montagu.yml`.

config/acme/diagnostic-reports.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
testGroup:
2+
testDisease:
3+
- report_name: diagnostic
4+
assignee: a.hill
5+
success_email:
6+
recipients:
7+
8+
9+
subject: "VIMC diagnostic report: {touchstone} - {group} - {disease}"

config/acme/montagu.yml

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
## Prefix for container names; we'll use {container_prefix}-(container_name)
2+
container_prefix: montagu
3+
4+
## Set this flag to true to prevent use of --volumes in the cli to remove
5+
## volumes on stop
6+
protect_data: false
7+
8+
## Docker org for images
9+
repo: vimc
10+
11+
## The name of the docker network that containers will be attached to.
12+
## If you want to proxy Packit to the host, you will need to
13+
## arrange a proxy on this network
14+
network: montagu-network
15+
16+
# Domain where this instance of Montagu will be deployed. E.g. science.montagu.dide.ic.uk
17+
hostname: montagu.org
18+
19+
## Names of the docker volumes to use
20+
volumes:
21+
db: db_volume
22+
burden_estimates: burden_estimate_files
23+
emails: emails
24+
templates: template_volume
25+
guidance: guidance_volume
26+
mq: mq
27+
acme-challenge: acme-challenge
28+
certificates: certificates
29+
certbot: certbot
30+
31+
api:
32+
name: montagu-api
33+
tag: master
34+
admin:
35+
name: montagu-cli
36+
tag: master
37+
db:
38+
name: montagu-db
39+
tag: master
40+
root_user: vimc
41+
migrate:
42+
name: montagu-migrate
43+
tag: master
44+
users:
45+
api:
46+
password: "apipassword"
47+
permissions: all
48+
import:
49+
password: "importpassword"
50+
permissions: all
51+
orderly:
52+
password: "orderlypassword"
53+
permissions: all
54+
readonly:
55+
password: "readonlypassword"
56+
permissions: readonly
57+
protected_tables:
58+
- gavi_support_level
59+
- activity_type
60+
- burden_outcome
61+
- gender
62+
- responsibility_set_status
63+
- impact_outcome
64+
- gavi_support_level
65+
- support_type
66+
- touchstone_status
67+
- permission
68+
- role
69+
- role_permission
70+
proxy:
71+
name: montagu-reverse-proxy
72+
tag: master
73+
port_http: 80
74+
port_https: 443
75+
metrics:
76+
repo: nginx
77+
name: nginx-prometheus-exporter
78+
tag: 1.3.0
79+
acme:
80+
81+
additional_domains:
82+
- montagu-dev.org
83+
contrib:
84+
name: montagu-contrib-portal
85+
tag: master
86+
admin:
87+
name: montagu-admin-portal
88+
tag: master
89+
mq:
90+
repo: docker.io
91+
name: redis
92+
tag: latest
93+
port: 6379
94+
flower:
95+
repo: mher
96+
name: flower
97+
tag: 0.9.5
98+
port: 5555
99+
task_queue:
100+
name: task-queue-worker
101+
tag: master
102+
tasks:
103+
diagnostic_reports:
104+
use_additional_recipients: false
105+
poll_seconds: 5
106+
archive_folder_contents:
107+
min_file_age_seconds: 3600
108+
servers:
109+
youtrack:
110+
token: faketoken
111+
orderlyweb:
112+
url: http://orderly-web-web:8888
113+
montagu:
114+
115+
password: password
116+
smtp:
117+
118+
# If fake_smtp_server config is provided, the task_queue will use this as its smtp server
119+
# Note this will override other config provided in the task_queue section above
120+
fake_smtp_server:
121+
repo: reachfive
122+
name: fake-smtp-server
123+
tag: latest
124+
125+
orderly_web_api_url: https://localhost/reports/api/v2

config/basic/montagu.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ db:
6666
- role_permission
6767
proxy:
6868
name: montagu-reverse-proxy
69-
tag: vimc-7152
69+
tag: master
7070
port_http: 80
7171
port_https: 443
7272
metrics:

config/ci/montagu.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ db:
8383
- role_permission
8484
proxy:
8585
name: montagu-reverse-proxy
86-
tag: vimc-7152
86+
tag: master
8787
port_http: 80
8888
port_https: 443
8989
metrics:

config/complete/montagu.yml

-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ proxy:
9494
ssl:
9595
key: "k3y"
9696
certificate: "cert"
97-
dhparam: "param"
9897
contrib:
9998
name: montagu-contrib-portal
10099
tag: master

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ dependencies = [
4949
"pytest",
5050
"redis",
5151
"vault_dev",
52-
"YTClient"
52+
"YTClient",
53+
"cryptography"
5354
]
5455
[tool.hatch.envs.default.scripts]
5556
test = "pytest {args:tests}"

src/montagu_deploy/certbot.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# https://github.com/certbot/certbot/blob/v3.0.1/acme/examples/http01_example.py
2+
3+
import os.path
4+
import sys
5+
import tarfile
6+
from tempfile import TemporaryFile
7+
8+
import docker
9+
from constellation import docker_util
10+
11+
# The Docker API uses Go's FileMode values. These are different from the
12+
# standard values, as found in eg. stat.S_IFLNK.
13+
# https://pkg.go.dev/io/fs#FileMode
14+
DOCKER_MODE_TYPE = 0x8F280000
15+
DOCKER_MODE_SYMLINK = 0x8000000
16+
17+
18+
def read_file(container, path, *, follow_links=False):
19+
stream, status = container.get_archive(path)
20+
if follow_links and (status["mode"] & DOCKER_MODE_TYPE) == DOCKER_MODE_SYMLINK:
21+
return read_file(container, status["linkTarget"], follow_links=False)
22+
else:
23+
with TemporaryFile() as f:
24+
for d in stream:
25+
f.write(d)
26+
f.seek(0)
27+
28+
with tarfile.open(fileobj=f) as tar:
29+
return tar.extractfile(os.path.basename(path)).read()
30+
31+
32+
def obtain_certificate(cfg, extra_args):
33+
docker_util.ensure_volume(cfg.volumes["certbot"])
34+
docker_util.ensure_volume(cfg.volumes["acme-challenge"])
35+
36+
environment = {}
37+
command = [
38+
"certonly",
39+
"--non-interactive",
40+
"--agree-tos",
41+
"--webroot",
42+
"--webroot-path=/var/www",
43+
f"--email={cfg.acme_email}",
44+
f"--domain={cfg.hostname}",
45+
]
46+
47+
for d in cfg.acme_additional_domains:
48+
command.append(f"--domain={d}")
49+
50+
if cfg.acme_server:
51+
command.append(f"--server={cfg.acme_server}"),
52+
if cfg.acme_no_verify_ssl:
53+
command.append("--no-verify-ssl")
54+
environment["PYTHONWARNINGS"] = "ignore:Unverified HTTPS request"
55+
56+
command.extend(extra_args)
57+
58+
image = "certbot/certbot"
59+
container = docker.from_env().containers.run(
60+
image,
61+
command=command,
62+
detach=True,
63+
volumes={
64+
cfg.volumes["acme-challenge"]: {
65+
"bind": "/var/www/.well-known/acme-challenge",
66+
"mode": "rw",
67+
},
68+
cfg.volumes["certbot"]: {
69+
"bind": "/etc/letsencrypt",
70+
"mode": "rw",
71+
},
72+
},
73+
network=cfg.network,
74+
environment=environment,
75+
)
76+
77+
try:
78+
exit_status = container.wait()["StatusCode"]
79+
80+
sys.stderr.write(container.logs().decode("utf-8"))
81+
if exit_status != 0:
82+
raise docker.errors.ContainerError(container, exit_status, command, image, None)
83+
84+
cert = read_file(container, f"/etc/letsencrypt/live/{cfg.hostname}/fullchain.pem", follow_links=True)
85+
key = read_file(container, f"/etc/letsencrypt/live/{cfg.hostname}/privkey.pem", follow_links=True)
86+
87+
return (cert, key)
88+
89+
finally:
90+
container.remove()

src/montagu_deploy/cli.py

+21-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
montagu status <path>
55
montagu stop <path> [--volumes] [--network] [--kill] [--force]
66
[--extra=PATH] [--option=OPTION]...
7+
montagu renew-certificate <path> [--option=OPTION]... [--] [ARGS...]
78
89
Options:
910
--extra=PATH Path, relative to <path>, of yml file of additional
@@ -15,24 +16,21 @@
1516
--volumes Remove volumes (WARNING: irreversible data loss)
1617
--network Remove network
1718
--kill Kill the containers (faster, but possible db corruption)
18-
--force Force stop even if containers are corrupted and cannot
19-
signal their running configuration, or if config cannot be
20-
parsed. Use with extra and/or option to force stop with
21-
configuration options.
2219
"""
2320

2421
import docopt
2522
import yaml
2623

2724
import montagu_deploy.__about__ as about
25+
from montagu_deploy.certbot import obtain_certificate
2826
from montagu_deploy.config import MontaguConfig
29-
from montagu_deploy.montagu_constellation import montagu_constellation
27+
from montagu_deploy.montagu_constellation import montagu_constellation, proxy_update_certificate
3028

3129

3230
def main(argv=None):
3331
path, extra, options, args = parse_args(argv)
3432
if args.version:
35-
return about.__version__
33+
print(about.__version__)
3634
else:
3735
cfg = MontaguConfig(path, extra, options)
3836
obj = montagu_constellation(cfg)
@@ -42,7 +40,8 @@ def main(argv=None):
4240
montagu_status(obj)
4341
elif args.action == "stop":
4442
montagu_stop(obj, args, cfg)
45-
return True
43+
elif args.action == "renew-certificate":
44+
montagu_renew_certificate(obj, cfg, args.extra_args)
4645

4746

4847
def parse_args(argv=None):
@@ -61,6 +60,18 @@ def montagu_status(obj):
6160
obj.status()
6261

6362

63+
def montagu_renew_certificate(obj, cfg, extra_args):
64+
if cfg.ssl_mode != "acme":
65+
msg = "Proxy is not configured to use automatic certificates"
66+
raise Exception(msg)
67+
68+
print("Renewing certificates")
69+
(cert, key) = obtain_certificate(cfg, extra_args)
70+
71+
container = obj.containers.get("proxy", cfg.container_prefix)
72+
proxy_update_certificate(container, cert, key, reload=True)
73+
74+
6475
def montagu_stop(obj, args, cfg):
6576
if args.volumes:
6677
verify_data_loss(cfg)
@@ -123,9 +134,12 @@ def __init__(self, args):
123134
self.action = "status"
124135
elif args["stop"]:
125136
self.action = "stop"
137+
elif args["renew-certificate"]:
138+
self.action = "renew-certificate"
126139

127140
self.pull = args["--pull"]
128141
self.kill = args["--kill"]
129142
self.volumes = args["--volumes"]
130143
self.network = args["--network"]
131144
self.version = args["--version"]
145+
self.extra_args = args["ARGS"]

0 commit comments

Comments
 (0)