Merge branch 'master' into dhcid

This commit is contained in:
Tom Limoncelli 2023-09-14 13:18:10 -04:00 committed by GitHub
commit 5761028cbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 203 additions and 41 deletions

View file

@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: restore_cache
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
@ -28,7 +28,7 @@ jobs:
gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES
- name: Enforce Go Formatted Code
run: "[ `go fmt ./... | wc -l` -eq 0 ]"
- uses: actions/upload-artifact@v3.1.2
- uses: actions/upload-artifact@v3.1.3
with:
path: "/tmp/test-results"
@ -178,7 +178,7 @@ jobs:
- uses: actions/checkout@v4
- run: mkdir -p "$TEST_RESULTS"
- name: restore_cache
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
@ -192,7 +192,7 @@ jobs:
gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- -timeout 30m -v -verbose -provider ${{ matrix.provider }} -cfworkers=false
fi
working-directory: integrationTest
- uses: actions/upload-artifact@v3.1.2
- uses: actions/upload-artifact@v3.1.3
with:
path: "/tmp/test-results"
integrtests-diff2:
@ -285,7 +285,7 @@ jobs:
- uses: actions/checkout@v4
- run: mkdir -p "$TEST_RESULTS"
- name: restore_cache
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
@ -299,7 +299,7 @@ jobs:
echo "Skip test for ${{ matrix.provider }} provider"
fi
working-directory: integrationTest
- uses: actions/upload-artifact@v3.1.2
- uses: actions/upload-artifact@v3.1.3
with:
path: "/tmp/test-results"
# release:
@ -321,17 +321,17 @@ jobs:
# docker-password: "${{ secrets.DOCKER_PASSWORD }}"
# docker-username: "${{ env.DOCKER_LOGIN }}"
# - name: restore_cache
# uses: actions/cache@v3.3.1
# uses: actions/cache@v3.3.2
# with:
# key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
# restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
# - name: Install goreleaser
# run: go install github.com/goreleaser/goreleaser@latest
# - run: goreleaser release
# - uses: actions/upload-artifact@v3.1.2
# - uses: actions/upload-artifact@v3.1.3
# with:
# path: dist
# - uses: actions/upload-artifact@v3.1.2
# - uses: actions/upload-artifact@v3.1.3
# with:
# path: |-
# dist/*.rpm

View file

@ -1,6 +1,7 @@
package commands
import (
"encoding/json"
"fmt"
"os"
"strings"
@ -45,6 +46,13 @@ type PreviewArgs struct {
Full bool
}
type ReportItem struct {
Domain string `json:"domain"`
Corrections int `json:"corrections"`
Provider string `json:"provider,omitempty"`
Registrar string `json:"registrar,omitempty"`
}
func (args *PreviewArgs) flags() []cli.Flag {
flags := args.GetDNSConfigArgs.flags()
flags = append(flags, args.GetCredentialsArgs.flags()...)
@ -93,6 +101,7 @@ var _ = cmd(catMain, func() *cli.Command {
type PushArgs struct {
PreviewArgs
Interactive bool
Report string
}
func (args *PushArgs) flags() []cli.Flag {
@ -102,21 +111,26 @@ func (args *PushArgs) flags() []cli.Flag {
Destination: &args.Interactive,
Usage: "Interactive. Confirm or Exclude each correction before they run",
})
flags = append(flags, &cli.StringFlag{
Name: "report",
Destination: &args.Report,
Usage: `Generate a machine-parseable report of performed corrections.`,
})
return flags
}
// Preview implements the preview subcommand.
func Preview(args PreviewArgs) error {
return run(args, false, false, printer.DefaultPrinter)
return run(args, false, false, printer.DefaultPrinter, nil)
}
// Push implements the push subcommand.
func Push(args PushArgs) error {
return run(args.PreviewArgs, true, args.Interactive, printer.DefaultPrinter)
return run(args.PreviewArgs, true, args.Interactive, printer.DefaultPrinter, &args.Report)
}
// run is the main routine common to preview/push
func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report *string) error {
// TODO: make truly CLI independent. Perhaps return results on a channel as they occur
// This is a hack until we have the new printer replacement.
@ -151,7 +165,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
// create a WaitGroup with the length of domains for the anonymous functions (later goroutines) to wait for
var wg sync.WaitGroup
wg.Add(len(cfg.Domains))
var reportItems []ReportItem
// For each domain in dnsconfig.js...
for _, domain := range cfg.Domains {
// Run preview or push operations per domain as anonymous function, in preparation for the later use of goroutines.
@ -232,6 +246,11 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
totalCorrections += len(corrections)
// When diff1 goes away, the call to printReports() should be moved to HERE.
//printReports(domain.Name, provider.Name, reports, out, push, notifier)
reportItems = append(reportItems, ReportItem{
Domain: domain.Name,
Corrections: len(corrections),
Provider: provider.Name,
})
anyErrors = printOrRunCorrections(domain.Name, provider.Name, corrections, out, push, interactive, notifier) || anyErrors
}
@ -253,6 +272,11 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
return
}
totalCorrections += len(corrections)
reportItems = append(reportItems, ReportItem{
Domain: domain.Name,
Corrections: len(corrections),
Registrar: domain.RegistrarName,
})
anyErrors = printOrRunCorrections(domain.Name, domain.RegistrarName, corrections, out, push, interactive, notifier) || anyErrors
}(domain)
}
@ -269,6 +293,20 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
if totalCorrections != 0 && args.WarnChanges {
return fmt.Errorf("there are pending changes")
}
if report != nil {
f, err := os.OpenFile(*report, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
b, err := json.MarshalIndent(reportItems, "", " ")
if err != nil {
return err
}
if _, err := f.Write(b); err != nil {
return err
}
}
return nil
}

