mirror of
https://github.com/rubenhensen/k8scd.git
synced 2026-09-16 18:02: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