dnscontrol/commands/printIR.go
Tom Limoncelli 703084160f
REFACTOR: BIND/GANDI_V5 add "RP" record type, rewrite CLOUDFLAREAPI CF_* and more (#3886)
# Issue

* New record type: "RP" (supported by BIND and GANDI_V5) 
* Cloudflare: CF_REDIRECT/CF_TEMP_REDIRECT now generate
CF_SINGLE_REDIRECT records. All PAGE_RULE-based code is removed.
PAGE_RULEs are deprecated at Cloudflare. (be careful when upgrading!)
* New "v2" RecordConfig: RP and CF_SINGLE_REDIRECT are the only record
types that use this method. It shifts most of the work out of JavaScript
and into the Go code, making new record types easier to make, easier to
test, and easier to use by providers. This opens the door to new things
like a potential code-generator for rtypes. Converting existing rtypes
will happen over the next year.
* When only the TTL changes (MODIFY-TTL), the output lists the TTL
change first, not at the end of the line where it is visually lost.
* CF_REDIRECT/CF_TEMP_REDIRECT generate different rule "names". They
will be updated the first time you "push" with this release. The order
of the rules may also change. If you rules depend on a particular order,
be very careful with this upgrade!

Refactoring:

* New "v2" RecordConfig: Record types using this new method simply
package the parameters from dnsconfig.js statements like
CF_REDIRECT(foo,bar) and send them (raw) to the Go code. The Go code
does all processing, validation, etc. and turns them into RecordConfig
that store all the rdata in `RecordConfig.F`. No more adding fields to
RecordConfig for each new record type!
* RecordConfig.IsModernType() returns true if the record uses the new v2
record mechanism.
* PostProcess is now a method on DnsConfig and DomainConfig.
* DOC: How to create new rtypes using the v2 method (incomplete)

Other things:

* Integration tests for CF "full proxy" are removed. This feature
doesn't exist any more.
* DEV: Debugger tips now includes VSCode advice
* TESTING: The names of testgroup's can now have extra spaces to make
data align better
* CF_TEMP_REDIRECT/CF_REDIRECT is now a "builder" that generates
CLOUDFLAREAPI_SINGLE_REDIRECT records.
* And more!

# Resolution

---------

Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
2025-12-04 16:42:20 -05:00

181 lines
4.4 KiB
Go

package commands
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/js"
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/urfave/cli/v2"
)
var _ = cmd(catDebug, func() *cli.Command {
var args PrintIRArgs
return &cli.Command{
Name: "print-ir",
Usage: "Output intermediate representation (IR) after running validation and normalization logic.",
Action: func(c *cli.Context) error {
return exit(PrintIR(args))
},
Flags: args.flags(),
}
}())
// CheckArgs encapsulates the flags/arguments for the check command.
type CheckArgs struct {
GetDNSConfigArgs
}
var _ = cmd(catDebug, func() *cli.Command {
var args CheckArgs
// This is the same as print-ir with the following changes:
// - no JSON is output
// - error messages are sent to stdout.
// - prints "No errors." if there were no errors.
return &cli.Command{
Name: "check",
Usage: "Check and validate dnsconfig.js. Output to stdout. Do not access providers.",
Action: func(c *cli.Context) error {
// Create a PrintIRArgs struct and copy our args to the
// appropriate fields.
var pargs PrintIRArgs
// Copy these verbatim:
pargs.JSFile = args.JSFile
pargs.JSONFile = args.JSONFile
pargs.DevMode = args.DevMode
pargs.Variable = args.Variable
// Force these settings:
pargs.Pretty = false
pargs.Output = os.DevNull
// "check" sends all errors to stdout, not stderr.
cli.ErrWriter = os.Stdout
log.SetOutput(os.Stdout)
err := exit(PrintIR(pargs))
rfc4183.PrintWarning()
if err == nil {
fmt.Fprintf(os.Stdout, "No errors.\n")
}
return err
},
Flags: args.flags(),
}
}())
// PrintIRArgs encapsulates the flags/arguments for the print-ir command.
type PrintIRArgs struct {
GetDNSConfigArgs
PrintJSONArgs
Raw bool
}
func (args *PrintIRArgs) flags() []cli.Flag {
flags := append(args.GetDNSConfigArgs.flags(), args.PrintJSONArgs.flags()...)
flags = append(flags, &cli.BoolFlag{
Name: "raw",
Usage: "Skip validation and normalization. Just print js result.",
Destination: &args.Raw,
})
return flags
}
// PrintIR implements the print-ir subcommand.
func PrintIR(args PrintIRArgs) error {
cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
if err != nil {
return err
}
if !args.Raw {
errs := normalize.ValidateAndNormalizeConfig(cfg)
if PrintValidationErrors(errs) {
return errors.New("exiting due to validation errors")
}
}
return PrintJSON(args.PrintJSONArgs, cfg)
}
// PrintValidationErrors formats and prints the validation errors and warnings.
func PrintValidationErrors(errs []error) (fatal bool) {
if len(errs) == 0 {
return false
}
log.Printf("%d Validation errors:\n", len(errs))
for _, err := range errs {
if _, ok := err.(normalize.Warning); ok {
log.Printf("WARNING: %s\n", err)
} else {
fatal = true
log.Printf("ERROR: %s\n", err)
}
}
return
}
// ExecuteDSL executes the dnsconfig.js contents.
func ExecuteDSL(args ExecuteDSLArgs) (*models.DNSConfig, error) {
if args.JSFile == "" {
return nil, errors.New("no config specified")
}
dnsConfig, err := js.ExecuteJavaScript(args.JSFile, args.DevMode, stringSliceToMap(args.Variable))
if err != nil {
return nil, fmt.Errorf("executing %s: %w", args.JSFile, err)
}
err = dnsConfig.PostProcess()
if err != nil {
return nil, err
}
return dnsConfig, nil
}
// PrintJSON outputs/prettyprints the IR data.
func PrintJSON(args PrintJSONArgs, config *models.DNSConfig) (err error) {
var dat []byte
if args.Pretty {
dat, err = json.MarshalIndent(config, "", " ")
} else {
dat, err = json.Marshal(config)
}
if err != nil {
return err
}
if args.Output != "" {
f, err := os.Create(args.Output)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(dat)
return err
}
fmt.Println(string(dat))
return nil
}
func exit(err error) error {
if err == nil {
return nil
}
return cli.Exit(err, 1)
}
// stringSliceToMap converts cli.StringSlice to map[string]string for further processing
func stringSliceToMap(stringSlice cli.StringSlice) map[string]string {
mapString := make(map[string]string, len(stringSlice.Value()))
for _, values := range stringSlice.Value() {
parts := strings.SplitN(values, "=", 2)
if len(parts) == 2 {
mapString[parts[0]] = parts[1]
}
}
return mapString
}