diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2b7be8e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,21 @@ +name: Publish to NPM +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies and build 🔧 + run: npm ci && npm run build + - name: Publish package on NPM 📦 + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index f9849f3..f4b3646 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,105 @@ ### Cloudflare Domains Manager -Inside the `configuration.yaml` file there is the mapping of every DNS record used in this repo.
+This tool is helpful to manage cloudflare domains using a versionate file.
-For use the script just run under the domains folder: +## How to use? + +Create a folder for your new project: ``` -npm install -npm run update -``` -Note:
-This script works only with domains managed by cloudflare.
-You have to create a `.env` file with Cloudflare credentials inside `domains` folder.
-In the configuration, `ttl=1` is the automatic ttl provided by cloudflare.
-In the configuration, `proxied=true` means the record will be proxed by cloudflare system.
-In the configuration, `deleted=true` means the record will be delete from cloudflare. Use it only when you need to delete a dns record. You can remove the dns from the yaml after the deletion.
-Please use load balancers mapping instead of explicit value in DNS records.
-If you have created a new domain in cloudflare you can run this command to get the mandatory zone_id for the configuration file: +mkdir test-domains +cd test-domains +``` + +Create a new node app: +``` +npm init +``` + +Install this package: +``` +npm i cloudflare-domains-manager +``` + +Create a index.js with this content: +``` +const CloudflareDomainsManager = require ('cloudflare-domains-manager') + +const cdm = new CloudflareDomainsManager(); +cdm.run(); +``` + +Create a .env file with your clouflare data: +``` +CF_EMAIL=(your cloudflare email) +CF_KEY= (your cloudflare token) ``` -node index.js action=list +you can generate your cloudflare token here: https://dash.cloudflare.com/profile/api-tokens + + +Edit the package.json adding these lines: +``` + "main": "index.js", + "scripts": { + "update": "node index.js action=update", + "list": "node index.js action=list" + } +``` + +install the dependencies: +``` +npm install ``` -if you want to update a single domain instead of all, just add it as a parameter: + +you can now get a list of your domains with: ``` -npm run update uala.it +npm run list ``` -Dry run mode is supported, just add it as a parameter: + +## The configuration file + +if it works, you can now create your `configuration.yaml` file starting with something like: ``` -npm run update uala.it --dry-run +load_balancers: + - example-lb: &example-lb example-lb.your-infrastructure.com + - example-lb-ip: &example-lb-ip 172.0.0.10 +domains: + - name: example.com + zone_id: Y0UR_Z0N3_1D + dns_records: + - name: example.com + type: CNAME + content: *example-lb + ttl: 1 + proxied: true + - name: test.example.com + type: A + content: *example-lb-ip + ttl: 1 + proxied: true + - name: www.example.com + type: CNAME + content: *example-lb + ttl: 1 + proxied: true + - name: tobedeleted.example.com + type: CNAME + deleted: true + content: *example-lb + ttl: 1 + proxied: true ``` + +In the configuration: +- `ttl=1` is the automatic ttl provided by cloudflare.
+- `proxied=true` means the record will be proxed by cloudflare system.
+- `deleted=true` means the record will be delete from cloudflare. Use it only when you need to delete a dns record. You can remove the dns from the yaml after the deletion.
+ +Please use load balancers mapping instead of explicit value in DNS records.
+ + +## Commands list + +- `npm run list`: Get the list of all your domains in cloudflare +- `npm run update`: Update all your cloudflare domains with the content of `configuration.yaml`. Domains that don't exist in the file will be ignored +- `npm run update example.com`: Update only the domain `example.com` +- `npm run update example.com --dry-run`: Update the domain `example.com` in dry-run mode (nothing will change in cloudflare) diff --git a/configuration.yaml.example b/configuration.yaml.example new file mode 100644 index 0000000..879c29a --- /dev/null +++ b/configuration.yaml.example @@ -0,0 +1,28 @@ +load_balancers: + - example-lb: &example-lb example-lb.your-infrastructure.com + - example-lb-ip: &example-lb-ip 172.0.0.10 +domains: + - name: example.com + zone_id: Y0UR_Z0N3_1D + dns_records: + - name: example.com + type: CNAME + content: *example-lb + ttl: 1 + proxied: true + - name: test.example.com + type: A + content: *example-lb-ip + ttl: 1 + proxied: true + - name: www.example.com + type: CNAME + content: *example-lb + ttl: 1 + proxied: true + - name: tobedeleted.example.com + type: CNAME + deleted: true + content: *example-lb + ttl: 1 + proxied: true diff --git a/package.json b/package.json index 624b0e2..0b85d91 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,7 @@ "description": "An easy to use tool to manage your cloudflare domains", "main": "dist/index.js", "scripts": { - "build": "npx tsc", - "update": "npx tsc && node dist/index.js action=update", - "list": "npx tsc && node dist/index.js action=list" + "build": "npx tsc" }, "author": "", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 1b1479d..33a6d18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,167 +1,175 @@ -import fs = require('fs'); -import yaml = require('js-yaml'); +import cloudflare = require('cloudflare'); import colors = require('colors/safe'); import dotenv = require('dotenv') -import cloudflare = require('cloudflare'); +import fs = require('fs'); +import yaml = require('js-yaml'); dotenv.config(); -var cf = new cloudflare({ - email: process.env.CF_EMAIL, - key: process.env.CF_KEY -}); +type Zone = { + name: string; + action: { id: string }; + jump_start?: boolean | undefined; + type?: "full" | "partial" | undefined; +}; + +type ZonesResponse = { result: Zone[] }; -function groupBy(xs: any, f) { - return xs.reduce((r, v, i, a, k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), {}); -} +class CloudflareDomainsManager { -async function getDnsRecords(zoneId: string) { - return (await cf.dnsRecords.browse(zoneId,{ - page: 1, - per_page: 5000 - })).result -} + cf: cloudflare; -async function updateDomains(domain: string, dryRun: boolean) { - let fileContent = fs.readFileSync('./configuration.yaml', 'utf8'); - let conf: any = yaml.load(fileContent); - //console.log(conf.domains[0].dns_records); - //return; - let domains = conf.domains; - if (domain) { - domains = conf.domains.filter(x => x.name == domain); + async run() { + try { + this.cf = new cloudflare({ + email: process.env.CF_EMAIL, + key: process.env.CF_KEY + }); + + // check cf auth + await this.cf.user.read() + + let action: string; + if (process.argv[2]) { + let split = process.argv[2].split("="); + if (split[0] == "action") { + action = split[1]; + } + } + + let domain: string; + if (action == "update" && process.argv[3]) { + domain = process.argv[3]; + } + + let dryRun = false; + if (process.env['npm_config_dry_run'] == 'true') { + dryRun = true; + } + + switch (action) { + case 'update': + await this.updateDomains(domain, dryRun); + break; + case 'list': + await this.listDomains(); + break; + default: + console.log(colors.red(`[ERROR] Sorry, you have to pass a valid action (update | list). + ex: node index.js action=list` )); + process.exit(1); + } + } catch (e) { + if (e.message.includes("Forbidden")) { + console.error(colors.red("[ERROR] Ops, are you sure to have good CF credentials?")) + } + else { + console.error(e); + } + } } - var dryRunPrefix = ""; - if (dryRun) { - console.log(colors.yellow(`** DRY RUN MODE! **\n`)) - dryRunPrefix = colors.white("[DRY-RUN] "); + groupBy(xs: any, f) { + return xs.reduce((r, v, i, a, k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), {}); } - for (const domain of domains) { - // console.log(domain) - console.log(colors.green(`Check domain: ${colors.white(domain.name)}`)) + async getDnsRecords(zoneId: string) { + return (await this.cf.dnsRecords.browse(zoneId,{ + page: 1, + per_page: 5000 + })).result + } - // load the cloudflare zone dns records - const CFdnsRecords: any = await getDnsRecords(domain.zone_id); - // console.log(CFdnsRecords); + async updateDomains(domain: string, dryRun: boolean) { + let fileContent = fs.readFileSync('./configuration.yaml', 'utf8'); + let conf: any = yaml.load(fileContent); + //console.log(conf.domains[0].dns_records); + //return; + let domains = conf.domains; + if (domain) { + domains = conf.domains.filter(x => x.name == domain); + } - // check conf.domains with registered dns records - console.group(); - for (const dnsRecord of domain.dns_records) { - // console.log(dnsRecord); - console.log("Check record: " + colors.blue(dnsRecord.name)) + var dryRunPrefix = ""; + if (dryRun) { + console.log(colors.yellow(`** DRY RUN MODE! **\n`)) + dryRunPrefix = colors.white("[DRY-RUN] "); + } + + for (const domain of domains) { + // console.log(domain) + console.log(colors.green(`Check domain: ${colors.white(domain.name)}`)) - // find record in CFdnsRecords - const CFdnsRecord = CFdnsRecords.find(x => x.name == dnsRecord.name); + // load the cloudflare zone dns records + const CFdnsRecords: any = await this.getDnsRecords(domain.zone_id); + // console.log(CFdnsRecords); + // check conf.domains with registered dns records console.group(); - if (CFdnsRecord) { - // console.log(CFdnsRecord); - if (dnsRecord.deleted) { - if (!dryRun) { - await cf.dnsRecords.del(domain.zone_id, CFdnsRecord.id); + for (const dnsRecord of domain.dns_records) { + // console.log(dnsRecord); + console.log("Check record: " + colors.blue(dnsRecord.name)) + + // find record in CFdnsRecords + const CFdnsRecord = CFdnsRecords.find(x => x.name == dnsRecord.name); + + console.group(); + if (CFdnsRecord) { + // console.log(CFdnsRecord); + if (dnsRecord.deleted) { + if (!dryRun) { + await this.cf.dnsRecords.del(domain.zone_id, CFdnsRecord.id); + } + console.log(dryRunPrefix + colors.bgYellow("Record *Deleted*.")); + } + else if ( + CFdnsRecord.type == dnsRecord.type && + CFdnsRecord.content == dnsRecord.content && + CFdnsRecord.ttl == dnsRecord.ttl && + CFdnsRecord.proxied == dnsRecord.proxied + ) { + console.log(dryRunPrefix + colors.green("OK.")); + } + else { + if (!dryRun) { + await this.cf.dnsRecords.edit(domain.zone_id, CFdnsRecord.id, dnsRecord); + } + console.log(dryRunPrefix + colors.yellow("Record Updated.")); } - console.log(dryRunPrefix + colors.bgYellow("Record *Deleted*.")); } - else if ( - CFdnsRecord.type == dnsRecord.type && - CFdnsRecord.content == dnsRecord.content && - CFdnsRecord.ttl == dnsRecord.ttl && - CFdnsRecord.proxied == dnsRecord.proxied - ) { - console.log(dryRunPrefix + colors.green("OK.")); + else if (dnsRecord.deleted) { + console.log(dryRunPrefix + colors.green("Record already deleted.")); } else { if (!dryRun) { - await cf.dnsRecords.edit(domain.zone_id, CFdnsRecord.id, dnsRecord); + await this.cf.dnsRecords.add(domain.zone_id, dnsRecord); } - console.log(dryRunPrefix + colors.yellow("Record Updated.")); - } - } - else if (dnsRecord.deleted) { - console.log(dryRunPrefix + colors.green("Record already deleted.")); - } - else { - if (!dryRun) { - await cf.dnsRecords.add(domain.zone_id, dnsRecord); + console.log(dryRunPrefix + colors.bgYellow("Record Created.")); } - console.log(dryRunPrefix + colors.bgYellow("Record Created.")); + console.groupEnd(); } console.groupEnd(); } - console.groupEnd(); } -} -type Zone = { - name: string; - action: { id: string }; - jump_start?: boolean | undefined; - type?: "full" | "partial" | undefined; -}; -type ZonesResponse = { result: Zone[] }; - -async function listDomains() { -// @ts-ignore - const zones = ((await cf.zones.browse({ - page: 1, - per_page: 100 - }) as unknown) as ZonesResponse).result; - // console.log(zones); - const accounts = groupBy(zones, zone => zone.account.name); - // console.log(accounts) - - for (const accountName of Object.keys(accounts)) { - console.log(`account: ${colors.green(accountName)}`) - for (const zone of accounts[accountName]) { - console.log(` Domain: ${colors.white(zone.name)}`) - console.log(` zone_id: ${colors.blue(zone.id)}`) + async listDomains() { + // @ts-ignore + const zones = ((await this.cf.zones.browse({ + page: 1, + per_page: 100 + }) as unknown) as ZonesResponse).result; + // console.log(zones); + const accounts = this.groupBy(zones, zone => zone.account.name); + // console.log(accounts) + + for (const accountName of Object.keys(accounts)) { + console.log(`account: ${colors.green(accountName)}`) + for (const zone of accounts[accountName]) { + console.log(` Domain: ${colors.white(zone.name)}`) + console.log(` zone_id: ${colors.blue(zone.id)}`) + } } } } -void async function main() { - try { - // check cf auth - await cf.user.read() - - let action; - if (process.argv[2]) { - let split = process.argv[2].split("="); - if (split[0] == "action") { - action = split[1]; - } - } - - let domain; - if (action == "update" && process.argv[3]) { - domain = process.argv[3]; - } - - let dryRun = false; - if (process.env['npm_config_dry_run'] == 'true') { - dryRun = true; - } - - switch (action) { - case 'update': - await updateDomains(domain, dryRun); - break; - case 'list': - await listDomains(); - break; - default: - console.log(colors.red(`[ERROR] Sorry, you have to pass a valid action (update | list). - ex: node index.js action=list` )); - process.exit(1); - } - } catch (e) { - if (e.message.includes("Forbidden")) { - console.error(colors.red("[ERROR] Ops, are you sure to have good CF credentials?")) - } - else { - console.error(e); - } - } -}() +module.exports = CloudflareDomainsManager;