Skip to content

Commit f71a980

Browse files
committed
Initial commit
0 parents  commit f71a980

File tree

13 files changed

+463
-0
lines changed

13 files changed

+463
-0
lines changed

.devcontainer/devcontainer.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/python
3+
{
4+
"name": "Python 3",
5+
"image": "mcr.microsoft.com/devcontainers/python:3.10",
6+
7+
// Configure tool-specific properties.
8+
"customizations": {
9+
// Configure properties specific to VS Code.
10+
"vscode": {
11+
// Set *default* container specific settings.json values on container create.
12+
"settings": {
13+
"python.defaultInterpreterPath": "/usr/local/bin/python",
14+
"python.linting.enabled": true,
15+
"python.linting.pylintEnabled": true,
16+
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
17+
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
18+
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
19+
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
20+
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
21+
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
22+
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
23+
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
24+
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
25+
},
26+
27+
// Add the IDs of extensions you want installed when the container is created.
28+
"extensions": [
29+
"ms-python.python",
30+
"ms-python.vscode-pylance"
31+
]
32+
}
33+
},
34+
35+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
36+
// "forwardPorts": [],
37+
38+
// Use 'postCreateCommand' to run commands after the container is created.
39+
"postCreateCommand": "pip3 install --user -r requirements.txt",
40+
41+
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
42+
"remoteUser": "vscode",
43+
"features": {
44+
"git": "latest"
45+
}
46+
}

.github/workflows/main.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: ci
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- 'app/**'
8+
branches:
9+
- 'main'
10+
11+
jobs:
12+
docker:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v3
17+
- name: Set up QEMU
18+
uses: docker/setup-qemu-action@v2
19+
with:
20+
platforms: 'arm64'
21+
- name: Set up Docker Buildx
22+
uses: docker/setup-buildx-action@v2
23+
- name: Login to Docker Hub
24+
uses: docker/login-action@v2
25+
with:
26+
username: ${{ secrets.DOCKERHUB_USERNAME }}
27+
password: ${{ secrets.DOCKERHUB_TOKEN }}
28+
- name: Build and push
29+
uses: docker/build-push-action@v3
30+
with:
31+
context: .
32+
platforms: "linux/amd64,linux/arm64"
33+
push: true
34+
tags: maxanderson95/digitalocean-dyndns-updater:latest, maxanderson95/digitalocean-dyndns-updater:v1.${{github.run_number}}

.gitignore

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
settings.toml
2+
.secrets.toml
3+
4+
# Byte-compiled / optimized / DLL files
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
9+
# C extensions
10+
*.so
11+
12+
# Distribution / packaging
13+
.Python
14+
build/
15+
develop-eggs/
16+
dist/
17+
downloads/
18+
eggs/
19+
.eggs/
20+
lib/
21+
lib64/
22+
parts/
23+
sdist/
24+
var/
25+
wheels/
26+
pip-wheel-metadata/
27+
share/python-wheels/
28+
*.egg-info/
29+
.installed.cfg
30+
*.egg
31+
MANIFEST
32+
33+
# PyInstaller
34+
# Usually these files are written by a python script from a template
35+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
36+
*.manifest
37+
*.spec
38+
39+
# Installer logs
40+
pip-log.txt
41+
pip-delete-this-directory.txt
42+
43+
# Unit test / coverage reports
44+
htmlcov/
45+
.tox/
46+
.nox/
47+
.coverage
48+
.coverage.*
49+
.cache
50+
nosetests.xml
51+
coverage.xml
52+
*.cover
53+
*.py,cover
54+
.hypothesis/
55+
.pytest_cache/
56+
57+
# Translations
58+
*.mo
59+
*.pot
60+
61+
# Django stuff:
62+
*.log
63+
local_settings.py
64+
db.sqlite3
65+
db.sqlite3-journal
66+
67+
# Flask stuff:
68+
instance/
69+
.webassets-cache
70+
71+
# Scrapy stuff:
72+
.scrapy
73+
74+
# Sphinx documentation
75+
docs/_build/
76+
77+
# PyBuilder
78+
target/
79+
80+
# Jupyter Notebook
81+
.ipynb_checkpoints
82+
83+
# IPython
84+
profile_default/
85+
ipython_config.py
86+
87+
# pyenv
88+
.python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
98+
__pypackages__/
99+
100+
# Celery stuff
101+
celerybeat-schedule
102+
celerybeat.pid
103+
104+
# SageMath parsed files
105+
*.sage.py
106+
107+
# Environments
108+
.env
109+
.venv
110+
env/
111+
venv/
112+
ENV/
113+
env.bak/
114+
venv.bak/
115+
116+
# Spyder project settings
117+
.spyderproject
118+
.spyproject
119+
120+
# Rope project settings
121+
.ropeproject
122+
123+
# mkdocs documentation
124+
/site
125+
126+
# mypy
127+
.mypy_cache/
128+
.dmypy.json
129+
dmypy.json
130+
131+
# Pyre type checker
132+
.pyre/
133+
134+
# Ignore dynaconf secret files
135+
.secrets.*

