diff --git a/Pipfile b/Pipfile index 7f56cc9..a798b27 100644 --- a/Pipfile +++ b/Pipfile @@ -13,4 +13,5 @@ octodns-cloudflare = "*" python_version = "3.12" [scripts] -dns-dryrun = "octodns-sync --config-file=./octodns-config.yml" +dns-dryrun = "octodns-sync --config-file=./dns/octodns-config.yml" +dns-apply = "octodns-sync --config-file=./dns/octodns-config.yml --doit" diff --git a/deno.json b/deno.json index fb61b75..a601c1c 100644 --- a/deno.json +++ b/deno.json @@ -3,5 +3,9 @@ "dns:import": "script/import-records", "dns:apply": "script/apply", "utils:check-tokens": "deno run -EN utils/test-tokens.ts" + }, + "imports": { + "commander": "npm:commander", + "execa": "npm:execa" } } \ No newline at end of file diff --git a/deno.lock b/deno.lock index 37f10ce..4f095b6 100644 --- a/deno.lock +++ b/deno.lock @@ -1,4 +1,171 @@ { "version": "3", - "remote": {} + "packages": { + "specifiers": { + "jsr:@std/bytes@^1.0.2": "jsr:@std/bytes@1.0.2", + "jsr:@std/path": "jsr:@std/path@1.0.3", + "jsr:@std/streams": "jsr:@std/streams@1.0.3", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:commander": "npm:commander@12.1.0", + "npm:execa": "npm:execa@9.3.1", + "npm:which": "npm:which@2.0.2" + }, + "jsr": { + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/path@1.0.3": { + "integrity": "cd89d014ce7eb3742f2147b990f6753ee51d95276bfc211bc50c860c1bc7df6f" + }, + "@std/streams@1.0.3": { + "integrity": "d62e645ab981cee2c3d03040eb03cf387fc6bceef6d4564f3ed485a43741a81f", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + } + }, + "npm": { + "@sec-ant/readable-stream@0.4.1": { + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dependencies": {} + }, + "@sindresorhus/merge-streams@4.0.0": { + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "commander@12.1.0": { + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dependencies": {} + }, + "cross-spawn@7.0.3": { + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "path-key@3.1.1", + "shebang-command": "shebang-command@2.0.0", + "which": "which@2.0.2" + } + }, + "execa@9.3.1": { + "integrity": "sha512-gdhefCCNy/8tpH/2+ajP9IQc14vXchNdd0weyzSJEFURhRMGncQ+zKFxwjAufIewPEJm9BPOaJnvg2UtlH2gPQ==", + "dependencies": { + "@sindresorhus/merge-streams": "@sindresorhus/merge-streams@4.0.0", + "cross-spawn": "cross-spawn@7.0.3", + "figures": "figures@6.1.0", + "get-stream": "get-stream@9.0.1", + "human-signals": "human-signals@8.0.0", + "is-plain-obj": "is-plain-obj@4.1.0", + "is-stream": "is-stream@4.0.1", + "npm-run-path": "npm-run-path@5.3.0", + "pretty-ms": "pretty-ms@9.1.0", + "signal-exit": "signal-exit@4.1.0", + "strip-final-newline": "strip-final-newline@4.0.0", + "yoctocolors": "yoctocolors@2.1.1" + } + }, + "figures@6.1.0": { + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dependencies": { + "is-unicode-supported": "is-unicode-supported@2.0.0" + } + }, + "get-stream@9.0.1": { + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dependencies": { + "@sec-ant/readable-stream": "@sec-ant/readable-stream@0.4.1", + "is-stream": "is-stream@4.0.1" + } + }, + "human-signals@8.0.0": { + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dependencies": {} + }, + "is-plain-obj@4.1.0": { + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dependencies": {} + }, + "is-stream@4.0.1": { + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dependencies": {} + }, + "is-unicode-supported@2.0.0": { + "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "dependencies": {} + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dependencies": {} + }, + "npm-run-path@5.3.0": { + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dependencies": { + "path-key": "path-key@4.0.0" + } + }, + "parse-ms@4.0.0": { + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dependencies": {} + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dependencies": {} + }, + "path-key@4.0.0": { + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dependencies": {} + }, + "pretty-ms@9.1.0": { + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dependencies": { + "parse-ms": "parse-ms@4.0.0" + } + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "shebang-regex@3.0.0" + } + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dependencies": {} + }, + "signal-exit@4.1.0": { + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dependencies": {} + }, + "strip-final-newline@4.0.0": { + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dependencies": {} + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "isexe@2.0.0" + } + }, + "yoctocolors@2.1.1": { + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dependencies": {} + } + } + }, + "redirects": { + "https://esm.sh/v135/@types/which@latest/index.d.ts": "https://esm.sh/v135/@types/which@3.0.4/index.d.ts", + "https://esm.sh/which": "https://esm.sh/which@4.0.0" + }, + "remote": { + "https://esm.sh/v135/isexe@3.1.1/denonext/isexe.mjs": "d65846d3b9b0a24891c794b88b554deee0d5de4e4f2628cf9309a3c991cfe3f0", + "https://esm.sh/v135/which@4.0.0/denonext/which.mjs": "f240e7704e18c757b79c7c5d60fda9745d91ff9ce148b035a64294e5910e9738", + "https://esm.sh/which@4.0.0": "ce75510a9a44c890d7f485069484dbe4c67e31da1db2ce6a3f0e747d4e13a7a5" + }, + "workspace": { + "dependencies": [ + "npm:commander", + "npm:execa" + ] + } } diff --git a/script/dns-records b/script/dns-records index ec11791..2d014f5 100755 --- a/script/dns-records +++ b/script/dns-records @@ -1,63 +1,22 @@ #!/usr/bin/env bash - if [[ $DEBUG != "" ]]; then set -x fi -SCRIPT_DIRECTORY="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" -# Load import.sh into the script first... -source "${SCRIPT_DIRECTORY}/import" -source "$SCRIPT_DIRECTORY/provider-auth" - -usage() { - echo "Usage: $0 [flags] command" >&2 - return 2 -} - -checkEnvVars() { - for env in CLOUDFLARE_API_TOKEN TEST; do - if [[ $"${env}" == "" ]]; then - return 1 - fi - done -} - -print_message() { - local message - local severity - local red - local green - local yellow - local nc +GIT_ROOT="$(git rev-parse --show-toplevel)" +OLDPWD="$PWD" - message="${1}" - severity="${2}" - red='\e[0;31m' - green='\e[0;32m' - yellow='\e[1;33m' - nc='\e[0m' - - case "${severity}" in - "info" ) echo -e "${nc}${message}${nc}";; - "ok" ) echo -e "${green}${message}${nc}";; - "error" ) echo -e "${red}${message}${nc}";; - "warn" ) echo -e "${yellow}${message}${nc}";; - esac -} - -dryrun() { - pipenv run dns-dryrun -} +if [[ "$PWD" != "$GIT_ROOT" ]]; then + echo "going to GIT_ROOT to avoid errors on octodns-sync and friends" + # shellcheck disable=SC2164 + cd "$GIT_ROOT" +fi -main() { - - case "$1" in - "dry-run" | "test" | "validate") - checkEnvVars - dryrun - ;; - *) usage && exit 1;; - esac -} +if [ -z "${CI}" ]; then + dotenvx run -f .env -- deno run -A "$GIT_ROOT/utils/dns-records.ts" "$@" +else + dotenvx run -f .env.ci -- deno run -A "$GIT_ROOT/utils/dns-records.ts" "$@" +fi -main "$*" \ No newline at end of file +# shellcheck disable=SC2164 +cd "$OLDPWD" \ No newline at end of file diff --git a/utils/dns-records.ts b/utils/dns-records.ts new file mode 100755 index 0000000..8df7823 --- /dev/null +++ b/utils/dns-records.ts @@ -0,0 +1,154 @@ +#!/usr/bin/env -S deno run -A +import {program} from "commander"; +import which from "https://esm.sh/which@4.0.0" +import * as path from "jsr:@std/path"; +const gitRoot = path.fromFileUrl(new URL("..", import.meta.url).href) +const whereCli = path.resolve(which.sync("pipenv")) + +let stderrLog: string +let stdoutLog: string + +program.description("a Deno script to manage DNS records easily over octodns") + +program.command("import") + .aliases(["dump"]) + .description("Dump DNS records from") + .argument("", "Domain name to extract DNS records from the provider API") + .argument("[provider]", "DNS nameserver provider name", "cf") + .action(async(domain: string, provider: string) => { + const addTrailingDot = (domain: string) => { + if (domain == "*") { + return "'*'" + } else { + return `${domain}.` + } + } + const args = [ + "run", + "--", + "octodns-dump", + "--config-file", + `${gitRoot}dns/octodns-config.yml`, + "--output-dir", + `${gitRoot}dns` + ] + args.push(addTrailingDot(domain),`${provider || "cf"}`) + console.log(`trying to exec ${whereCli} ${args.join(" ")}`) + const ops = new Deno.Command(whereCli, { + args, + env: { + "PIPENV_DONT_LOAD_ENV": "1" + }, + stderr: "piped", + stdout: "piped" + }).spawn() + const stdoutStreamReader = ops.stdout.pipeThrough(new TextDecoderStream()).getReader() + const stderrStreamReader = ops.stderr.pipeThrough(new TextDecoderStream()).getReader() + + while (true) { + const {value: stdoutData, done: stdoutDone} = await stdoutStreamReader.read() + if (stdoutDone) break + console.log(`stdout: ${stdoutData}`) + stdoutLog += stdoutData + } + + while (true) { + const {value: stderrData, done: stderrDone} = await stderrStreamReader.read() + if (stderrDone) break + console.log(`stderr: ${stderrData}`) + stderrLog += stderrData + } + + if ((await ops.status).success != true) { + console.error(`Run error with code ${(await ops.status).code}`) + Deno.exit((await ops.status).code) + } + }) + +program.command("plan") + .aliases(["dry-run","check", "validate", ]) + .action(async(opts) => { + if (Deno.env.get("CLOUDFLARE_TOKEN") == undefined) { + throw new Error("Cloudflare API token missing, maybe forgot to set DOTENV_PRIVATE_KEY?") + } + const args = ["run", "--", "octodns-sync", "--config-file", `${gitRoot}dns/octodns-config.yml`] + console.log(`trying to exec ${whereCli} ${args.join(" ")}`) + const ops = new Deno.Command(whereCli, { + args, + env: { + "PIPENV_DONT_LOAD_ENV": "1" + }, + stderr: "piped", + stdout: "piped" + }).spawn() + const stdoutStreamReader = ops.stdout.pipeThrough(new TextDecoderStream()).getReader() + const stderrStreamReader = ops.stderr.pipeThrough(new TextDecoderStream()).getReader() + + while (true) { + const {value: stdoutData, done: stdoutDone} = await stdoutStreamReader.read() + if (stdoutDone) break + console.log(`stdout: ${stdoutData}`) + stdoutLog += stdoutData + } + + while (true) { + const {value: stderrData, done: stderrDone} = await stderrStreamReader.read() + if (stderrDone) break + console.log(`stderr: ${stderrData}`) + stderrLog += stderrData + } + + if ((await ops.status).success != true) { + console.error(`Run error with code ${(await ops.status).code}`) + Deno.exit((await ops.status).code) + } + }) + +program.command("apply") + .aliases(["deploy"]) + .description("Deploy DNS record changes") + .option("-f, --force-apply, --force", "force apply to skip change threadshold checks in octodns") + .action(async(opts) => { + if (Deno.env.get("CLOUDFLARE_TOKEN") == undefined) { + throw new Error("Cloudflare API token missing, maybe forgot to set DOTENV_PRIVATE_KEY?") + } + + const args = ["run", "--", "octodns-sync", "--config-file", `${gitRoot}dns/octodns-config.yml`, "--doit"] + + if (opts.forceApply == true) { + args.push("--force") + } + + console.log(`trying to exec ${whereCli} ${args.join(" ")}`) + const ops = new Deno.Command(whereCli, { + args, + env: { + "PIPENV_DONT_LOAD_ENV": "1" + }, + stderr: "piped", + stdout: "piped" + }).spawn() + const stdoutStreamReader = ops.stdout.pipeThrough(new TextDecoderStream()).getReader() + const stderrStreamReader = ops.stderr.pipeThrough(new TextDecoderStream()).getReader() + + while (true) { + const {value: stdoutData, done: stdoutDone} = await stdoutStreamReader.read() + if (stdoutDone) break + console.log(`stdout: ${stdoutData}`) + stdoutLog += stdoutData + } + + while (true) { + const {value: stderrData, done: stderrDone} = await stderrStreamReader.read() + if (stderrDone) break + console.log(`stderr: ${stderrData}`) + stderrLog += stderrData + } + + if ((await ops.status).success != true) { + console.error(`Run error with code ${(await ops.status).code}`) + Deno.exit((await ops.status).code) + } + }) + +program.parse() \ No newline at end of file