├── .drone.yml ├── .swarm.yml ├── Dockerfile ├── README.md ├── docker-compose.yml ├── main.py └── requirements.txt /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: default 5 | 6 | clone: 7 | depth: 1 8 | 9 | steps: 10 | - name: deploy 11 | image: automagistre/docker:stable 12 | volumes: 13 | - name: docker.sock 14 | path: /var/run/docker.sock 15 | commands: 16 | - docker stack deploy --prune --with-registry-auth --compose-file .swarm.yml dhu 17 | when: 18 | branch: [ master ] 19 | 20 | volumes: 21 | - name: docker.sock 22 | host: 23 | path: /var/run/docker.sock 24 | -------------------------------------------------------------------------------- /.swarm.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | redirect: 5 | image: schmunk42/nginx-redirect:0.6.0 6 | environment: 7 | SERVER_REDIRECT: github.com 8 | SERVER_REDIRECT_PATH: /grachevko/docker-hosts-updater 9 | SERVER_REDIRECT_SCHEME: https 10 | networks: 11 | - traefik 12 | deploy: 13 | mode: replicated 14 | replicas: 1 15 | update_config: 16 | parallelism: 1 17 | order: start-first 18 | restart_policy: 19 | condition: on-failure 20 | labels: 21 | - "traefik.enable=true" 22 | - "traefik.http.routers.whoami.rule=Host(`dhu.grachevko.ru`)" 23 | - "traefik.http.routers.whoami.entrypoints=websecure" 24 | - "traefik.http.routers.whoami.tls=true" 25 | - "traefik.http.routers.whoami.tls.certresolver=leresolver" 26 | - "traefik.http.services.whoami-service.loadbalancer.server.port=80" 27 | - "traefik.http.services.whoami-service.loadbalancer.server.scheme=http" 28 | 29 | networks: 30 | traefik: 31 | external: true 32 | name: traefik 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.0-alpine3.10 2 | 3 | WORKDIR /usr/local/app 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | COPY *.py ./ 10 | 11 | CMD ["python", "main.py"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docker-hosts-updater 2 | ---------- 3 | Automatic update `/etc/hosts` on start/stop containers by labels. 4 | 5 | Requirements 6 | ----- 7 | * **Native linux** 8 | _This tool has no effect on macOS or windows, because docker on these OS run in 9 | VM and you can't directly access from host to each container via ip. 10 | Yet you can pass traffic through loadbalancer, see section above._ 11 | 12 | Usage 13 | ----- 14 | Start up `docker-hosts-updater`: 15 | 16 | ```bash 17 | $ docker run -d --restart=always \ 18 | --name docker-hosts-updater \ 19 | -v /var/run/docker.sock:/var/run/docker.sock \ 20 | -v /etc/hosts:/opt/hosts \ 21 | grachevko/docker-hosts-updater 22 | ``` 23 | 24 | Start containers with label `ru.grachevko.dhu` option 25 | 26 | % docker run -d --label ru.grachevko.dhu=nginx.local nginx 27 | 28 | Try to ping from host 29 | 30 | % ping nginx.local 31 | 32 | Default hosts 33 | ----- 34 | By default adding records with container name and container hostname. 35 | To disable it you can use environments `CONTAINER_HOSTNAME_DISABLED` and `CONTAINER_NAME_DISABLED`: 36 | ```bash 37 | $ docker run -d --restart=always \ 38 | --name docker-hosts-updater \ 39 | -v /var/run/docker.sock:/var/run/docker.sock \ 40 | -v /etc/hosts:/opt/hosts \ 41 | -e CONTAINER_HOSTNAME_DISABLED=false \ 42 | -e CONTAINER_NAME_DISABLED=false \ 43 | grachevko/docker-hosts-updater 44 | ``` 45 | 46 | Multiple Hosts 47 | ----- 48 | You can add multiple hosts, just separate them by semicolon: 49 | 50 | ```bash 51 | $ docker run --label ru.grachevko.dhu="nginx.local;nginx.ru" nginx 52 | $ ping nginx.local 53 | $ ping nginx.ru 54 | ``` 55 | 56 | Subdomains 57 | ----- 58 | Add subdomains by using pattern `{www,api}.nginx.local`: 59 | 60 | ```bash 61 | $ docker run -d --label ru.grachevko.dhu="{www,api}.nginx.local" nginx 62 | $ ping nginx.local 63 | $ ping www.nginx.local 64 | $ ping api.nginx.local 65 | ``` 66 | 67 | Priority 68 | ---- 69 | If you want to run two containers with same hosts and want one override another, 70 | just add priority after colon: 71 | 72 | ```bash 73 | $ docker run -d --label ru.grachevko.dhu="nginx.local" nginx 74 | $ docker run -d --label ru.grachevko.dhu="nginx.local:10" nginx 75 | $ ping nginx.local 76 | ``` 77 | Container with greater priority will be used. Default priority 0. 78 | If priority is the same then early created container will be used. 79 | 80 | Load Balancer 81 | ---- 82 | In order to pass the traffic through the loadbalancer you should define container's name or valid IPv4. 83 | Just add one more colon and container name after it. 84 | ```bash 85 | $ docker run -d --name lb nginx 86 | $ docker run -d --label ru.grachevko.dhu="nginx1.local:0:lb" nginx 87 | $ docker run -d --label ru.grachevko.dhu="nginx2.local:0:127.0.0.1" nginx 88 | $ ping nginx1.local // ip of lb 89 | $ ping nginx2.local // ip of lb 90 | ``` 91 | Keep in mind, loadbalancer container must have fixed name. 92 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | dhu: 5 | build: 6 | context: . 7 | image: grachev/dhu 8 | # environment: 9 | # CONTAINER_HOSTNAME_DISABLED: 'false' 10 | # CONTAINER_NAME_DISABLED: 'false' 11 | volumes: 12 | - /var/run/docker.sock:/var/run/docker.sock 13 | - ./:/usr/local/app 14 | - /etc/hosts:/opt/hosts 15 | 16 | lb: 17 | image: nginx:alpine 18 | container_name: lb 19 | 20 | nginx: 21 | image: nginx:alpine 22 | labels: 23 | ru.grachevko.dhu: '{www,api,img,admin,profile}.nginx.local' 24 | 25 | nginx2: 26 | image: nginx:alpine 27 | labels: 28 | ru.grachevko.dhu: '{www,admin,api}.nginx.local:2' 29 | 30 | nginx3: 31 | image: nginx:alpine 32 | labels: 33 | ru.grachevko.dhu: 'img.nginx.local:3;api.nginx.local:5' 34 | 35 | nginx4: 36 | image: nginx:alpine 37 | labels: 38 | ru.grachevko.dhu: 'nginx4.local:0:lb' 39 | 40 | nginx5: 41 | image: nginx:alpine 42 | labels: 43 | ru.grachevko.dhu: 'nginx5.local:0:lb' 44 | 45 | nginx6: 46 | image: nginx:alpine 47 | labels: 48 | ru.grachevko.dhu: 'nginx6.local:0:127.0.0.1' 49 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import docker 2 | import re 3 | import os 4 | from netaddr import valid_ipv4 5 | 6 | LABEL = 'ru.grachevko.dhu' 7 | MARKER = '#### DOCKER HOSTS UPDATER ####' 8 | HOSTS_PATH = '/opt/hosts' 9 | CONTAINER_HOSTNAME_DISABLED = bool(os.getenv('CONTAINER_HOSTNAME_DISABLED', False)) 10 | CONTAINER_NAME_DISABLED = bool(os.getenv('CONTAINER_NAME_DISABLED', False)) 11 | 12 | 13 | def listen(): 14 | for event in docker.events(decode=True): 15 | if 'container' == event.get('Type') and event.get('Action') in ["start", "stop", "die"]: 16 | handle() 17 | 18 | 19 | def scan(): 20 | containers = [] 21 | for container in docker.containers.list(): 22 | label = container.attrs.get('Config').get('Labels').get(LABEL) 23 | if not label: 24 | continue 25 | 26 | for string in label.split(';'): 27 | priority = 0 28 | lb = container 29 | ip = False 30 | 31 | if ':' in string: 32 | parts = string.split(':') 33 | string = parts[0] 34 | priority = int(parts[1]) if len(parts) >= 2 else priority 35 | 36 | if len(parts) == 3: 37 | lbString = parts[2] 38 | 39 | if valid_ipv4(lbString): 40 | ip = lbString 41 | else: 42 | lb = docker.containers.get(lbString) 43 | 44 | if ip == False: 45 | ip = next(iter(lb.attrs.get('NetworkSettings').get('Networks').values())).get('IPAddress') 46 | 47 | hosts = string_to_array(string) 48 | if not CONTAINER_HOSTNAME_DISABLED: 49 | hosts.append(container.attrs.get('Config').get('Hostname')) 50 | if not CONTAINER_NAME_DISABLED: 51 | hosts.append(container.name) 52 | 53 | if ip: 54 | containers.append({ 55 | 'ip': ip, 56 | 'priority': priority, 57 | 'hosts': hosts, 58 | 'createdAt': container.attrs.get('Created'), 59 | }) 60 | 61 | return containers 62 | 63 | 64 | def string_to_array(input_string): 65 | dd = [(rec.group().replace("{", "").replace("}", "").split(","), rec.span()) for rec in 66 | re.finditer("{[^}]*}", input_string)] 67 | 68 | texts = [] 69 | if len(dd) != 0: 70 | for i in range(len(dd)): 71 | if i == 0: 72 | if dd[0][1][0] == 0: 73 | texts.append("") 74 | else: 75 | texts.append(input_string[0:dd[0][1][0]]) 76 | else: 77 | texts.append(input_string[dd[i - 1][1][1]:dd[i][1][0]]) 78 | if i == len(dd) - 1: 79 | texts.append(input_string[dd[-1][1][1]:]) 80 | else: 81 | texts = [input_string] 82 | 83 | if len(dd) > 0: 84 | idxs = [0] * len(dd) 85 | summary = [] 86 | 87 | while idxs[0] != len(dd[0][0]): 88 | summary_string = "" 89 | for i in range(len(idxs)): 90 | summary_string += texts[i] + dd[i][0][idxs[i]] 91 | summary_string += texts[-1] 92 | summary.append(summary_string) 93 | for j in range(len(idxs) - 1, -1, -1): 94 | if j == len(idxs) - 1: 95 | idxs[j] += 1 96 | if j > 0 and idxs[j] == len(dd[j][0]): 97 | idxs[j] = 0 98 | idxs[j - 1] += 1 99 | else: 100 | summary = texts 101 | 102 | return summary 103 | 104 | 105 | def update(items): 106 | f = open(HOSTS_PATH, 'r+') 107 | lines = [] 108 | skip_lines = False 109 | for line in f.read().split('\n'): 110 | if line == MARKER: 111 | skip_lines = not skip_lines 112 | continue 113 | 114 | if not skip_lines: 115 | lines.append(line) 116 | 117 | if items: 118 | lines.append(MARKER) 119 | for ip, value in items.items(): 120 | line = '{} {}'.format(ip, ' '.join(value)) 121 | lines.append(line) 122 | print(line) 123 | lines.append(MARKER) 124 | 125 | summary = '\n'.join(lines) 126 | 127 | f.seek(0) 128 | f.truncate() 129 | f.write(summary) 130 | f.close() 131 | 132 | 133 | def handle(): 134 | print('Recompiling...') 135 | items = scan() 136 | 137 | map_dict = {} 138 | for item in items: 139 | for host in item.get('hosts'): 140 | if host in map_dict: 141 | priority_left = map_dict[host].get('priority') 142 | priority_right = item.get('priority') 143 | 144 | if priority_left > priority_right: 145 | continue 146 | 147 | if priority_left == priority_right and map_dict[host].get('createdAt') < item.get('createdAt'): 148 | continue 149 | 150 | map_dict[host] = item 151 | 152 | summary = {} 153 | for item in items: 154 | ip = item.get('ip') 155 | for host in item.get('hosts'): 156 | if map_dict[host].get('ip') == ip: 157 | if ip not in summary: 158 | summary[ip] = [] 159 | summary[ip].append(host) 160 | 161 | update(summary) 162 | 163 | 164 | docker = docker.from_env() 165 | handle() 166 | listen() 167 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docker>=4.0.2 2 | netaddr 3 | --------------------------------------------------------------------------------