feat: add Cloudflare DNS tool suite with multi-instance support
- zone-list, record-list, record-create, record-update, record-delete - Named instance support (-a flag) with configurable default - Zone name-to-ID auto-resolution in shared _lib.sh - Updated credentials loader with cloudflare/cloudflare-<name> services - TOOLS.md and INFRASTRUCTURE.md guide documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
23
TOOLS.md
23
TOOLS.md
@@ -72,6 +72,27 @@ Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection a
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b <branch>
|
||||
```
|
||||
|
||||
### DNS — Cloudflare
|
||||
|
||||
Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal`, `work`). Omit `-a` to use the default from `cloudflare.default` in credentials.json.
|
||||
|
||||
```bash
|
||||
# List zones (domains)
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records (zone by name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-a instance] [-t type] [-n name]
|
||||
|
||||
# Create DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl]
|
||||
|
||||
# Delete DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
```
|
||||
|
||||
### IT Service — GLPI
|
||||
|
||||
```bash
|
||||
@@ -100,7 +121,7 @@ Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection a
|
||||
# Source in any script to load service credentials
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh
|
||||
load_credentials <service-name>
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
```
|
||||
|
||||
## Git Providers
|
||||
|
||||
@@ -153,6 +153,75 @@ The human is escalation-only for missing access, hard policy conflicts, or irrev
|
||||
- Magic variables (`SERVICE_FQDN_*`) require list-style env syntax, not dict-style
|
||||
- Rate limit: 200 requests per interval
|
||||
|
||||
### Cloudflare DNS Operations
|
||||
|
||||
Use the Cloudflare tools for any DNS configuration: pointing domains at services, adding TXT verification records, managing MX records, etc.
|
||||
|
||||
**Multi-instance support**: Credentials support named instances (e.g. `personal`, `work`). A `default` key in credentials.json determines which instance is used when `-a` is omitted. Pass `-a <instance>` to target a specific account.
|
||||
|
||||
```bash
|
||||
# List all zones (domains) in the account
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records for a zone (accepts zone name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-t type] [-n name]
|
||||
|
||||
# Create a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update a DNS record (requires record ID from record-list)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-p]
|
||||
|
||||
# Delete a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id>
|
||||
```
|
||||
|
||||
**Flag reference:**
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|---------|
|
||||
| `-z` | Zone name (e.g. `mosaicstack.dev`) or 32-char zone ID |
|
||||
| `-a` | Named Cloudflare instance (omit for default) |
|
||||
| `-t` | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `SRV`, etc. |
|
||||
| `-n` | Record name: short (`app`) or FQDN (`app.example.com`) |
|
||||
| `-c` | Record content/value (IP, hostname, TXT string, etc.) |
|
||||
| `-r` | Record ID (from `record-list.sh` output) |
|
||||
| `-p` | Enable Cloudflare proxy (orange cloud) — omit for DNS-only (grey cloud) |
|
||||
| `-l` | TTL in seconds (default: `1` = auto) |
|
||||
| `-P` | Priority for MX/SRV records |
|
||||
| `-f` | Output format: `table` (default) or `json` |
|
||||
|
||||
**Common workflows:**
|
||||
|
||||
```bash
|
||||
# Point a new subdomain at a server (proxied through Cloudflare)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t A -n myapp -c 203.0.113.10 -p
|
||||
|
||||
# Add a TXT record for domain verification (never proxied)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t TXT -n _verify -c "verification=abc123"
|
||||
|
||||
# Check what records exist before making changes
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z example.com -t CNAME
|
||||
|
||||
# Update an existing record (get record ID from record-list first)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh \
|
||||
-z example.com -r <record-id> -t A -n myapp -c 10.0.0.5 -p
|
||||
```
|
||||
|
||||
**DNS + Deployment integration**: When deploying a new service via Coolify or Portainer that needs a public domain, the typical sequence is:
|
||||
|
||||
1. Create the DNS record pointing at the host IP (with `-p` for Cloudflare proxy if desired)
|
||||
2. Deploy the service via Coolify/Portainer
|
||||
3. Verify the domain resolves and the service is reachable
|
||||
|
||||
**Proxy (`-p`) guidance:**
|
||||
|
||||
- Use proxy (orange cloud) for web services — provides CDN, DDoS protection, and hides origin IP
|
||||
- Skip proxy (grey cloud) for non-HTTP services (mail, SSH), wildcard records, or when the service handles its own TLS termination and needs direct client IP visibility
|
||||
- Proxy is NOT compatible with non-standard ports outside Cloudflare's supported range
|
||||
|
||||
### Stack Health Check
|
||||
|
||||
Verify all infrastructure services are reachable:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# Supported services:
|
||||
# portainer, coolify, authentik, glpi, github,
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
#
|
||||
# After loading, service-specific env vars are exported.
|
||||
# Run `load_credentials --help` for details.
|
||||
@@ -49,6 +49,8 @@ Services and exported variables:
|
||||
gitea-mosaicstack → GITEA_URL, GITEA_TOKEN
|
||||
gitea-usc → GITEA_URL, GITEA_TOKEN
|
||||
woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN
|
||||
cloudflare → CLOUDFLARE_API_TOKEN (uses default instance)
|
||||
cloudflare-<name> → CLOUDFLARE_API_TOKEN (specific instance, e.g. cloudflare-personal)
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
@@ -110,9 +112,25 @@ EOF
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
;;
|
||||
cloudflare-*)
|
||||
local cf_instance="${service#cloudflare-}"
|
||||
export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-$(_mosaic_read_cred ".cloudflare.${cf_instance}.api_token")}"
|
||||
export CLOUDFLARE_INSTANCE="$cf_instance"
|
||||
[[ -n "$CLOUDFLARE_API_TOKEN" ]] || { echo "Error: cloudflare.${cf_instance}.api_token not found" >&2; return 1; }
|
||||
;;
|
||||
cloudflare)
|
||||
# Resolve default instance, then load it
|
||||
local cf_default
|
||||
cf_default="${CLOUDFLARE_INSTANCE:-$(_mosaic_read_cred '.cloudflare.default')}"
|
||||
if [[ -z "$cf_default" ]]; then
|
||||
echo "Error: cloudflare.default not set and no CLOUDFLARE_INSTANCE env var" >&2
|
||||
return 1
|
||||
fi
|
||||
load_credentials "cloudflare-${cf_default}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown service '$service'" >&2
|
||||
echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker" >&2
|
||||
echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare, cloudflare-<name>" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
67
tools/cloudflare/_lib.sh
Executable file
67
tools/cloudflare/_lib.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# _lib.sh — Shared helpers for Cloudflare tool scripts
|
||||
#
|
||||
# Usage: source "$(dirname "$0")/_lib.sh"
|
||||
#
|
||||
# Provides:
|
||||
# CF_API — Base API URL
|
||||
# cf_auth — Authorization header value
|
||||
# cf_load_instance <instance> — Load credentials for a specific or default instance
|
||||
# cf_resolve_zone <name_or_id> — Resolves a zone name to its ID (passes IDs through)
|
||||
|
||||
CF_API="https://api.cloudflare.com/client/v4"
|
||||
|
||||
cf_auth() {
|
||||
echo "Bearer $CLOUDFLARE_API_TOKEN"
|
||||
}
|
||||
|
||||
# Load credentials for a Cloudflare instance.
|
||||
# If instance is empty, loads the default.
|
||||
cf_load_instance() {
|
||||
local instance="$1"
|
||||
if [[ -n "$instance" ]]; then
|
||||
load_credentials "cloudflare-${instance}"
|
||||
else
|
||||
load_credentials cloudflare
|
||||
fi
|
||||
}
|
||||
|
||||
# Resolve a zone name (e.g. "mosaicstack.dev") to its zone ID.
|
||||
# If the input is already a 32-char hex ID, passes it through.
|
||||
cf_resolve_zone() {
|
||||
local input="$1"
|
||||
|
||||
# If it looks like a zone ID (32 hex chars), pass through
|
||||
if [[ "$input" =~ ^[0-9a-f]{32}$ ]]; then
|
||||
echo "$input"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Resolve by name
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?name=${input}&status=active")
|
||||
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to resolve zone '$input' (HTTP $http_code)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local zone_id
|
||||
zone_id=$(echo "$body" | jq -r '.result[0].id // empty')
|
||||
|
||||
if [[ -z "$zone_id" ]]; then
|
||||
echo "Error: Zone '$input' not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$zone_id"
|
||||
}
|
||||
86
tools/cloudflare/record-create.sh
Executable file
86
tools/cloudflare/record-create.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-create.sh — Create a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name, e.g. "app" or "app.example.com" (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority (default: 10)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build JSON payload
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
# Add priority for MX/SRV records
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to create record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$body" | jq -r '.result.id')
|
||||
echo "Created $TYPE record: $NAME → $CONTENT (ID: $record_id)"
|
||||
55
tools/cloudflare/record-delete.sh
Executable file
55
tools/cloudflare/record-delete.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-delete.sh — Delete a DNS record from a Cloudflare zone
|
||||
#
|
||||
# Usage: record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
|
||||
while getopts "z:a:r:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" ]]; then
|
||||
echo "Error: -z and -r are both required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X DELETE \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to delete record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deleted DNS record $RECORD_ID from zone $ZONE"
|
||||
81
tools/cloudflare/record-list.sh
Executable file
81
tools/cloudflare/record-list.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-list.sh — List DNS records for a Cloudflare zone
|
||||
#
|
||||
# Usage: record-list.sh -z <zone> [-a instance] [-t type] [-n name] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -t type Filter by record type (A, AAAA, CNAME, MX, TXT, etc.)
|
||||
# -n name Filter by record name
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "z:a:t:n:f:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> [-a instance] [-t type] [-n name] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" ]]; then
|
||||
echo "Error: -z zone is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build query params
|
||||
params="per_page=100"
|
||||
[[ -n "$TYPE" ]] && params="${params}&type=${TYPE}"
|
||||
[[ -n "$NAME" ]] && params="${params}&name=${NAME}"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records?${params}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list records (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "RECORD ID TYPE NAME CONTENT PROXIED TTL"
|
||||
echo "-------------------------------- ----- -------------------------------------- ------------------------------- ------- -----"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.type,
|
||||
.name,
|
||||
.content,
|
||||
(if .proxied then "yes" else "no" end),
|
||||
(if .ttl == 1 then "auto" else (.ttl | tostring) end)
|
||||
] | @tsv' | while IFS=$'\t' read -r id type name content proxied ttl; do
|
||||
printf "%-32s %-5s %-38s %-31s %-7s %s\n" "$id" "$type" "${name:0:38}" "${content:0:31}" "$proxied" "$ttl"
|
||||
done
|
||||
86
tools/cloudflare/record-update.sh
Executable file
86
tools/cloudflare/record-update.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-update.sh — Update a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:r:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -r, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to update record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updated $TYPE record: $NAME → $CONTENT (ID: $RECORD_ID)"
|
||||
59
tools/cloudflare/zone-list.sh
Executable file
59
tools/cloudflare/zone-list.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# zone-list.sh — List Cloudflare zones (domains)
|
||||
#
|
||||
# Usage: zone-list.sh [-a instance] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
INSTANCE=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "a:f:h" opt; do
|
||||
case $opt in
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-a instance] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?per_page=50")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list zones (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ZONE ID NAME STATUS PLAN"
|
||||
echo "-------------------------------- ---------------------------- -------- ----------"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.name,
|
||||
.status,
|
||||
.plan.name
|
||||
] | @tsv' | while IFS=$'\t' read -r id name status plan; do
|
||||
printf "%-32s %-28s %-8s %s\n" "$id" "$name" "$status" "$plan"
|
||||
done
|
||||
Reference in New Issue
Block a user