mirror of
https://github.com/rubenhensen/k8scd.git
synced 2026-09-17 02:12:55 +02:00
Add programmatic DNS with Transip API
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: dns-sync
|
||||||
|
spec:
|
||||||
|
schedule: "*/5 * * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
successfulJobsHistoryLimit: 1
|
||||||
|
failedJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: dns-sync
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: sync
|
||||||
|
image: python:3.12-alpine
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
pip install --quiet PyJWT cryptography requests pyyaml &&
|
||||||
|
python /config/sync.py
|
||||||
|
env:
|
||||||
|
- name: TRANSIP_ACCOUNT_NAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: transip-credentials
|
||||||
|
key: account_name
|
||||||
|
- name: TRANSIP_PRIVATE_KEY_PATH
|
||||||
|
value: /secrets/private_key
|
||||||
|
volumeMounts:
|
||||||
|
- name: secrets
|
||||||
|
mountPath: /secrets
|
||||||
|
readOnly: true
|
||||||
|
- name: script
|
||||||
|
mountPath: /config/sync.py
|
||||||
|
subPath: sync.py
|
||||||
|
readOnly: true
|
||||||
|
- name: domains
|
||||||
|
mountPath: /config/domains
|
||||||
|
readOnly: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "200m"
|
||||||
|
volumes:
|
||||||
|
- name: secrets
|
||||||
|
secret:
|
||||||
|
secretName: transip-credentials
|
||||||
|
items:
|
||||||
|
- key: private_key
|
||||||
|
path: private_key
|
||||||
|
- name: script
|
||||||
|
configMap:
|
||||||
|
name: dns-sync-script
|
||||||
|
- name: domains
|
||||||
|
configMap:
|
||||||
|
name: dns-domains
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
records:
|
||||||
|
- name: "@"
|
||||||
|
expire: 86400
|
||||||
|
type: A
|
||||||
|
content: "62.41.87.114"
|
||||||
|
- name: mail
|
||||||
|
expire: 86400
|
||||||
|
type: A
|
||||||
|
content: "46.224.26.65"
|
||||||
|
- name: "phocaslustrum"
|
||||||
|
expire: 86400
|
||||||
|
type: CNAME
|
||||||
|
content: "@"
|
||||||
|
- name: "ynab"
|
||||||
|
expire: 86400
|
||||||
|
type: CNAME
|
||||||
|
content: "@"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: transip-credentials
|
||||||
|
spec:
|
||||||
|
secretStoreRef:
|
||||||
|
name: vault-backend
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
refreshInterval: 15m
|
||||||
|
target:
|
||||||
|
name: transip-credentials
|
||||||
|
creationPolicy: Owner
|
||||||
|
data:
|
||||||
|
- secretKey: private_key
|
||||||
|
remoteRef:
|
||||||
|
key: kv/transip
|
||||||
|
property: private_key
|
||||||
|
- secretKey: account_name
|
||||||
|
remoteRef:
|
||||||
|
key: kv/transip
|
||||||
|
property: account_name
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: dns
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- external-secret.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- cronjob.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: dns-sync-script
|
||||||
|
files:
|
||||||
|
- sync.py
|
||||||
|
- name: dns-domains
|
||||||
|
files:
|
||||||
|
- domains/rubenhensen.nl.yaml
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: dns-sync
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync DNS records from YAML files to TransIP via their REST API."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from glob import glob
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
TRANSIP_API = "https://api.transip.nl/v6"
|
||||||
|
DOMAINS_DIR = "/config/domains"
|
||||||
|
|
||||||
|
|
||||||
|
def get_access_token(account_name: str, private_key: str) -> str:
|
||||||
|
"""Authenticate with TransIP API and return a bearer token."""
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {
|
||||||
|
"iss": account_name,
|
||||||
|
"sub": account_name,
|
||||||
|
"aud": "api.transip.nl",
|
||||||
|
"jti": str(uuid.uuid4()),
|
||||||
|
"iat": now,
|
||||||
|
"nbf": now,
|
||||||
|
"exp": now + 300,
|
||||||
|
"global_key": True,
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, private_key, algorithm="RS512")
|
||||||
|
resp = requests.post(
|
||||||
|
f"{TRANSIP_API}/auth",
|
||||||
|
json={"login": account_name, "nonce": payload["jti"], "global_key": True},
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()["token"]
|
||||||
|
|
||||||
|
|
||||||
|
def sync_domain(domain: str, records: list, token: str) -> None:
|
||||||
|
"""Replace all DNS entries for a domain."""
|
||||||
|
entries = []
|
||||||
|
for r in records:
|
||||||
|
entries.append({
|
||||||
|
"name": r["name"],
|
||||||
|
"expire": r["expire"],
|
||||||
|
"type": r["type"],
|
||||||
|
"content": r["content"],
|
||||||
|
})
|
||||||
|
|
||||||
|
resp = requests.put(
|
||||||
|
f"{TRANSIP_API}/domains/{domain}/dns",
|
||||||
|
json={"dnsEntries": entries},
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
print(f"Synced {len(entries)} records for {domain}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
account_name = os.environ.get("TRANSIP_ACCOUNT_NAME")
|
||||||
|
private_key_path = os.environ.get("TRANSIP_PRIVATE_KEY_PATH", "/secrets/private_key")
|
||||||
|
|
||||||
|
if not account_name:
|
||||||
|
print("ERROR: TRANSIP_ACCOUNT_NAME not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
private_key = Path(private_key_path).read_text().strip()
|
||||||
|
|
||||||
|
print("Authenticating with TransIP API...")
|
||||||
|
token = get_access_token(account_name, private_key)
|
||||||
|
|
||||||
|
domain_files = sorted(glob(f"{DOMAINS_DIR}/*.yaml"))
|
||||||
|
if not domain_files:
|
||||||
|
print("No domain files found")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
errors = 0
|
||||||
|
for filepath in domain_files:
|
||||||
|
domain = Path(filepath).stem
|
||||||
|
print(f"Processing {domain}...")
|
||||||
|
try:
|
||||||
|
with open(filepath) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
sync_domain(domain, data["records"], token)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR syncing {domain}: {e}")
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"Completed with {errors} error(s)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("All domains synced successfully")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user