burrow/Scripts/cloudflare-upsert-a-record.sh

165 lines
4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: Scripts/cloudflare-upsert-a-record.sh --zone <zone> --name <fqdn> --ipv4 <address> [options]
Upsert a DNS-only or proxied Cloudflare A record without putting the API token on
the process list.
Options:
--zone <zone> Cloudflare zone name, for example burrow.net
--name <fqdn> Fully-qualified DNS record name
--ipv4 <address> IPv4 address for the A record
--token-file <path> Cloudflare API token file
default: intake/cloudflare-token.txt
--ttl <seconds|auto> Record TTL, or auto
default: auto
--proxied <true|false> Whether to proxy through Cloudflare
default: false
-h, --help Show this help
EOF
}
ZONE_NAME=""
RECORD_NAME=""
IPV4=""
TOKEN_FILE="intake/cloudflare-token.txt"
TTL_VALUE="auto"
PROXIED="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--zone)
ZONE_NAME="${2:?missing value for --zone}"
shift 2
;;
--name)
RECORD_NAME="${2:?missing value for --name}"
shift 2
;;
--ipv4)
IPV4="${2:?missing value for --ipv4}"
shift 2
;;
--token-file)
TOKEN_FILE="${2:?missing value for --token-file}"
shift 2
;;
--ttl)
TTL_VALUE="${2:?missing value for --ttl}"
shift 2
;;
--proxied)
PROXIED="${2:?missing value for --proxied}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "${ZONE_NAME}" || -z "${RECORD_NAME}" || -z "${IPV4}" ]]; then
usage >&2
exit 2
fi
if [[ ! -f "${TOKEN_FILE}" ]]; then
echo "Cloudflare token file not found: ${TOKEN_FILE}" >&2
exit 1
fi
if [[ ! "${IPV4}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "Invalid IPv4 address: ${IPV4}" >&2
exit 1
fi
case "${PROXIED}" in
true|false)
;;
*)
echo "--proxied must be true or false" >&2
exit 1
;;
esac
case "${TTL_VALUE}" in
auto)
TTL_JSON=1
;;
''|*[!0-9]*)
echo "--ttl must be a number of seconds or auto" >&2
exit 1
;;
*)
TTL_JSON="${TTL_VALUE}"
;;
esac
TOKEN="$(tr -d '\r\n' < "${TOKEN_FILE}")"
if [[ -z "${TOKEN}" ]]; then
echo "Cloudflare token file is empty: ${TOKEN_FILE}" >&2
exit 1
fi
cf_api() {
local method="$1"
local path="$2"
local body="${3-}"
if [[ -n "${body}" ]]; then
curl -fsS -X "${method}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data "${body}" \
"https://api.cloudflare.com/client/v4${path}"
else
curl -fsS -X "${method}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4${path}"
fi
}
zone_lookup="$(cf_api GET "/zones?name=${ZONE_NAME}&status=active")"
zone_id="$(jq -r '.result[0].id // empty' <<<"${zone_lookup}")"
if [[ -z "${zone_id}" ]]; then
echo "Active Cloudflare zone not found: ${ZONE_NAME}" >&2
exit 1
fi
payload="$(jq -cn \
--arg type "A" \
--arg name "${RECORD_NAME}" \
--arg content "${IPV4}" \
--argjson proxied "${PROXIED}" \
--argjson ttl "${TTL_JSON}" \
'{type: $type, name: $name, content: $content, proxied: $proxied, ttl: $ttl}')"
record_lookup="$(cf_api GET "/zones/${zone_id}/dns_records?type=A&name=${RECORD_NAME}")"
record_id="$(jq -r '.result[0].id // empty' <<<"${record_lookup}")"
if [[ -n "${record_id}" ]]; then
result="$(cf_api PUT "/zones/${zone_id}/dns_records/${record_id}" "${payload}")"
action="updated"
else
result="$(cf_api POST "/zones/${zone_id}/dns_records" "${payload}")"
action="created"
fi
jq -r --arg action "${action}" '
if .success != true then
.errors | tostring | halt_error(1)
else
"Cloudflare DNS " + $action + ": " + .result.name + " -> " + .result.content +
" (proxied=" + (.result.proxied | tostring) + ", ttl=" + (.result.ttl | tostring) + ")"
end
' <<<"${result}"