.vscode/launch.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Python: Current File",
9+
"type": "python",
10+
"request": "launch",
11+
"program": "main.py",
12+
"cwd": "${workspaceFolder}/app",
13+
"console": "integratedTerminal",
14+
"justMyCode": true,
15+
"env": {
16+
"PYTHONDONTWRITEBYTECODE": "1",
17+
"DIGITALOCEAN_TOKEN": "",
18+
"RECORD_NAME": "example4",
19+
"ZONE_NAME": "maxanderson.tech",
20+
"LOG_LEVEL": "DEBUG"
21+
}
22+
}
23+
]
24+
}

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"python.linting.enabled": false,
3+
"vs-kubernetes": {
4+
"disable-linters": ["resource-limits"],
5+
},
6+
}

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.11-alpine
2+
COPY ./requirements.txt /app/requirements.txt
3+
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
4+
COPY ./app /app
5+
WORKDIR /app
6+
CMD ["python", "main.py"]

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# DigitalOcean Dynamic DNS Updater
2+
This is a Python script that determines your public egress IP address, then calls to the DigitalOcean API to either create or update a record with this IP.
3+
4+
This script is designed to run, perform the action, and then shutdown. It doesn't run as a daemon. The idea is to run this script (or ideally container image) on a system that allows for scheduling (such as a Kubernetes cron job).
5+
6+
## Configuration
7+
To configure the script, set the following environment variables prior to execution:
8+
9+
|Name|Description|Required|Default|Example|
10+
|---|---|---|---|---|
11+
|DIGITALOCEAN_TOKEN|Must have read and write access|*true*||*N/A*|
12+
|RECORD_NAME|The name of the DNS record to update|*true*||server01|
13+
|ZONE_NAME|The name of the zone where the record should be created or updated|*true*||example.com|
14+
|TTL|The time to live of the DNS record|*false*|3600|60|
15+
|LOG_LEVEL|The log level of the script|*false*|INFO|DEBUG|
16+
17+
## Docker Image
18+
A docker image which runs this script on startup has been published to the [DockerHub](https://hub.docker.com/repository/docker/maxanderson95/digitalocean-dyndns-updater).
19+
20+
## Run as a Kubernetes Cron Job
21+
First create a secret that stores the DigitalOcean token
22+
```yaml
23+
apiVersion: v1
24+
kind: Secret
25+
metadata:
26+
name: digitalocean-token
27+
type: Opaque
28+
data:
29+
token: <Base64 Encoded Token>
30+
```
31+
Then create a CronJob that runs the container image (for example) once per hour.
32+
```yaml
33+
apiVersion: batch/v1
34+
kind: CronJob
35+
metadata:
36+
name: dyndns
37+
spec:
38+
schedule: "0 * * * *"
39+
jobTemplate:
40+
spec:
41+
template:
42+
spec:
43+
containers:
44+
- name: dyndns-updater
45+
image: maxanderson95/digitalocean-dyndns-updater:latest
46+
imagePullPolicy: IfNotPresent
47+
env:
48+
- name: DIGITALOCEAN_TOKEN
49+
valueFrom:
50+
secretKeyRef:
51+
name: digitalocean-token
52+
key: token
53+
- name: RECORD_NAME
54+
value: "server01"
55+
- name: ZONE_NAME
56+
value: "example.com"
57+
restartPolicy: OnFailure
58+
```

app/config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import logging
2+
from dynaconf import Dynaconf, Validator
3+
4+
settings = Dynaconf(
5+
envvar_prefix=False,
6+
validators=[
7+
Validator("DIGITALOCEAN_TOKEN", must_exist=True),
8+
Validator("RECORD_NAME", must_exist=True),
9+
Validator("ZONE_NAME", must_exist=True),
10+
Validator("TTL", default=3600, must_exist=True,
11+
is_type_of=int, gte=30),
12+
Validator("LOG_LEVEL", default=logging.INFO)
13+
]
14+
)
15+
16+
settings.validators.validate_all()

app/ip.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import requests
2+
import random
3+
from logger import configure_logging
4+
5+
logger = configure_logging(__name__)
6+
7+
urls = [
8+
"https://ifconfig.io",
9+
"https://ipconfig.io",
10+
"https://ifconfig.co"
11+
]
12+
13+
headers = {
14+
'User-Agent': 'curl'
15+
}
16+
17+
18+
def get_public_ip() -> str:
19+
# Randomize the url list so that it doesn't use the same service every time
20+
random.shuffle(urls)
21+
last_item = urls[-1]
22+
23+
for url in urls:
24+
try:
25+
logger.debug(f"Sending request for public IP to {url}")
26+
response = requests.get(url, headers=headers)
27+
if response.status_code == 200:
28+
ip = response.text.strip()
29+
logger.info(f"Public IP address discovered: {ip}")
30+
return ip
31+
except requests.exceptions.RequestException as e:
32+
logger.debug(f"Failed to retreive IP from url {url}")
33+
if url != last_item:
34+
logger.debug("Trying next available IP lookup service.")
35+
continue
36+
else:
37+
logger.exception(
38+
f"Failed to retreive public IP from any of the available lookup services")
39+
raise Exception(
40+
f"Failed to retreive public IP from any of the available lookup services") from e

app/logger.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import logging
2+
import time
3+
from config import settings
4+
5+
6+
def configure_logging(name: str) -> logging.Logger:
7+
logger = logging.getLogger(name)
8+
handler = logging.StreamHandler()
9+
formatter = logging.Formatter(
10+
'[%(asctime)s][%(name)-10s][%(levelname)-7s] %(message)s')
11+
handler.setFormatter(formatter)
12+
logging.Formatter.converter = time.gmtime
13+
logger.addHandler(handler)
14+
logger.setLevel(settings.LOG_LEVEL)
15+
16+
return logger

0 commit comments

Comments
 (0)