Skip to content

Commit df65483

Browse files
committed
Initial Commit
1 parent cc7bd64 commit df65483

6 files changed

+211
-1
lines changed

Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:alpine as base
2+
3+
FROM base as build
4+
COPY requirements.txt .
5+
RUN pip install --prefix=/install -r requirements.txt
6+
7+
FROM base
8+
COPY --from=build /install /usr/local
9+
ADD *.py ./
10+
ENTRYPOINT ["python3"]
11+
CMD ["-u","app.py"]

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 David Chidell
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,65 @@
11
# traefik-cloudflare-updater
2-
Updates CloudFlare with traefik configuration
2+
3+
Instead of using a wildcard DNS record, create records dynamically using this.
4+
5+
Only works with traefikv2 configuration, look at the above repo for v1 support.
6+
7+
When you run the container, it'll update all the DNS records it can find, afterward it will sit and wait for any new traefik enabled containers to start and then updtae them. If this container restarts, it'll re-update everything.
8+
9+
Idea taken from the following GitHub repo: https://github.com/tiredofit/docker-traefik-cloudflare-companion
10+
11+
12+
## Why write this, and not use the above repo?
13+
14+
I didn't like the following things (just my preference):
15+
* No updating of existing records
16+
* No exclusions
17+
* Python 2
18+
* Stuff not needed for the operation of this service within the container
19+
* Bash and python mix
20+
21+
What I did like:
22+
* The general idea, wildcard domains are not nice
23+
* The fact it's at least in python
24+
* The elegant usage of docker events to update new containers
25+
26+
## Usage
27+
Example `docker-compose.yml` service:
28+
29+
```yml
30+
dnsupdater:
31+
image: dchidell/traefik-cloudflare-updater
32+
restart: unless-stopped
33+
volumes:
34+
- /var/run/docker.sock:/var/run/docker.sock:ro
35+
environment:
36+
37+
- CF_TOKEN=1234567890
38+
- TARGET_DOMAIN=example.com
39+
- DOMAIN1=mydomain1.com
40+
- DOMAIN1_ZONE_ID=1234567890
41+
- DOMAIN2=mydomain2.com
42+
- DOMAIN2_ZONE_ID=1234567890
43+
- EXCLUDED_DOMAINS=static.mydomain1.com,test.mydomain2.com
44+
```
45+
46+
47+
### Mandatory ENV vars:
48+
49+
`TARGET_DOMAIN` - a CNAME will be created pointing to this target
50+
51+
`CF_EMAIL` - CloudFlare API Email
52+
53+
`CF_TOKEN` - CloudFlare API Token
54+
55+
`DOMAIN#` - Multiple of these per domain, e.g. `DOMAIN1=example.com`, `DOMAIN2=example.net` ... `DOMAINn=example.org`
56+
57+
`DOMAIN#_ZONE_ID` - CloudFlare zone ID for domain index.
58+
59+
60+
61+
### Optional ENV vars:
62+
63+
`DOMAIN#_PROXIED` - Whether to use CloudFlare proxy. Should be 'TRUE' or 'FALSE' (not 1 or 0) (defaults to TRUE)
64+
65+
`EXCLUDED_DOMAINS` - Comma separated domains to be excluded from updating (i.e. if you want to statically define something) e.g. `EXCLUDED_DOMAINS=sub.domain.com,sub2.domain.com`