View file

@ -154,6 +154,7 @@
* [Nameservers and Delegations](nameservers.md)
* [Notifications](notifications.md)
* [Useful code tricks](code-tricks.md)
* [JSON Reports](json-reports.md)
## Developer info

View file

@ -8,17 +8,38 @@ ts_ignore: true
`require(...)` loads the specified JavaScript or JSON file, allowing
to split your configuration across multiple files.
A better name for this function might be "include".
If the supplied `path` string ends with `.js`, the file is interpreted
as JavaScript code, almost as though its contents had been included in
the currently-executing file. If the path string ends with `.json`,
`require()` returns the `JSON.parse()` of the file's contents.
If the path string begins with a `.`, it is interpreted relative to
If the path string begins with a `./`, it is interpreted relative to
the currently-loading file (which may not be the file where the
`require()` statement is, if called within a function), otherwise it
`require()` statement is, if called within a function). Otherwise it
is interpreted relative to the program's working directory at the time
of the call.
### Example 1: Simple
In this example, we separate our macros in one file, and put groups of domains
in 3 other files. The result is a cleaner separation of code vs. domains.
{% code title="dnsconfig.js" %}
```javascript
require("lib/macros.json");
require("domains/main.json");
require("domains/parked.json");
require("domains/otherstuff.json");
```
{% endcode %}
### Example 2: Complex
Here's a more complex example:
{% code title="dnsconfig.js" %}
```javascript
require("kubernetes/clusters.js");
@ -60,7 +81,9 @@ function includeK8Sdev() {
```
{% endcode %}
You can also use it to require JSON files and initialize variables with it:
### Example 3: JSON
Requiring JSON files initializes variables:
{% code title="dnsconfig.js" %}
```javascript
@ -83,16 +106,13 @@ for (var domain in domains) {
```
{% endcode %}
# Future
# Notes
It might be better to rename the function to something like
`include()` instead, (leaving `require` as a deprecated alias) because
by analogy it is *much* closer to PHP's `include()` function than it
is to node's `require()`. After all, the reason node.js calls it
"require" is because it's a declarative statement saying the file is
needed, and so should be loaded if it hasn't already been loaded.
`require()` is *much* closer to PHP's `include()` function than it
is to node's `require()`.
Node's `require()` only includes a file once.
In contrast, DNSControl's `require()` is actually an imperative command to
load the file and execute the code or parse the data from it. (So if
two files both `require("./tools.js")`, for example, then it will be
loaded twice, whereas in node.js it would only be loaded once.)
load the file and execute the code or parse the data from it. For example if
two files both `require("./tools.js")`, then it will be
loaded twice, whereas in node.js it would only be loaded once.

View file

