ranflare
Version:
Rancher + Cloudflare External DNS
181 lines (149 loc) • 6.17 kB
JavaScript
const axios = require("axios");
const program = require("caporal");
const winston = require("winston");
const CloudFlare = require("cloudflare");
const package = require("./package.json");
/**
* Updates cloudflare data
*/
async function update(args, options, logger, cloudflare) {
logger.info(`Updating ${options.cloudflareZone.name} (${options.cloudflareUser.email}) zone`);
const version = "2016-07-29";
const response = await axios.request({
url: `/${version}`,
baseURL: options.rancherMetadataUrl,
headers: {
"Accept": "application/json",
}
});
const data = response.data;
const balancers = data.services.filter(service => service.kind === "loadBalancerService" && service.state === "active" && !!service.lb_config);
if (balancers.length <= 0) {
logger.error("No load balancers running.");
return;
}
const hosts = data.hosts.filter(host => {
for (let b in balancers) {
for (let c in balancers[b].containers) {
if (balancers[b].containers[c].state !== 'running') {
continue;
}
if (host.uuid === balancers[b].containers[c].host_uuid) {
return true;
}
}
}
return false;
});
if (hosts.length <= 0) {
logger.error("No available hosts running the load balancer.");
return;
}
const addresses = Array.from(new Set(hosts.map(host => host.agent_ip)));
const entries = balancers
.map(balancer => balancer.lb_config)
.reduce((prev, next) => prev.concat(next.port_rules), [])
.map(config => config.hostname);
// List all registered entries
let pages = 1;
let allRecords = [];
for (let i = 0; i < pages; i++) {
const page = await cloudflare.dnsRecords.browse(options.cloudflareZone.id, { page: i + 1 });
allRecords = allRecords.concat(page.result);
pages = page.result_info.total_pages;
}
// Removes entries if they point to invalid (outdated/removed) host addresses
const existingRecords = allRecords.filter(r => r.type === "A" && entries.includes(r.name));
for (const record of existingRecords) {
if (!addresses.includes(record.content)) {
logger.info(`Removing "${record.content}" that points to unknown address "${record.content}"`);
try {
await cloudflare.dnsRecords.del(options.cloudflareZone.id, record.id);
} catch (err) {
logger.error(`Failed to remove dns record "${record.name}" (${record.id})`);
}
}
}
for (const entry of entries) {
for (const address of addresses) {
const existingAddresses = allRecords.filter(r => r.type === "A" && r.name === entry).map(record => record.content);
if (!existingAddresses.includes(address)) {
logger.info(`Adding DNS record "${entry}" pointing to "${address}"`);
try {
await cloudflare.dnsRecords.add(options.cloudflareZone.id, {
type: "A",
name: entry,
content: address,
proxied: !options.cloudflareNoproxy,
});
} catch (err) {
logger.error(`Failed to add dns record ${entry} -> ${address}`);
logger.error(err);
}
}
}
}
}
/**
* Bootstrap code
*/
async function bootstrap() {
winston.configure({
transports: [
new winston.transports.Console({
colorize: true,
prettyPrint: true,
})
],
})
program
.version(package.version)
.logger(winston)
.command("run", "Runs the agent, monitoring docker and updating cloudflare entries.")
.option("--cloudflare.email <email>", "CloudFlare Email", program.STRING)
.option("--cloudflare.key <email>", "CloudFlare API Key", program.STRING)
.option("--cloudflare.zone <zone>", "CloudFlare Zone ID", program.STRING)
.option("--cloudflare.noproxy", "CloudFlare Proxy Usage", program.BOOLEAN, false)
.option("--rancher.metadata.url <url>", "Rancher Metadata URL", program.STRING, "http://rancher-metadata")
.option("--poll.interval <interval>", "Poll interval", program.NUMBER, 300000)
.action(async (args, options, logger) => {
if (!options.cloudflareEmail || !options.cloudflareKey || !options.cloudflareZone) {
throw new Error("Missing cloudflare email and/or key.")
}
const cloudflare = CloudFlare({
email: options.cloudflareEmail,
key: options.cloudflareKey,
});
try {
const response = await cloudflare.user.read();
options.cloudflareUser = response.result;
} catch (err) {
if (err.statusCode === 403) {
throw new Error("Invalid cloudflare's email and/or key");
} else {
throw new Error("Failed to retrieve cloudflare's user information");
}
}
try {
const response = await cloudflare.zones.read(options.cloudflareZone);
options.cloudflareZone = response.result;
} catch (err) {
if (err.statusCode === 403) {
throw new Error("Invalid cloudflare's zone id");
} else {
throw new Error("Failed to retrieve cloudflare's zone information");
}
}
while (true) {
try {
await update(args, options, logger, cloudflare);
} catch (err) {
logger.error(err);
}
await new Promise(resolve => setTimeout(resolve, options.pollInterval));
}
});
program.parse(process.argv);
}
bootstrap();