TraefikUpdater.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
import re
3+
import time
4+
import docker
5+
import CloudFlare
6+
7+
__author__ = "David Chidell"
8+
9+
class TraefikUpdater:
10+
def __init__(self):
11+
self.target_domain = os.environ['TARGET_DOMAIN']
12+
excluded = os.environ.get('EXCLUDED_DOMAINS')
13+
if excluded is None:
14+
self.excluded_domains = []
15+
else:
16+
self.excluded_domains = excluded.split(',')
17+
18+
self.tld_info = {}
19+
self.get_domain_vars()
20+
21+
self.host_pattern = re.compile('\`([a-zA-Z0-9\.]+)\`')
22+
23+
self.cf = CloudFlare.CloudFlare(email=os.environ['CF_EMAIL'] , token=os.environ['CF_TOKEN'])
24+
self.dkr = docker.from_env()
25+
26+
def enter_update_loop(self):
27+
print(f'Listening for new containers...')
28+
t = int(time.time())
29+
for event in self.dkr.events(since=t, filters={'status': 'start'}, decode=True):
30+
if event.get('status') == 'start':
31+
try:
32+
container = self.dkr.containers.get(event.get('id'))
33+
except docker.errors.NotFound as e:
34+
pass
35+
else:
36+
if container.labels.get("traefik.enable",'False').upper() == "TRUE":
37+
print(f'New container online: {container.name}, processing...')
38+
self.process_container(container)
39+
40+
def process_containers(self):
41+
containers = self.dkr.containers.list(filters={"status":"running","label":"traefik.enable=true"})
42+
print(f'Found {len(containers)} existing containers to process')
43+
for container in containers:
44+
self.process_container(container)
45+
print(f'Finished bulk updating containers!')
46+
47+
def process_container(self,container):
48+
for label, value in container.labels.items():
49+
if 'rule' in label and 'Host' in value:
50+
domains = self.host_pattern.findall(value)
51+
print(f'Found domains: {domains} for container: {container.name}')
52+
for domain in domains:
53+
self.update_domain(domain)
54+
55+
def update_domain(self,domain):
56+
dom_split = domain.split('.')
57+
if len(dom_split) >= 3:
58+
tld = '.'.join(dom_split[1:])
59+
else:
60+
tld = domain
61+
62+
if self.tld_info.get(tld) is None:
63+
print(f'TLD {tld} not in updatable list')
64+
return False
65+
66+
dom_info = self.tld_info[tld]
67+
common_dict = {i: dom_info[i] for i in ('type', 'content', 'proxied')}
68+
post_dict = {**{'name':domain},**common_dict}
69+
try:
70+
get_records = self.cf.zones.dns_records.get(dom_info['zone'], params={'name':domain})
71+
if len(get_records) == 0:
72+
post_record = self.cf.zones.dns_records.post(dom_info['zone'], data=post_dict)
73+
print(f'New record created: {domain}')
74+
else:
75+
for record in get_records:
76+
post_record = self.cf.zones.dns_records.put(dom_info['zone'], record['id'], data=post_dict)
77+
print(f'Existing record updated: {domain}')
78+
79+
except CloudFlare.exceptions.CloudFlareAPIError as e:
80+
print(f'API call failed: {str(e)}')
81+
return False
82+
return True
83+
84+
def get_domain_vars(self):
85+
tld_count = 0
86+
self.tld_info = {}
87+
while True:
88+
tld_count += 1
89+
try:
90+
domain = os.environ[f'DOMAIN{tld_count}']
91+
zone = os.environ[f'DOMAIN{tld_count}_ZONE_ID']
92+
try:
93+
proxied = os.environ.get(f'DOMAIN{tld_count}_PROXIED',"TRUE").upper() == 'TRUE'
94+
except KeyError:
95+
proxied = false
96+
self.tld_info[domain] = {"zone":zone, "proxied":proxied, "type":"CNAME", "content":self.target_domain}
97+
except KeyError:
98+
break
99+
print(f'Found {tld_count-1} TLDs! {self.tld_info}')

app.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/python
2+
3+
import os
4+
from TraefikUpdater import TraefikUpdater
5+
6+
def main():
7+
updater = TraefikUpdater()
8+
updater.process_containers()
9+
10+
# This blocks
11+
updater.enter_update_loop()
12+
13+
if __name__ == "__main__":
14+
main()

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docker
2+
cloudflare

0 commit comments

Comments
 (0)