-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
295 lines (238 loc) · 9.71 KB
/
main.py
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
import json
import jinja2
import docker
import socket
import threading
import web
import logging
import itertools
import sys
import time
import getopt
from os import environ
from subprocess import call
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
c = docker.DockerClient(base_url='unix://var/run/docker.sock')
def get_current_domains():
'''Fetch current domains we have a cert for from renew-conf'''
path = '/etc/letsencrypt/renewal/{}.conf'.format(environ['DOMAIN_NAME'])
try:
with open(path) as f:
lines = f.readlines()
lines = list(itertools.dropwhile(lambda l: 'webroot_map' not in l,
lines))[1:]
return [line.split('=')[0].strip() for line in lines]
except IOError:
return []
def generate_certs_for_aliases(aliases, domain):
fqdns = {domain}
fqdns.update(set(['{}.{}'.format(a, domain) for a in aliases]))
fqdns.update(get_current_domains())
cmd = 'certbot certonly --cert-name {} --webroot --agree-tos --expand \
--email=admin@{} --non-interactive -w /var/www/letsencrypt ' \
.format(domain, domain)
cmd += ' '.join(['-d {}'.format(fqdn) for fqdn in fqdns])
# Run certbot
logging.info('Generating certs for: {}'.format(fqdns))
call(cmd, shell=True)
logging.info('Certificates generated')
def get_aliases(container, network):
network_settings = container.attrs['NetworkSettings']
return [alias for alias in
network_settings['Networks']
.get(network.name, {}).get('Aliases', [])
if not container.id.startswith(alias)]
def get_routes(container, network):
ports = [key.split('/')[0] for key, value in
container.attrs['NetworkSettings']['Ports'].items()
if value is None]
non_routed_aliases = ['registry']
aliases = [alias for alias in get_aliases(container, network)
if alias not in non_routed_aliases]
logging.info('Getting routes for: {}, aliases: {}, ports: {}'.format(
container, aliases, ports
))
if len(aliases) > 0:
return {
"alias": aliases[0],
"port": ports[0] if len(ports) else '8080',
"auth": container.labels.get("auth", None),
"oauth2_provider": container.labels.get("oauth2_provider", None),
}
def update_nginx_conf(network, use_certificates=True):
routes = [v for v in [get_routes(container, network)
for container in network.containers]
if v is not None]
template = jinja2.Template(open('nginx.tmpl').read(),
trim_blocks=True,
lstrip_blocks=True)
env = dict(environ)
nginx_conf = template.render(
env=env,
routes=routes,
use_certificates=use_certificates
)
with open('/etc/nginx/nginx.conf', 'w') as f:
f.write(nginx_conf)
logging.info('nginx.conf updated')
return [r['alias'] for r in routes]
def setup_network(network_name):
'''Setup a network, and add this container to it'''
container_id = socket.gethostname()
container = c.containers.get(container_id)
try:
network = c.networks.get(network_name)
except docker.errors.NotFound:
# Create network
network = c.networks.create(network_name)
if container not in network.containers:
network.connect(container, aliases=['daas'])
return network
def get_containers_with_alias(network, alias):
containers = []
for container in network.containers:
if alias in get_aliases(container, network):
containers.append(container)
return containers
def update_container(network_name, repo, tag, alias=None):
# Try to find an existing container
network = c.networks.get(network_name)
alias = alias or repo
image_name = '{}:{}'.format(repo, tag)
logging.info("Updating: {}".format(image_name))
old_containers = get_containers_with_alias(network, alias)
logging.info("Found old containers: {}".format(old_containers))
env = []
if old_containers:
env = old_containers[0].attrs['Config']['Env']
old_img = old_containers[0].image
new_img = c.images.get(image_name)
if old_img.id == new_img.id:
logging.info("Already running this image")
# Same image, just let the old one run
return
try:
volumes = list(
(c.images.get(image_name).attrs['Config']['Volumes'] or {}).keys()
)
volume_config = [
'{}-{}-{}-{}:{}'.format(environ.get('DOMAIN_NAME', 'daas'), alias,
tag, vol.replace('/', '_'), vol)
for vol in volumes
]
new_container = c.containers.create('{}:{}'.format(repo, tag),
environment=env,
volumes=volume_config)
except docker.errors.NotFound as e:
logging.error(e)
# Just abort for now
return
logging.info("Starting new container: {}".format(new_container))
network.connect(new_container, aliases=[alias])
new_container.start()
if old_containers:
for container in old_containers:
logging.info("Killing old container: {}".format(container))
container.stop()
container.remove()
def update_environment(network_name, alias, env):
# Find container running with alias, and create a copy with new environment
containers = get_containers_with_alias(network_name, alias)
container = containers[0]
new = c.create_container(container['Image'], environment=env)
c.connect_container_to_network(new, network_name, aliases=[alias])
c.start(new)
for old in containers:
c.stop(old)
c.remove_container(old)
def setup_registry(network):
'''Setup a registry in network'''
c.images.build(fileobj=open('registry.dockerfile', mode='rb'),
tag='registry:notifs')
update_container(network.name, 'registry', 'notifs')
logging.info("Registry running")
class EventHandler(object):
def POST(self):
d = json.loads(web.data())
for e in d['events']:
if e['action'] == 'push' and 'tag' in e['target']:
repo_name, tag = e['target']['repository'], e['target']['tag']
repo = '{}/{}'.format(environ['DOMAIN_NAME'], repo_name)
try:
c.images.pull(repo, tag=tag)
update_container(self.network_name, repo, tag,
alias=repo_name)
except docker.errors.APIError:
# This might be the registry being down, us trying to pull
# a plugin or something similar, just error out
pass
class ConfigHandler(object):
def GET(self, alias):
def _get_info(cont):
aliases = get_aliases(cont, c.networks.get(self.network_name))
return {
'alias': aliases[0] if len(aliases) else "-MISSING-",
'env': cont.attrs['Config']['Env'],
'state': cont.status,
}
container_info = [_get_info(cont) for cont in c.containers.list()]
return json.dumps(container_info)
def PUT(self, alias):
d = json.loads(web.data())
env = [e for e in d['env'] if e != '']
update_environment(self.network_name, alias, env)
class IndexHandler(object):
def GET(self):
return web.template.render('').index()
app = web.application((
'/', 'IndexHandler',
'/events', 'EventHandler',
'/config(?:/(?P<alias>[^/]*))?/?', 'ConfigHandler'),
locals())
def runwebapp():
web.httpserver.runsimple(app.wsgifunc(), ('0.0.0.0', 8080))
def start_event_listener(network):
# Ugly way to pass network_name to web-handler
EventHandler.network_name = network.name
ConfigHandler.network_name = network.name
thread = threading.Thread(target=runwebapp)
thread.daemon = True
thread.start()
logging.info("HTTP started")
def watch(network):
start_event_listener(network)
setup_registry(network)
# First we generate this conf without certificates, since we might not
# have the certificates on disk yet, and otherwise nginx will fail
aliases = update_nginx_conf(network, use_certificates=False)
call('nginx', shell=True)
if 'DOMAIN_NAME' in environ:
generate_certs_for_aliases(aliases, environ.get('DOMAIN_NAME'))
aliases = update_nginx_conf(network)
call('nginx -s reload', shell=True)
# Sleep so that we're sure nginx is up again before trying login
time.sleep(1.0)
c.login(environ.get('USERNAME'), environ.get('PASSWORD'),
registry=environ.get('DOMAIN_NAME'))
for ev in c.events(decode=True, filters={'network': network_name}):
logging.info("Got event: {}".format(ev))
# Make sure network has updated data
network.reload()
aliases = update_nginx_conf(network)
call('nginx -s reload', shell=True)
if 'DOMAIN_NAME' in environ:
generate_certs_for_aliases(aliases, environ.get('DOMAIN_NAME'))
call('nginx -s reload', shell=True)
call('nginx -s stop', shell=True)
if __name__ == '__main__':
network_name = environ.get('NETWORK_NAME', 'daas')
network = setup_network(network_name)
logging.info("Network fixed")
opts, args = getopt.getopt(sys.argv[1:], '', ['renew', 'watch'])
for opt, _ in opts:
if opt == '--watch':
watch(network)
elif opt == '--renew' and 'DOMAIN_NAME' in environ:
aliases = update_nginx_conf(network)
generate_certs_for_aliases(aliases, environ.get('DOMAIN_NAME'))