1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

require() should handle paths as relative (like nodeJS) (#443)

* Integrate @nlowe's documentation and tests
* Use filepath.Clean() instead of filepath.Abs()
* Use a stackless implementation
This commit is contained in:
Tom Limoncelli
2019-01-29 10:29:00 -05:00
committed by GitHub
parent f9fc6243d4
commit 061ea63322
13 changed files with 171 additions and 21 deletions

3
.gitignore vendored

@ -17,4 +17,5 @@ certs/
spfcache*
adzonedump.*
zones/
stack.sh
stack.sh
.idea/

@ -3,7 +3,6 @@ package commands
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/StackExchange/dnscontrol/models"
@ -101,11 +100,8 @@ func ExecuteDSL(args ExecuteDSLArgs) (*models.DNSConfig, error) {
if args.JSFile == "" {
return nil, errors.Errorf("No config specified")
}
text, err := ioutil.ReadFile(args.JSFile)
if err != nil {
return nil, errors.Errorf("Reading js file %s: %s", args.JSFile, err)
}
dnsConfig, err := js.ExecuteJavascript(string(text), args.DevMode)
dnsConfig, err := js.ExecuteJavascript(args.JSFile, args.DevMode)
if err != nil {
return nil, errors.Errorf("Executing javascript in %s: %s", args.JSFile, err)
}

@ -0,0 +1,52 @@
---
name: require
parameters:
- path
---
`require(...)` behaves similarly to its equivalent in node.js. You can use it
to split your configuration across multiple files. If the path starts with a
`.`, it is calculated relative to the current file. For example:
{% include startExample.html %}
{% highlight js %}
// dnsconfig.js
require('kubernetes/clusters.js');
D("mydomain.net", REG, PROVIDER,
IncludeKubernetes()
);
{%endhighlight%}
{% highlight js %}
// kubernetes/clusters.js
require('./clusters/prod.js');
require('./clusters/dev.js');
function IncludeKubernetes() {
return [includeK8Sprod(), includeK8Sdev()];
}
{%endhighlight%}
{% highlight js %}
// kubernetes/clusters/prod.js
function includeK8Sprod() {
return [ /* ... */ ];
}
{%endhighlight%}
{% highlight js %}
// kubernetes/clusters/dev.js
function includeK8Sdev() {
return [ /* ... */ ];
}
{%endhighlight%}
{% include endExample.html %}

@ -3,19 +3,35 @@ package js
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/pkg/printer"
"github.com/StackExchange/dnscontrol/pkg/transform"
"github.com/robertkrimen/otto"
// load underscore js into vm by default
"github.com/pkg/errors"
"github.com/robertkrimen/otto" // load underscore js into vm by default
_ "github.com/robertkrimen/otto/underscore" // required by otto
)
// currentDirectory is the current directory as used by require().
// This is used to emulate nodejs-style require() directory handling.
// If require("a/b/c.js") is called, any require() statement in c.js
// needs to be accessed relative to "a/b". Therefore we
// track the currentDirectory (which is the current directory as
// far as require() is concerned, not the actual os.Getwd().
var currentDirectory string
// ExecuteJavascript accepts a javascript string and runs it, returning the resulting dnsConfig.
func ExecuteJavascript(script string, devMode bool) (*models.DNSConfig, error) {
func ExecuteJavascript(file string, devMode bool) (*models.DNSConfig, error) {
script, err := ioutil.ReadFile(file)
if err != nil {
return nil, errors.Errorf("Reading js file %s: %s", file, err)
}
// Record the directory path leading up to this file.
currentDirectory = filepath.Clean(filepath.Dir(file))
vm := otto.New()
vm.Set("require", require)
@ -57,16 +73,36 @@ func require(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
throw(call.Otto, "require takes exactly one argument")
}
file := call.Argument(0).String()
printer.Debugf("requiring: %s\n", file)
data, err := ioutil.ReadFile(file)
file := call.Argument(0).String() // The filename as given by the user
// relFile is the file we're actually going to pass to ReadFile().
// It defaults to the user-provided name unless it is relative.
relFile := file
cleanFile := filepath.Clean(filepath.Join(currentDirectory, file))
if strings.HasPrefix(file, ".") {
relFile = cleanFile
}
// Record the old currentDirectory so that we can return there.
currentDirectoryOld := currentDirectory
// Record the directory path leading up to the file we're about to require.
currentDirectory = filepath.Clean(filepath.Dir(cleanFile))
printer.Debugf("requiring: %s (%s)\n", file, relFile)
data, err := ioutil.ReadFile(relFile)
if err != nil {
throw(call.Otto, err.Error())
}
_, err = call.Otto.Run(string(data))
if err != nil {
throw(call.Otto, err.Error())
}
// Pop back to the old directory.
currentDirectory = currentDirectoryOld
return otto.TrueValue()
}

@ -34,11 +34,7 @@ func TestParsedFiles(t *testing.T) {
m := minify.New()
m.AddFunc("json", minjson.Minify)
t.Run(f.Name(), func(t *testing.T) {
content, err := ioutil.ReadFile(filepath.Join(testDir, f.Name()))
if err != nil {
t.Fatal(err)
}
conf, err := ExecuteJavascript(string(content), true)
conf, err := ExecuteJavascript(string(filepath.Join(testDir, f.Name())), true)
if err != nil {
t.Fatal(err)
}

@ -1,2 +1,2 @@
require("pkg/js/parse_tests/import.js")
require("./import.js")

@ -0,0 +1 @@
require('./complexImports/base.js');

@ -0,0 +1,38 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "none",
"dnsProviders": {},
"records": [
{
"type": "A",
"name": "@",
"target": "1.2.3.4"
},
{
"type": "CNAME",
"name": "A",
"target": "foo.com."
},
{
"type": "CNAME",
"name": "C",
"target": "foo.com."
},
{
"type": "CNAME",
"name": "D",
"target": "foo.com."
},
{
"type": "CNAME",
"name": "B",
"target": "foo.com."
}
]
}
]
}

@ -0,0 +1,3 @@
function a() {
return CNAME("A", "foo.com.")
}

@ -0,0 +1,8 @@
require('../a.js');
function c() {
return [
a(),
CNAME("C", "foo.com.")
]
}

@ -0,0 +1,8 @@
require('pkg/js/parse_tests/complexImports/b/d/d.js');
function b() {
return [
d(),
CNAME("B", "foo.com.")
];
}

@ -0,0 +1,3 @@
function d() {
return CNAME("D", "foo.com.")
}

@ -0,0 +1,8 @@
require('./a/c/c.js');
require('./b/b.js');
D("foo.com","none",
A("@","1.2.3.4"),
c(),
b()
);