@ -8,7 +8,7 @@ parameter_types:
recursive: boolean
---
`require_glob()` can recursively load `.js` files, optionally non-recursive as well.
`require_glob()` recursively loads `.js` files that match a glob (wildcard). The recursion can be disabled.
Possible parameters are:
@ -31,8 +31,13 @@ require_glob("./domains/", false);
```
{% endcode %}
One more important thing to note: `require_glob()` is as smart as `require()` is. It loads files always relative to the JavaScript
file where it's being executed in. Let's go with an example, as it describes it better:
# Comparison to require()
`require_glob()` and `require()` both use the same rules for determining which directory path is
relative to.
This will load files being present underneath `./domains/user1/` and **NOT** at below `./domains/`, as `require_glob()`
is called in the subfolder `domains/`.
{% code title="dnsconfig.js" %}
```javascript
@ -45,6 +50,3 @@ require("domains/index.js");
require_glob("./user1/");
```
{% endcode %}
This will now load files being present underneath `./domains/user1/` and **NOT** at below `./domains/`, as `require_glob()`
is called in the subfolder `domains/`.

View file

@ -0,0 +1,34 @@
# JSON Reports
DNSControl has build in functionality to generate a machine-parseable report after pushing changes. This report is JSON formated and contains the zonename, the provider or registrar name and the amount of performed changes.
## Usage
To enable the report option you must use the `push` operation in combination with the `--report <filename>` option. This generates the json file.
{% code title="report.json" %}
```json
[
{
"domain": "private.example.com",
"corrections": 10,
"provider": "bind"
},
{
"domain": "private.example.com",
"corrections": 0,
"registrar": "none"
},
{
"domain": "admin.example.com",
"corrections": 5,
"provider": "bind"
},
{
"domain": "admin.example.com",
"corrections": 0,
"registrar": "none"
}
]
```
{% endcode %}

View file

@ -53,7 +53,7 @@ If a feature is definitively not supported for whatever reason, we would also li
| [`ORACLE`](providers/oracle.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| [`OVH`](providers/ovh.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ❔ | ❔ | ❔ | ❌ | ❔ | ✅ | ✅ | ✅ | ❔ | ✅ | ❌ | ✅ | ✅ |
| [`PACKETFRAME`](providers/packetframe.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ |
| [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| [`POWERDNS`](providers/powerdns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
| [`RWTH`](providers/rwth.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ✅ |

View file

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"sort"
"time"
)
@ -18,7 +19,7 @@ type porkbunProvider struct {
secretKey string
}
type requestParams map[string]string
type requestParams map[string]any
type errorResponse struct {
Status string `json:"status"`
@ -40,6 +41,11 @@ type recordResponse struct {
Records []domainRecord `json:"records"`
}
type nsResponse struct {
Status string `json:"status"`
Nameservers []string `json:"ns"`
}
func (c *porkbunProvider) post(endpoint string, params requestParams) ([]byte, error) {
params["apikey"] = c.apiKey
params["secretapikey"] = c.secretKey
@ -82,7 +88,7 @@ func (c *porkbunProvider) ping() error {
func (c *porkbunProvider) createRecord(domain string, rec requestParams) error {
if _, err := c.post("/dns/create/"+domain, rec); err != nil {
return fmt.Errorf("failed create record (porkbun): %s", err)
return fmt.Errorf("failed create record (porkbun): %w", err)
}
return nil
}
@ -90,14 +96,14 @@ func (c *porkbunProvider) createRecord(domain string, rec requestParams) error {
func (c *porkbunProvider) deleteRecord(domain string, recordID string) error {
params := requestParams{}
if _, err := c.post(fmt.Sprintf("/dns/delete/%s/%s", domain, recordID), params); err != nil {
return fmt.Errorf("failed delete record (porkbun): %s", err)
return fmt.Errorf("failed delete record (porkbun): %w", err)
}
return nil
}
func (c *porkbunProvider) modifyRecord(domain string, recordID string, rec requestParams) error {
if _, err := c.post(fmt.Sprintf("/dns/edit/%s/%s", domain, recordID), rec); err != nil {
return fmt.Errorf("failed update (porkbun): %s", err)
return fmt.Errorf("failed update (porkbun): %w", err)
}
return nil
}
@ -106,7 +112,7 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
params := requestParams{}
var bodyString, err = c.post("/dns/retrieve/"+domain, params)
if err != nil {
return nil, fmt.Errorf("failed fetching record list from porkbun: %s", err)
return nil, fmt.Errorf("failed fetching record list from porkbun: %w", err)
}
var dr recordResponse
@ -121,3 +127,26 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
}
return records, nil
}
func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
params := requestParams{}
var bodyString, err = c.post(fmt.Sprintf("/domain/getNs/%s", domain), params)
if err != nil {
return nil, fmt.Errorf("failed fetching nameserver list from porkbun: %w", err)
}
var ns nsResponse
json.Unmarshal(bodyString, &ns)
sort.Strings(ns.Nameservers)
return ns.Nameservers, nil
}
func (c *porkbunProvider) updateNameservers(ns []string, domain string) error {
params := requestParams{}
params["ns"] = ns
if _, err := c.post(fmt.Sprintf("/domain/updateNs/%s", domain), params); err != nil {
return fmt.Errorf("failed NS update (porkbun): %w", err)
}
return nil
}

View file

@ -3,6 +3,7 @@ package porkbun
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
@ -25,8 +26,16 @@ var defaultNS = []string{
"salvador.ns.porkbun.com",
}
// NewPorkbun creates the provider.
func NewPorkbun(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
func newReg(conf map[string]string) (providers.Registrar, error) {
return newPorkbun(conf, nil)
}
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
return newPorkbun(conf, metadata)
}
// newPorkbun creates the provider.
func newPorkbun(m map[string]string, metadata json.RawMessage) (*porkbunProvider, error) {
c := &porkbunProvider{}
c.apiKey, c.secretKey = m["api_key"], m["secret_key"]
@ -63,8 +72,9 @@ var features = providers.DocumentationNotes{
}
func init() {
providers.RegisterRegistrarType("PORKBUN", newReg)
fns := providers.DspFuncs{
Initializer: NewPorkbun,
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("PORKBUN", fns, features)
@ -317,3 +327,31 @@ func fixTTL(ttl uint32) uint32 {
}
return minimumTTL
}
func (c *porkbunProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
nss, err := c.getNameservers(dc.Name)
if err != nil {
return nil, err
}
foundNameservers := strings.Join(nss, ",")
expected := []string{}
for _, ns := range dc.Nameservers {
expected = append(expected, ns.Name)
}
sort.Strings(expected)
expectedNameservers := strings.Join(expected, ",")
if foundNameservers == expectedNameservers {
return nil, nil
}
return []*models.Correction{
{
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
F: func() error {
return c.updateNameservers(expected, dc.Name)
},
},
}, nil
}