/
update-dns.sh
executable file
·170 lines (144 loc) · 5.39 KB
/
update-dns.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/bin/bash
usage() {
echo 'Usage ./update-dns.sh [SERVICE] [RECORD] [TTL]'
echo
echo 'Updates DNS records on Cloudflare.'
echo
echo 'Required environment variables:'
echo 'CF_ROOT_DOMAIN domain associated with Cloudflare zone'
echo 'CF_API_KEY API key generated from Cloudflare "My Account" page'
echo 'CF_AUTH_EMAIL email address associated with your Cloudflare user account'
echo 'CONSUL hostname or IP of Consul server (will use address of linked consul if available)'
echo
echo 'Required parameters (or environment variables):'
echo 'SERVICE name of service to query from Consul'
echo 'RECORD DNS record name to update (ex. mycompany.example.com)'
echo 'TTL DNS TTL of the record (in seconds)'
}
missingParam() {
echo "Missing required parameter."
}
writeLog() {
echo $(date -u "+%Y-%m-%dT%H:%M:%SZ") $@
}
CF_API=https://api.cloudflare.com/client/v4
SERVICE=${1-${SERVICE:-}}
RECORD=${2:-${RECORD:-}}
TTL=${3:-${TTL:-}}
CONSUL=${CONSUL:-${CONSUL_PORT_8500_TCP_ADDR:-}} # allows links to work
: ${CF_ROOT_DOMAIN?"$(missingParam)$(usage)"}
: ${CF_API_KEY?"$(missingParam)$(usage)"}
: ${CF_AUTH_EMAIL?"$(missingParam)$(usage)"}
: ${SERVICE?"$(missingParam)$(usage)"}
: ${RECORD?"$(missingParam)$(usage)"}
: ${TTL?"$(missingParam)$(usage)"}
: ${CONSUL?"$(missingParam)$(usage)"}
# get all the healthy nodes for our service and assign to an array for our A-records
getFromConsul() {
CURRENT=( $(curl -s ${CONSUL}:8500/v1/health/service/${SERVICE}?passing | jq -r '[.[].Service.Address]|sort|.[]') )
: ${CURRENT?"No Consul records found."}
}
# https://api.cloudflare.com/#zone-list-zones
getZone() {
ZONE_ID=$(curl --fail -sX GET "${CF_API}/zones/?name=${CF_ROOT_DOMAIN}" \
-H "X-Auth-Key:${CF_API_KEY}" \
-H "X-Auth-Email:${CF_AUTH_EMAIL}" \
-H "Content-Type: application/json" | jq -r .result[0].id)
: ${ZONE_ID?"No zone found."}
writeLog "DNS zone ID:" ${ZONE_ID}
}
# https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
getRecords() {
RECORDS=$(curl -sX GET "${CF_API}/zones/${ZONE_ID}/dns_records?type=A&name=${RECORD}&page=1&per_page=20&order=type&direction=desc&match=all" \
-H "X-Auth-Key:${CF_API_KEY}" \
-H "X-Auth-Email:${CF_AUTH_EMAIL}" \
-H "Content-Type: application/json")
: ${RECORDS?"No records found."}
writeLog "DNS record IDs:" $(echo ${RECORDS} | jq -r '.result[].id')
}
compareRecords() {
# we need the ID of old records in order to delete them but bash doesn't
# support multi-dimensional arrays so we'll just use two w/ the same indexes
OLD=( $(echo $RECORDS | jq -r '[.result[].content]|sort|.[]') )
OLD_IDS=( $(echo $RECORDS | jq -r '[.result[].id]|sort|.[]') )
writeLog old=${OLD[*]}
writeLog current=${CURRENT[*]}
# if we only have one record and have none to remove, we just want
# to update it
if [[ ${#CURRENT[*]} == 1 ]]; then
if [[ ${#OLD[*]} == 1 ]]; then
updateRecord ${OLD_IDS[0]} ${CURRENT}
return 0
fi
fi
# add new records before removing the old ones so that we can do a
# rolling deploy
for new in ${CURRENT[*]}
do
if ! contains OLD $new; then
addRecord $new
fi
done
# remove any stale records (exists in old but not in new)
for ((i=0;i < ${#OLD[*]};i++)) {
local old=${OLD[i]}
if ! contains CURRENT $old; then
deleteRecord ${OLD_IDS[i]} $old
fi
}
}
# utility to check if array contains a string value
contains() {
local array="$1[@]"
local search=$2
local found=1
for element in "${!array}"; do
if [[ $element == $search ]]; then
found=0
break
fi
done
return $found
}
# https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
updateRecord() {
local id=$1
local value=$2
writeLog "updateRecord:" ${id}, ${value}
curl -sX PUT "${CF_API}/zones/${ZONE_ID}/dns_records/${id}" \
-H "X-Auth-Key:${CF_API_KEY}" \
-H "X-Auth-Email:${CF_AUTH_EMAIL}" \
-H "Content-Type: application/json" \
--data "$(printf '{"id":"%s","type":"A","name":"%s","content":"%s","ttl":%s}' ${id} $RECORD $value $TTL)"
}
# https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
addRecord(){
local value=$1
writeLog "addRecord:" ${value}
curl -sX POST "${CF_API}/zones/${ZONE_ID}/dns_records" \
-H "X-Auth-Key:${CF_API_KEY}" \
-H "X-Auth-Email:${CF_AUTH_EMAIL}" \
-H "Content-Type: application/json" \
--data "$(printf '{"type":"A","name":"%s","content":"%s","ttl":%s}' $REC_ID $RECORD $value $TTL)"
}
# https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
deleteRecord() {
local id=$1
local value=$2
writeLog "deleteRecord:" ${id} ${value}
curl -sX DELETE "${CF_API}/zones/${ZONE_ID}/dns_records/${id}" \
-H "X-Auth-Key:${CF_API_KEY}" \
-H "X-Auth-Email:${CF_AUTH_EMAIL}" \
-H "Content-Type: application/json"
}
run() {
getFromConsul
getZone
getRecords
compareRecords
}
# `. update-dns.sh --source` will import all functions without executing
# the `run` function, enabling standalone testing of each function
if [ "$1" != "--source" ]; then
run "${@}"
fi