refactor into groups (#684)

* Refactor tests into "groups", each with its own filter (not/only/requires) to select which providers are appropriate.
* Test driver code is now a lot more simple and clear.
* Add support for not(), only(), and requires() as a way to select/reject providers for a test.
* Add docs explaining how to add tests
* Logging messages are much cleaner now, especially when tests are skipped.
* -start and -end now refer to test groups, not individual tests.  Log messages list the group numbers clearly.
* Add stringer for Capabilities
* Change the order of the tests so that simple tests are first
* Removed knownFailures from providers.json
* fmtjson providers.json

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Tom Limoncelli 2020-03-10 10:13:20 -04:00 committed by GitHub
parent fa160b7202
commit 67e78f7e15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 10344 additions and 9999 deletions

4
go.mod
View file

@ -50,10 +50,10 @@ require (
github.com/vultr/govultr v0.2.0
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
golang.org/x/mod v0.2.0 // indirect
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect
golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc // indirect
golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
google.golang.org/api v0.17.0
google.golang.org/appengine v1.6.5 // indirect

5
go.sum
View file

@ -277,6 +277,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjut
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -290,6 +292,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -330,6 +333,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc h1:AlqcddMxhvJRMNuW1bFwyuNdm+TCkcEGKvw6FVPTuR8=
golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8 h1:2oWBslXKHx1vReVc1bA4mflTOcSxgXz536kSRBhwDds=
golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -3,6 +3,7 @@ package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"testing"
@ -84,86 +85,168 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
return dc
}
func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, knownFailures map[int]bool, origConfig map[string]string) {
dc := getDomainConfigWithNameservers(t, prv, domainName)
// run tests one at a time
end := *endIdx
tests := makeTests(t)
if end == 0 || end >= len(tests) {
end = len(tests) - 1
// testPermitted returns nil if the test is permitted, otherwise an
// error explaining why it is not.
func testPermitted(t *testing.T, p string, f TestGroup) error {
// not() and only() can't be mixed.
if len(f.only) != 0 && len(f.not) != 0 {
return fmt.Errorf("invalid filter: can't mix not() and only()")
}
for i := *startIdx; i <= end; i++ {
tst := tests[i]
if t.Failed() {
break
// TODO(tlim): Have a separate validation pass so that such mistakes
// are more visible?
// If there are any required capabilities, make sure they all exist.
if len(f.required) != 0 {
for _, c := range f.required {
if !providers.ProviderHasCapability(*providerToRun, c) {
return fmt.Errorf("%s not supported", c)
}
}
t.Run(fmt.Sprintf("%d: %s", i, tst.Desc), func(t *testing.T) {
skipVal := false
if knownFailures[i] {
t.Log("SKIPPING VALIDATION FOR KNOWN FAILURE CASE")
skipVal = true
}
// If there are any "only" items, you must be one of them.
if len(f.only) != 0 {
for _, provider := range f.only {
if p == provider {
return nil
}
dom, _ := dc.Copy()
for _, r := range tst.Records {
rc := models.RecordConfig(*r)
if strings.Contains(rc.GetTargetField(), "**current-domain**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain**", domainName, 1) + ".")
}
if strings.Contains(rc.GetTargetField(), "**current-domain-no-trailing**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain-no-trailing**", domainName, 1))
}
if strings.Contains(rc.GetLabelFQDN(), "**current-domain**") {
rc.SetLabelFromFQDN(strings.Replace(rc.GetLabelFQDN(), "**current-domain**", domainName, 1), domainName)
}
if providers.ProviderHasCapability(*providerToRun, providers.CanUseAzureAlias) {
if strings.Contains(rc.GetTargetField(), "**subscription-id**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**subscription-id**", origConfig["SubscriptionID"], 1))
}
if strings.Contains(rc.GetTargetField(), "**resource-group**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**resource-group**", origConfig["ResourceGroup"], 1))
}
}
dom.Records = append(dom.Records, &rc)
}
return fmt.Errorf("disabled by only")
}
// If there are any "not" items, you must NOT be one of them.
if len(f.not) != 0 {
for _, provider := range f.not {
if p == provider {
return fmt.Errorf("excluded by not(\"%s\")", provider)
}
dom.IgnoredLabels = tst.IgnoredLabels
models.PostProcessRecords(dom.Records)
dom2, _ := dom.Copy()
// get corrections for first time
corrections, err := prv.GetDomainCorrections(dom)
if err != nil {
t.Fatal(fmt.Errorf("runTests: %w", err))
}
return nil
}
return nil
}
//func makeClearFilter() *TestCase {
// tc := tc("Empty")
// tc.ChangeFilter = true
// return tc
//}
// desc := fmt.Sprintf("%d: %s", i, tst.Desc)
// makeChanges runs one set of DNS record tests. Returns true on success.
func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.DomainConfig, tst *TestCase, desc string, expectChanges bool, origConfig map[string]string) bool {
domainName := dc.Name
return t.Run(desc, func(t *testing.T) {
dom, _ := dc.Copy()
for _, r := range tst.Records {
rc := models.RecordConfig(*r)
if strings.Contains(rc.GetTargetField(), "**current-domain**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain**", domainName, 1) + ".")
}
if !skipVal && i != *startIdx && len(corrections) == 0 {
if tst.Desc != "Empty" {
// There are "no corrections" if the last test was programmatically
// skipped. We detect this (possibly inaccurately) by checking to
// see if .Desc is "Empty".
t.Fatalf("Expect changes for all tests, but got none")
}
if strings.Contains(rc.GetTargetField(), "**current-domain-no-trailing**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain-no-trailing**", domainName, 1))
}
for _, c := range corrections {
if *verbose {
t.Log(c.Msg)
}
err = c.F()
if !skipVal && err != nil {
t.Fatal(err)
}
if strings.Contains(rc.GetLabelFQDN(), "**current-domain**") {
rc.SetLabelFromFQDN(strings.Replace(rc.GetLabelFQDN(), "**current-domain**", domainName, 1), domainName)
}
// run a second time and expect zero corrections
corrections, err = prv.GetDomainCorrections(dom2)
//if providers.ProviderHasCapability(*providerToRun, providers.CanUseAzureAlias) {
if strings.Contains(rc.GetTargetField(), "**subscription-id**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**subscription-id**", origConfig["SubscriptionID"], 1))
}
if strings.Contains(rc.GetTargetField(), "**resource-group**") {
_ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**resource-group**", origConfig["ResourceGroup"], 1))
}
//}
dom.Records = append(dom.Records, &rc)
}
dom.IgnoredLabels = tst.IgnoredLabels
models.PostProcessRecords(dom.Records)
dom2, _ := dom.Copy()
// get and run corrections for first time
corrections, err := prv.GetDomainCorrections(dom)
if err != nil {
t.Fatal(fmt.Errorf("runTests: %w", err))
}
if len(corrections) == 0 && expectChanges {
t.Fatalf("Expected changes, but got none")
}
for _, c := range corrections {
if *verbose {
t.Log(c.Msg)
}
err = c.F()
if err != nil {
t.Fatal(err)
}
if !skipVal && len(corrections) != 0 {
t.Logf("Expected 0 corrections on second run, but found %d.", len(corrections))
for i, c := range corrections {
t.Logf("#%d: %s", i, c.Msg)
}
t.FailNow()
}
// If we just emptied out the zone, no need for a second pass.
if len(tst.Records) == 0 {
return
}
// run a second time and expect zero corrections
corrections, err = prv.GetDomainCorrections(dom2)
if err != nil {
t.Fatal(err)
}
if len(corrections) != 0 {
t.Logf("Expected 0 corrections on second run, but found %d.", len(corrections))
for i, c := range corrections {
t.Logf("#%d: %s", i, c.Msg)
}
})
t.FailNow()
}
})
}
func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, knownFailures map[int]bool, origConfig map[string]string) {
dc := getDomainConfigWithNameservers(t, prv, domainName)
testGroups := makeTests(t)
firstGroup := *startIdx
lastGroup := *endIdx
if lastGroup == 0 {
lastGroup = len(testGroups)
}
// Start the zone with a clean slate.
makeChanges(t, prv, dc, tc("Empty"), "Clean Slate", false, nil)
curGroup := -1
for gIdx, group := range testGroups {
// Abide by -start -end flags
curGroup += 1
if curGroup < firstGroup || curGroup > lastGroup {
continue
}
// Abide by filter
if err := testPermitted(t, *providerToRun, *group); err != nil {
//t.Logf("%s: ***SKIPPED(%v)***", group.Desc, err)
makeChanges(t, prv, dc, tc("Empty"), fmt.Sprintf("%02d:%s ***SKIPPED(%v)***", gIdx, group.Desc, err), false, origConfig)
continue
}
// Run the tests.
for _, tst := range group.tests {
makeChanges(t, prv, dc, tst, fmt.Sprintf("%02d:%s", gIdx, group.Desc), true, origConfig)
if t.Failed() {
break
}
}
// Remove all records so next group starts with a clean slate.
makeChanges(t, prv, dc, tc("Empty"), "Post cleanup", false, nil)
}
}
func TestDualProviders(t *testing.T) {
@ -209,6 +292,14 @@ func TestDualProviders(t *testing.T) {
}
}
type TestGroup struct {
Desc string
required []providers.Capability
only []string
not []string
tests []*TestCase
}
type TestCase struct {
Desc string
Records []*rec
@ -349,6 +440,45 @@ func (r *rec) ttl(t uint32) *rec {
return r
}
func manyA(namePattern, target string, n int) []*rec {
recs := []*rec{}
for i := 0; i < n; i++ {
recs = append(recs, makeRec(fmt.Sprintf(namePattern, i), target, "A"))
}
return recs
}
func testgroup(desc string, items ...interface{}) *TestGroup {
group := &TestGroup{Desc: desc}
for _, item := range items {
switch v := item.(type) {
case requiresFilter:
if len(group.tests) != 0 {
fmt.Printf("ERROR: requires() must be before all tc(): %v\n", desc)
os.Exit(1)
}
group.required = append(group.required, v.caps...)
case notFilter:
if len(group.tests) != 0 {
fmt.Printf("ERROR: not() must be before all tc(): %v\n", desc)
os.Exit(1)
}
group.not = append(group.not, v.names...)
case onlyFilter:
if len(group.tests) != 0 {
fmt.Printf("ERROR: only() must be before all tc(): %v\n", desc)
os.Exit(1)
}
group.only = append(group.only, v.names...)
case *TestCase:
group.tests = append(group.tests, v)
default:
fmt.Printf("I don't know about type %T (%v)\n", v, v)
}
}
return group
}
func tc(desc string, recs ...*rec) *TestCase {
var records []*rec
var ignored []string
@ -366,90 +496,229 @@ func tc(desc string, recs ...*rec) *TestCase {
}
}
func manyA(namePattern, target string, n int) []*rec {
recs := []*rec{}
for i := 0; i < n; i++ {
recs = append(recs, makeRec(fmt.Sprintf(namePattern, i), target, "A"))
}
return recs
func clear(items ...interface{}) *TestCase {
return tc("Empty")
}
func makeTests(t *testing.T) []*TestCase {
// ALWAYS ADD TO BOTTOM OF LIST. Order and indexes matter.
tests := []*TestCase{
// A
tc("Empty"),
tc("Create an A record", a("@", "1.1.1.1")),
tc("Change it", a("@", "1.2.3.4")),
tc("Add another", a("@", "1.2.3.4"), a("www", "1.2.3.4")),
tc("Add another(same name)", a("@", "1.2.3.4"), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change a ttl", a("@", "1.2.3.4").ttl(1000), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change single target from set", a("@", "1.2.3.4").ttl(1000), a("www", "2.2.2.2"), a("www", "5.6.7.8")),
tc("Change all ttls", a("@", "1.2.3.4").ttl(500), a("www", "2.2.2.2").ttl(400), a("www", "5.6.7.8").ttl(400)),
tc("Delete one", a("@", "1.2.3.4").ttl(500), a("www", "5.6.7.8").ttl(400)),
tc("Add back and change ttl", a("www", "5.6.7.8").ttl(700), a("www", "1.2.3.4").ttl(700)),
tc("Change targets and ttls", a("www", "1.1.1.1"), a("www", "2.2.2.2")),
tc("Create wildcard", a("*", "1.2.3.4"), a("www", "1.1.1.1")),
tc("Delete wildcard", a("www", "1.1.1.1")),
type requiresFilter struct {
caps []providers.Capability
}
// CNAMES
tc("Empty"),
tc("Create a CNAME", cname("foo", "google.com.")),
tc("Change it", cname("foo", "google2.com.")),
tc("Change to A record", a("foo", "1.2.3.4")),
tc("Change back to CNAME", cname("foo", "google.com.")),
tc("Record pointing to @", cname("foo", "**current-domain**")),
func requires(c ...providers.Capability) requiresFilter {
return requiresFilter{caps: c}
}
// NS
tc("Empty"),
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
tc("NS Record pointing to @", ns("foo", "**current-domain**")),
type notFilter struct {
names []string
}
// IDNAs
tc("Empty"),
tc("Internationalized name", a("ööö", "1.2.3.4")),
tc("Change IDN", a("ööö", "2.2.2.2")),
tc("Internationalized CNAME Target", cname("a", "ööö.com.")),
tc("IDN CNAME AND Target", cname("öoö", "ööö.企业.")),
func not(n ...string) notFilter {
return notFilter{names: n}
}
// MX
tc("Empty"),
tc("MX record", mx("@", 5, "foo.com.")),
tc("Second MX record, same prio", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com.")),
tc("3 MX", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),
tc("Delete one", mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),
tc("Change to other name", mx("@", 5, "foo2.com."), mx("mail", 15, "foo3.com.")),
tc("Change Preference", mx("@", 7, "foo2.com."), mx("mail", 15, "foo3.com.")),
tc("Record pointing to @", mx("foo", 8, "**current-domain**")),
}
type onlyFilter struct {
names []string
}
// PTR
if !providers.ProviderHasCapability(*providerToRun, providers.CanUsePTR) {
t.Log("Skipping PTR Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("Create PTR record", ptr("4", "foo.com.")),
tc("Modify PTR record", ptr("4", "bar.com.")),
)
}
func only(n ...string) onlyFilter {
return onlyFilter{names: n}
}
// ALIAS
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseAlias) {
t.Log("Skipping ALIAS Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("ALIAS at root", alias("@", "foo.com.")),
tc("change it", alias("@", "foo2.com.")),
tc("ALIAS at subdomain", alias("test", "foo.com.")),
)
}
//
// NAPTR
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseNAPTR) {
t.Log("Skipping NAPTR Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
func makeTests(t *testing.T) []*TestGroup {
sha256hash := strings.Repeat("0123456789abcdef", 4)
sha512hash := strings.Repeat("0123456789abcdef", 8)
reversedSha512 := strings.Repeat("fedcba9876543210", 8)
// Each group of tests begins with testgroup("Title").
// The system will remove any records so that the tests
// begin with a clean slate (i.e. no records).
// Filters:
// Only apply to providers that CanUseAlias.
// requires(providers.CanUseAlias),
// Only apply to ROUTE53 + GANDI_V5:
// only("ROUTE53", "GANDI_V5")
// Only apply to all providers except ROUTE53 + GANDI_V5:
// not("ROUTE53", "GANDI_V5"),
// NOTE: You can't mix not() and only()
// reset(not("ROUTE53"), only("GCLOUD")), // ERROR!
// NOTE: All requires()/not()/only() must appear before any tc().
// tc()
// Each tc() indicates a set of records. The testgroup tries to
// migrate from one tc() to the next. For example the first tc()
// creates some records. The next tc() might list the same records
// but adds 1 new record and omits 1. Therefore migrating to this
// second tc() results in 1 record being created and 1 deleted; but
// for some providers it may be converting 1 record to another.
// Therefore some testgroups are testing the providers ability to
// transition between different states. Others are just testing
// whether or not a certain kind of record can be created and
// deleted.
// clear() is the same as tc("Empty"). It removes all records. You
// can use this to verify a provider can delete all the records in
// the last tc(), or to provide a clean slate for the next tc().
// Each testgroup() begins and ends with clear(), so you don't have
// to list the clear() yourself.
tests := []*TestGroup{
//
// Basic functionality (add/rename/change/delete).
//
testgroup("A",
// These tests aren't specific to "A" records. We're testing
// general ability to add/rename/change/delete any record.
tc("Create an A record", a("@", "1.1.1.1")),
tc("Change it", a("@", "1.2.3.4")),
tc("Add another", a("@", "1.2.3.4"), a("www", "1.2.3.4")),
tc("Add another(same name)", a("@", "1.2.3.4"), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change a ttl", a("@", "1.2.3.4").ttl(1000), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change single target from set", a("@", "1.2.3.4").ttl(1000), a("www", "2.2.2.2"), a("www", "5.6.7.8")),
tc("Change all ttls", a("@", "1.2.3.4").ttl(500), a("www", "2.2.2.2").ttl(400), a("www", "5.6.7.8").ttl(400)),
tc("Delete one", a("@", "1.2.3.4").ttl(500), a("www", "5.6.7.8").ttl(400)),
tc("Add back and change ttl", a("www", "5.6.7.8").ttl(700), a("www", "1.2.3.4").ttl(700)),
tc("Change targets and ttls", a("www", "1.1.1.1"), a("www", "2.2.2.2")),
tc("Create wildcard", a("*", "1.2.3.4"), a("www", "1.1.1.1")),
tc("Delete wildcard", a("www", "1.1.1.1")),
),
testgroup("CNAME",
tc("Create a CNAME", cname("foo", "google.com.")),
tc("Change CNAME target", cname("foo", "google2.com.")),
tc("Change to A record", a("foo", "1.2.3.4")),
tc("Change back to CNAME", cname("foo", "google.com.")),
clear(),
tc("Record pointing to @", cname("foo", "**current-domain**")),
),
testgroup("MX",
not("ACTIVEDIRECTORY_PS"), // Not implemented.
tc("MX record", mx("@", 5, "foo.com.")),
tc("Second MX record, same prio", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com.")),
tc("3 MX", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),
tc("Delete one", mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),
tc("Change to other name", mx("@", 5, "foo2.com."), mx("mail", 15, "foo3.com.")),
tc("Change Preference", mx("@", 7, "foo2.com."), mx("mail", 15, "foo3.com.")),
tc("Record pointing to @", mx("foo", 8, "**current-domain**")),
),
testgroup("NS",
not("DNSIMPLE", "EXOSCALE"),
// DNSIMPLE: Does not support NS records nor subdomains.
// EXOSCALE: FILL IN
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
tc("NS Record pointing to @", ns("foo", "**current-domain**")),
),
testgroup("IGNORE function",
tc("Create some records", txt("foo", "simple"), a("foo", "1.2.3.4")),
tc("Add a new record - ignoring foo", a("bar", "1.2.3.4"), ignore("foo")),
clear(),
tc("Create some records", txt("bar.foo", "simple"), a("bar.foo", "1.2.3.4")),
tc("Add a new record - ignoring *.foo", a("bar", "1.2.3.4"), ignore("*.foo")),
),
testgroup("single TXT",
tc("Create a TXT", txt("foo", "simple")),
tc("Change a TXT", txt("foo", "changed")),
clear(),
tc("Create a TXT with spaces", txt("foo", "with spaces")),
tc("Change a TXT with spaces", txt("foo", "with whitespace ")),
tc("Create 1 TXT as array", txtmulti("foo", []string{"simple"})),
clear(),
tc("Create a 255-byte TXT", txt("foo", strings.Repeat("A", 255))),
),
testgroup("empty TXT", not("DNSIMPLE", "CLOUDFLAREAPI"),
tc("TXT with empty str", txt("foo1", "")),
// https://github.com/StackExchange/dnscontrol/issues/598
// We decided that permitting the TXT target to be an empty
// string is not a requirement (even though RFC1035 permits it).
// In the future we might make it "capability" to
// indicate which vendors support an empty TXT record.
// However at this time there is no pressing need for this
// feature.
),
//
// Tests that exercise the API protocol and/or code
//
testgroup("Case Sensitivity",
// The decoys are required so that there is at least one actual change in each tc.
tc("Create CAPS", mx("BAR", 5, "BAR.com.")),
tc("Downcase label", mx("bar", 5, "BAR.com."), a("decoy", "1.1.1.1")),
tc("Downcase target", mx("bar", 5, "bar.com."), a("decoy", "2.2.2.2")),
tc("Upcase both", mx("BAR", 5, "BAR.COM."), a("decoy", "3.3.3.3")),
),
testgroup("IDNA",
not("SOFTLAYER"),
// SOFTLAYER: fails at direct internationalization, punycode works, of course.
tc("Internationalized name", a("ööö", "1.2.3.4")),
tc("Change IDN", a("ööö", "2.2.2.2")),
tc("Internationalized CNAME Target", cname("a", "ööö.com.")),
),
testgroup("IDNAs in CNAME targets",
not("LINODE"),
// LINODE: hostname validation does not allow the target domain TLD
tc("IDN CNAME AND Target", cname("öoö", "ööö.企业.")),
),
testgroup("page size",
// Tests the paging code of providers. Many providers page at 100.
// Notes:
// - gandi: page size is 100, therefore we test with 99, 100, and 101
// - ns1: free acct only allows 50 records, therefore we skip
// - digitalocean: fails due to rate limiting, not page limits.
not("NS1"),
tc("99 records", manyA("rec%04d", "1.2.3.4", 99)...),
tc("100 records", manyA("rec%04d", "1.2.3.4", 100)...),
tc("101 records", manyA("rec%04d", "1.2.3.4", 101)...),
),
testgroup("Large updates",
// Verify https://github.com/StackExchange/dnscontrol/issues/493
only("ROUTE53"),
tc("600 records", manyA("rec%04d", "1.2.3.4", 600)...),
tc("Update 600 records", manyA("rec%04d", "1.2.3.5", 600)...),
tc("Empty"), // Delete them all
tc("1200 records", manyA("rec%04d", "1.2.3.4", 1200)...),
tc("Update 1200 records", manyA("rec%04d", "1.2.3.5", 1200)...),
),
//
// CanUse* types:
//
testgroup("CAA",
requires(providers.CanUseCAA),
tc("CAA record", caa("@", "issue", 0, "letsencrypt.org")),
tc("CAA change tag", caa("@", "issuewild", 0, "letsencrypt.org")),
tc("CAA change target", caa("@", "issuewild", 0, "example.com")),
tc("CAA change flag", caa("@", "issuewild", 128, "example.com")),
tc("CAA many records",
caa("@", "issue", 0, "letsencrypt.org"),
caa("@", "issuewild", 0, "comodoca.com"),
caa("@", "iodef", 128, "mailto:test@example.com")),
tc("CAA delete", caa("@", "issue", 0, "letsencrypt.org")),
),
testgroup("CAA with ;",
requires(providers.CanUseCAA), not("DIGITALOCEAN"),
// Test support of ";" as a value
tc("CAA many records", caa("@", "issuewild", 0, ";")),
),
testgroup("NAPTR",
requires(providers.CanUseNAPTR),
tc("NAPTR record", naptr("test", 100, 10, "U", "E2U+sip", "!^.*$!sip:customer-service@example.com!", "example.foo.com.")),
tc("NAPTR second record", naptr("test", 102, 10, "U", "E2U+email", "!^.*$!mailto:information@example.com!", "example.foo.com.")),
tc("NAPTR delete record", naptr("test", 100, 10, "U", "E2U+email", "!^.*$!mailto:information@example.com!", "example.foo.com.")),
@ -459,14 +728,14 @@ func makeTests(t *testing.T) []*TestCase {
tc("NAPTR change flags", naptr("test", 103, 20, "A", "E2U+email", "!^.*$!mailto:information@example.com!", "example2.foo.com.")),
tc("NAPTR change service", naptr("test", 103, 20, "A", "E2U+sip", "!^.*$!mailto:information@example.com!", "example2.foo.com.")),
tc("NAPTR change regexp", naptr("test", 103, 20, "A", "E2U+sip", "!^.*$!sip:customer-service@example.com!", "example2.foo.com.")),
)
}
),
// SRV
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseSRV) {
t.Log("Skipping SRV Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
testgroup("PTR", requires(providers.CanUsePTR), not("ACTIVEDIRECTORY_PS"),
tc("Create PTR record", ptr("4", "foo.com.")),
tc("Modify PTR record", ptr("4", "bar.com.")),
),
testgroup("SRV", requires(providers.CanUseSRV), not("ACTIVEDIRECTORY_PS", "CLOUDNS"),
tc("SRV record", srv("_sip._tcp", 5, 6, 7, "foo.com.")),
tc("Second SRV record, same prio", srv("_sip._tcp", 5, 6, 7, "foo.com."), srv("_sip._tcp", 5, 60, 70, "foo2.com.")),
tc("3 SRV", srv("_sip._tcp", 5, 6, 7, "foo.com."), srv("_sip._tcp", 5, 60, 70, "foo2.com."), srv("_sip._tcp", 15, 65, 75, "foo3.com.")),
@ -475,19 +744,13 @@ func makeTests(t *testing.T) []*TestCase {
tc("Change Priority", srv("_sip._tcp", 52, 6, 7, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")),
tc("Change Weight", srv("_sip._tcp", 52, 62, 7, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")),
tc("Change Port", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")),
)
if *providerToRun == "NAMEDOTCOM" || *providerToRun == "HEXONET" || *providerToRun == "EXOSCALE" {
t.Log("Skipping SRV Null Target test because provider does not support them")
} else {
tests = append(tests, tc("Null Target", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, ".")))
}
}
),
testgroup("SRV w/ null target", not("NAMEDOTCOM", "HEXONET", "EXOSCALE"),
tc("Null Target", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, ".")),
),
// SSHFP
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseSSHFP) {
t.Log("Skipping SSHFP Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
testgroup("SSHFP",
requires(providers.CanUseSSHFP),
tc("SSHFP record",
sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")),
tc("SSHFP change algorithm",
@ -501,105 +764,19 @@ func makeTests(t *testing.T) []*TestCase {
sshfp("@", 2, 1, "8888888888888888fb4c84febfa178ad99bdd67c")),
tc("SSHFP delete two",
sshfp("@", 1, 1, "66666666666d75a1fb4c84febfa178ad99bdd67c")),
)
}
),
// CAA
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseCAA) {
t.Log("Skipping CAA Tests because provider does not support them")
} else {
manyRecordsTc := tc("CAA many records", caa("@", "issue", 0, "letsencrypt.org"), caa("@", "issuewild", 0, ";"), caa("@", "iodef", 128, "mailto:test@example.com"))
// Digitalocean doesn't support ";" as value for CAA records
if *providerToRun == "DIGITALOCEAN" {
manyRecordsTc = tc("CAA many records", caa("@", "issue", 0, "letsencrypt.org"), caa("@", "issuewild", 0, "comodoca.com"), caa("@", "iodef", 128, "mailto:test@example.com"))
}
tests = append(tests, tc("Empty"),
tc("CAA record", caa("@", "issue", 0, "letsencrypt.org")),
tc("CAA change tag", caa("@", "issuewild", 0, "letsencrypt.org")),
tc("CAA change target", caa("@", "issuewild", 0, "example.com")),
tc("CAA change flag", caa("@", "issuewild", 128, "example.com")),
manyRecordsTc,
tc("CAA delete", caa("@", "issue", 0, "letsencrypt.org")),
)
}
// TLSA
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseTLSA) {
t.Log("Skipping TLSA Tests because provider does not support them")
} else {
sha256hash := strings.Repeat("0123456789abcdef", 4)
sha512hash := strings.Repeat("0123456789abcdef", 8)
reversedSha512 := strings.Repeat("fedcba9876543210", 8)
tests = append(tests, tc("Empty"),
testgroup("TLSA",
requires(providers.CanUseTLSA),
tc("TLSA record", tlsa("_443._tcp", 3, 1, 1, sha256hash)),
tc("TLSA change usage", tlsa("_443._tcp", 2, 1, 1, sha256hash)),
tc("TLSA change selector", tlsa("_443._tcp", 2, 0, 1, sha256hash)),
tc("TLSA change matchingtype", tlsa("_443._tcp", 2, 0, 2, sha512hash)),
tc("TLSA change certificate", tlsa("_443._tcp", 2, 0, 2, reversedSha512)),
)
}
),
// Case
tests = append(tests, tc("Empty"),
tc("Create CAPS", mx("BAR", 5, "BAR.com.")),
tc("Downcase label", mx("bar", 5, "BAR.com."), a("decoy", "1.1.1.1")),
tc("Downcase target", mx("bar", 5, "bar.com."), a("decoy", "2.2.2.2")),
tc("Upcase both", mx("BAR", 5, "BAR.COM."), a("decoy", "3.3.3.3")),
// The decoys are required so that there is at least one actual change in each tc.
)
// Test large zonefiles.
// Mostly to test paging. Many providers page at 100
// Known page sizes:
// - gandi: 100
skip := map[string]bool{
"NS1": true, // ns1 free acct only allows 50 records
}
if skip[*providerToRun] {
t.Log("Skipping Large record count Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("99 records", manyA("rec%04d", "1.2.3.4", 99)...),
tc("100 records", manyA("rec%04d", "1.2.3.4", 100)...),
tc("101 records", manyA("rec%04d", "1.2.3.4", 101)...),
)
}
// NB(tlim): To temporarily skip most of the tests, insert a line like this:
//tests = nil
// TXT (single)
tests = append(tests, tc("Empty"),
tc("Create a TXT", txt("foo", "simple")),
tc("Change a TXT", txt("foo", "changed")),
tc("Empty"),
tc("Create a TXT with spaces", txt("foo", "with spaces")),
tc("Change a TXT with spaces", txt("foo", "with whitespace")),
tc("Create 1 TXT as array", txtmulti("foo", []string{"simple"})),
tc("Empty"),
tc("Create a 255-byte TXT", txt("foo", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")),
)
// FUTURE(tal): https://github.com/StackExchange/dnscontrol/issues/598
// We decided that handling an empty TXT string is not a
// requirement. In the future we might make it a "capability" to
// indicate which vendors fully support RFC 1035, which requires
// that a TXT string can be empty.
//
// // TXT (empty)
// if (provider supports empty txt strings) {
// tests = append(tests, tc("Empty"),
// tc("TXT with empty str", txt("foo1", "")),
// )
// }
// TXTMulti
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseTXTMulti) {
t.Log("Skipping TXTMulti Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
testgroup("TXTMulti",
requires(providers.CanUseTXTMulti),
tc("Create TXTMulti 1",
txtmulti("foo1", []string{"simple"}),
),
@ -622,61 +799,38 @@ func makeTests(t *testing.T) []*TestCase {
txtmulti("foo2", []string{"fun", "two"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
tc("Empty"),
tc("3x255-byte TXTMulti",
txtmulti("foo3", []string{"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"}),
),
)
}
txtmulti("foo3", []string{strings.Repeat("X", 255), strings.Repeat("Y", 255), strings.Repeat("Z", 255)})),
),
// ignored records
tests = append(tests, tc("Empty"),
tc("Create some records", txt("foo", "simple"), a("foo", "1.2.3.4")),
tc("Add a new record - ignoring foo", a("bar", "1.2.3.4"), ignore("foo")),
)
//
// Pseudo rtypes:
//
tests = append(tests, tc("Empty"),
tc("Create some records", txt("bar.foo", "simple"), a("bar.foo", "1.2.3.4")),
tc("Add a new record - ignoring *.foo", a("bar", "1.2.3.4"), ignore("*.foo")),
)
testgroup("ALIAS",
requires(providers.CanUseAlias),
tc("ALIAS at root", alias("@", "foo.com.")),
tc("change it", alias("@", "foo2.com.")),
tc("ALIAS at subdomain", alias("test", "foo.com.")),
),
// R53_ALIAS
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseRoute53Alias) {
t.Log("Skipping Route53 ALIAS Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("create dependent records", a("foo", "1.2.3.4"), a("quux", "2.3.4.5")),
tc("ALIAS to A record in same zone", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "foo.**current-domain**")),
tc("change it", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "quux.**current-domain**")),
)
}
// test r53 for very very large batch sizes
if *providerToRun == "ROUTE53" {
tests = append(tests, tc("Empty"),
tc("600 records", manyA("rec%04d", "1.2.3.4", 600)...),
tc("Update 600 records", manyA("rec%04d", "1.2.3.5", 600)...),
tc("Empty"),
tc("1200 records", manyA("rec%04d", "1.2.3.4", 1200)...),
tc("Update 1200 records", manyA("rec%04d", "1.2.3.5", 1200)...),
)
}
// AZURE_ALIAS
if !providers.ProviderHasCapability(*providerToRun, providers.CanUseAzureAlias) {
t.Log("Skipping AZURE_ALIAS Tests because provider does not support them")
} else {
t.Log("SubscriptionID: ")
tests = append(tests, tc("Empty"),
testgroup("AZURE_ALIAS",
requires(providers.CanUseAzureAlias),
tc("create dependent A records", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5")),
tc("ALIAS to A record in same zone", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5"), azureAlias("bar.a", "A", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/A/foo.a")),
tc("change it", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5"), azureAlias("bar.a", "A", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/A/quux.a")),
tc("create dependent CNAME records", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com")),
tc("ALIAS to CNAME record in same zone", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com"), azureAlias("bar", "CNAME", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/CNAME/foo.cname")),
tc("change it", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com"), azureAlias("bar.cname", "CNAME", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/CNAME/quux.cname")),
)
),
testgroup("R53_ALIAS",
requires(providers.CanUseRoute53Alias),
tc("create dependent records", a("foo", "1.2.3.4"), a("quux", "2.3.4.5")),
tc("ALIAS to A record in same zone", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "foo.**current-domain**")),
tc("change it", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "quux.**current-domain**")),
),
}
// Empty last
tests = append(tests, tc("Empty"))
return tests
}

View file

@ -1,94 +1,87 @@
{
"ACTIVEDIRECTORY_PS": {
"ADServer": "$AD_SERVER",
"domain": "$AD_DOMAIN",
"knownFailures": "29,30,31,32,33,34,35,38,39,40,41,48,50,51,52"
"domain": "$AD_DOMAIN"
},
"AZURE_DNS": {
"SubscriptionID": "$AZURE_SUBSCRIPTION_ID",
"ResourceGroup": "$AZURE_RESOURCE_GROUP",
"TenantID": "$AZURE_TENANT_ID",
"ClientID": "$AZURE_CLIENT_ID",
"ClientSecret": "$AZURE_CLIENT_SECRET",
"ResourceGroup": "$AZURE_RESOURCE_GROUP",
"SubscriptionID": "$AZURE_SUBSCRIPTION_ID",
"TenantID": "$AZURE_TENANT_ID",
"domain": "$AZURE_DOMAIN"
},
"BIND": {
"domain": "example.com"
},
"CLOUDNS": {
"auth-id": "$CLOUDNS_AUTH_ID",
"auth-password": "$CLOUDNS_AUTH_PASSWORD",
"domain": "$CLOUDNS_DOMAIN",
"knownFailures": "52"
"CLOUDFLAREAPI": {
"apitoken": "$CF_TOKEN",
"domain": "$CF_DOMAIN"
},
"CLOUDFLAREAPI_OLD": {
"apikey": "$CF_KEY",
"apiuser": "$CF_USER",
"domain": "$CF_DOMAIN"
},
"CLOUDFLAREAPI": {
"apitoken": "$CF_TOKEN",
"domain": "$CF_DOMAIN"
"CLOUDNS": {
"auth-id": "$CLOUDNS_AUTH_ID",
"auth-password": "$CLOUDNS_AUTH_PASSWORD",
"domain": "$CLOUDNS_DOMAIN"
},
"DIGITALOCEAN": {
"token": "$DO_TOKEN",
"domain": "$DO_DOMAIN"
"domain": "$DO_DOMAIN",
"token": "$DO_TOKEN"
},
"DNSIMPLE": {
"COMMENT": "20-22: no ns records managable. Not even for subdomains.",
"baseurl": "https://api.sandbox.dnsimple.com",
"domain": "$DNSIMPLE_DOMAIN",
"knownFailures": "20,21,22",
"token": "$DNSIMPLE_TOKEN"
},
"EXOSCALE": {
"dns-endpoint": "https://api.exoscale.ch/dns",
"apikey": "$EXOSCALE_API_KEY",
"secretkey": "$EXOSCALE_SECRET_KEY",
"dns-endpoint": "https://api.exoscale.ch/dns",
"domain": "$EXOSCALE_DOMAIN",
"knownFailures": "20,21,22"
"secretkey": "$EXOSCALE_SECRET_KEY"
},
"GANDI": {
"COMMENT": "5: gandi does not accept TTLs less than 300",
"apikey": "$GANDI_KEY",
"domain": "$GANDI_DOMAIN",
"knownFailures": "5"
"domain": "$GANDI_DOMAIN"
},
"GANDI-LIVEDNS": {
"COMMENT": "5: gandi does not accept TTLs less than 300",
"apikey": "$GANDILIVE_KEY",
"domain": "$GANDILIVE_DOMAIN",
"knownFailures": "5"
"domain": "$GANDILIVE_DOMAIN"
},
"GANDI_V5": {
"apikey": "$GANDI_V5_APIKEY",
"domain": "$GANDI_V5_DOMAIN"
},
"GCLOUD": {
"type": "$GCLOUD_TYPE",
"client_email": "$GCLOUD_EMAIL",
"domain": "$GCLOUD_DOMAIN",
"private_key": "$GCLOUD_PRIVATEKEY",
"project_id": "$GCLOUD_PROJECT"
"project_id": "$GCLOUD_PROJECT",
"type": "$GCLOUD_TYPE"
},
"HEXONET": {
"apientity": "$HEXONET_ENTITY",
"apilogin": "$HEXONET_UID",
"apipassword": "$HEXONET_PW",
"apientity": "$HEXONET_ENTITY",
"debugmode": "$HEXONET_DEBUGMODE",
"ipaddress": "$HEXONET_IP",
"domain": "dnscontrol.com"
"domain": "dnscontrol.com",
"ipaddress": "$HEXONET_IP"
},
"LINODE": {
"COMMENT": "25: Linode's hostname validation does not allow the target domain TLD",
"token": "$LINODE_TOKEN",
"domain": "$LINODE_DOMAIN",
"knownFailures": "27"
"token": "$LINODE_TOKEN"
},
"NS1": {
"domain": "$NS1_DOMAIN",
"api_token": "$NS1_TOKEN"
"NAMECHEAP": {
"apikey": "$NAMECHEAP_KEY",
"apiuser": "$NAMECHEAP_USER",
"domain": "$NAMECHEAP_DOMAIN"
},
"NAMEDOTCOM": {
"apikey": "$NAMEDOTCOM_KEY",
@ -96,14 +89,19 @@
"apiuser": "$NAMEDOTCOM_USER",
"domain": "$NAMEDOTCOM_DOMAIN"
},
"NAMECHEAP": {
"apikey": "$NAMECHEAP_KEY",
"apiuser": "$NAMECHEAP_USER",
"domain": "$NAMECHEAP_DOMAIN"
"NS1": {
"api_token": "$NS1_TOKEN",
"domain": "$NS1_DOMAIN"
},
"OCTODNS": {
"domain": "example.com",
"directory": "config"
"directory": "config",
"domain": "example.com"
},
"OVH": {
"app-key": "$OVH_APP_KEY",
"app-secret-key": "$OVH_APP_SECRET_KEY",
"consumer-key": "$OVH_CONSUMER_KEY",
"domain": "$OVH_DOMAIN"
},
"ROUTE53": {
"KeyId": "$R53_KEY_ID",
@ -112,19 +110,12 @@
},
"SOFTLAYER": {
"COMMENT": "22-25 softlayer fails at direct internationalization, puncode works though",
"knownFailures": "22,23,24,25",
"api_key": "$SL_API_KEY",
"domain": "$SL_DOMAIN",
"username": "$SL_USERNAME",
"api_key": "$SL_API_KEY"
"username": "$SL_USERNAME"
},
"VULTR": {
"token": "$VULTR_TOKEN",
"domain": "$VULTR_DOMAIN"
},
"OVH": {
"app-key": "$OVH_APP_KEY",
"app-secret-key": "$OVH_APP_SECRET_KEY",
"consumer-key": "$OVH_CONSUMER_KEY",
"domain": "$OVH_DOMAIN"
"domain": "$VULTR_DOMAIN",
"token": "$VULTR_TOKEN"
}
}

View file

@ -1,3 +1,5 @@
//go:generate stringer -type=Capability
package providers
import (

View file

@ -0,0 +1,38 @@
// Code generated by "stringer -type=Capability"; DO NOT EDIT.
package providers
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CanUseAlias-0]
_ = x[CanUseCAA-1]
_ = x[CanUsePTR-2]
_ = x[CanUseNAPTR-3]
_ = x[CanUseSRV-4]
_ = x[CanUseSSHFP-5]
_ = x[CanUseTLSA-6]
_ = x[CanUseTXTMulti-7]
_ = x[CanAutoDNSSEC-8]
_ = x[CantUseNOPURGE-9]
_ = x[DocOfficiallySupported-10]
_ = x[DocDualHost-11]
_ = x[DocCreateDomains-12]
_ = x[CanUseRoute53Alias-13]
_ = x[CanGetZones-14]
_ = x[CanUseAzureAlias-15]
}
const _Capability_name = "CanUseAliasCanUseCAACanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias"
var _Capability_index = [...]uint8{0, 11, 20, 29, 40, 49, 60, 70, 84, 97, 111, 133, 144, 160, 178, 189, 205}
func (i Capability) String() string {
if i >= Capability(len(_Capability_index)-1) {
return "Capability(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Capability_name[_Capability_index[i]:_Capability_index[i+1]]
}

View file

@ -40,7 +40,7 @@ func init() {
// features declares which features and options are available.
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(),
providers.CanUseAlias: providers.Can("Only on the bare domain. Otherwise CNAME will be substituted"),
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
@ -155,6 +155,11 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
if rec.Type == "ALIAS" && rec.Name != "@" {
// GANDI only permits aliases on a naked domain.
// Therefore, we change this to a CNAME.
rec.Type = "CNAME"
}
if rec.TTL < 300 {
printer.Warnf("Gandi does not support ttls < 300. Setting %s from %d to 300\n", rec.GetLabelFQDN(), rec.TTL)
rec.TTL = 300

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ package imports // import "golang.org/x/tools/imports"
import (
"go/build"
"log"
"os"
intimp "golang.org/x/tools/internal/imports"
@ -47,7 +48,6 @@ func Process(filename string, src []byte, opt *Options) ([]byte, error) {
GO111MODULE: os.Getenv("GO111MODULE"),
GOPROXY: os.Getenv("GOPROXY"),
GOSUMDB: os.Getenv("GOSUMDB"),
Debug: Debug,
LocalPrefix: LocalPrefix,
},
AllErrors: opt.AllErrors,
@ -57,6 +57,9 @@ func Process(filename string, src []byte, opt *Options) ([]byte, error) {
TabIndent: opt.TabIndent,
TabWidth: opt.TabWidth,
}
if Debug {
intopt.Env.Logf = log.Printf
}
return intimp.Process(filename, src, intopt)
}

View file

@ -76,8 +76,9 @@ func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) e
}
func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode) {
// golang.org/issue/15653
dirent := (*syscall.Dirent)(unsafe.Pointer(&buf[0]))
// golang.org/issue/37269
dirent := &syscall.Dirent{}
copy((*[unsafe.Sizeof(syscall.Dirent{})]byte)(unsafe.Pointer(dirent))[:], buf)
if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
}

121
vendor/golang.org/x/tools/internal/gocommand/invoke.go generated vendored Normal file
View file

@ -0,0 +1,121 @@
// Package gocommand is a helper for calling the go command.
package gocommand
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// An Invocation represents a call to the go command.
type Invocation struct {
Verb string
Args []string
BuildFlags []string
Env []string
WorkingDir string
Logf func(format string, args ...interface{})
}
// Run runs the invocation, returning its stdout and an error suitable for
// human consumption, including stderr.
func (i *Invocation) Run(ctx context.Context) (*bytes.Buffer, error) {
stdout, _, friendly, _ := i.RunRaw(ctx)
return stdout, friendly
}
// RunRaw is like Run, but also returns the raw stderr and error for callers
// that want to do low-level error handling/recovery.
func (i *Invocation) RunRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) {
log := i.Logf
if log == nil {
log = func(string, ...interface{}) {}
}
goArgs := []string{i.Verb}
switch i.Verb {
case "mod":
// mod needs the sub-verb before build flags.
goArgs = append(goArgs, i.Args[0])
goArgs = append(goArgs, i.BuildFlags...)
goArgs = append(goArgs, i.Args[1:]...)
case "env":
// env doesn't take build flags.
goArgs = append(goArgs, i.Args...)
default:
goArgs = append(goArgs, i.BuildFlags...)
goArgs = append(goArgs, i.Args...)
}
cmd := exec.Command("go", goArgs...)
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
cmd.Stdout = stdout
cmd.Stderr = stderr
// On darwin the cwd gets resolved to the real path, which breaks anything that
// expects the working directory to keep the original path, including the
// go command when dealing with modules.
// The Go stdlib has a special feature where if the cwd and the PWD are the
// same node then it trusts the PWD, so by setting it in the env for the child
// process we fix up all the paths returned by the go command.
cmd.Env = append(append([]string{}, i.Env...), "PWD="+i.WorkingDir)
cmd.Dir = i.WorkingDir
defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
rawError = runCmdContext(ctx, cmd)
friendlyError = rawError
if rawError != nil {
// Check for 'go' executable not being found.
if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
friendlyError = fmt.Errorf("go command required, not found: %v", ee)
}
if ctx.Err() != nil {
friendlyError = ctx.Err()
}
friendlyError = fmt.Errorf("err: %v: stderr: %s", rawError, stderr)
}
return
}
// runCmdContext is like exec.CommandContext except it sends os.Interrupt
// before os.Kill.
func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
if err := cmd.Start(); err != nil {
return err
}
resChan := make(chan error, 1)
go func() {
resChan <- cmd.Wait()
}()
select {
case err := <-resChan:
return err
case <-ctx.Done():
}
// Cancelled. Interrupt and see if it ends voluntarily.
cmd.Process.Signal(os.Interrupt)
select {
case err := <-resChan:
return err
case <-time.After(time.Second):
}
// Didn't shut down in response to interrupt. Kill it hard.
cmd.Process.Kill()
return <-resChan
}
func cmdDebugStr(cmd *exec.Cmd) string {
env := make(map[string]string)
for _, kv := range cmd.Env {
split := strings.Split(kv, "=")
k, v := split[0], split[1]
env[k] = v
}
return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args)
}

View file

@ -23,8 +23,10 @@ import (
// Options controls the behavior of a Walk call.
type Options struct {
Debug bool // Enable debug logging
ModulesEnabled bool // Search module caches. Also disables legacy goimports ignore rules.
// If Logf is non-nil, debug logging is enabled through this function.
Logf func(format string, args ...interface{})
// Search module caches. Also disables legacy goimports ignore rules.
ModulesEnabled bool
}
// RootType indicates the type of a Root.
@ -80,14 +82,14 @@ func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root
// walkDir creates a walker and starts fastwalk with this walker.
func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) {
if _, err := os.Stat(root.Path); os.IsNotExist(err) {
if opts.Debug {
log.Printf("skipping nonexistent directory: %v", root.Path)
if opts.Logf != nil {
opts.Logf("skipping nonexistent directory: %v", root.Path)
}
return
}
start := time.Now()
if opts.Debug {
log.Printf("gopathwalk: scanning %s", root.Path)
if opts.Logf != nil {
opts.Logf("gopathwalk: scanning %s", root.Path)
}
w := &walker{
root: root,
@ -100,8 +102,8 @@ func walkDir(root Root, add func(Root, string), skip func(root Root, dir string)
log.Printf("gopathwalk: scanning directory %v: %v", root.Path, err)
}
if opts.Debug {
log.Printf("gopathwalk: scanned %s in %v", root.Path, time.Since(start))
if opts.Logf != nil {
opts.Logf("gopathwalk: scanned %s in %v", root.Path, time.Since(start))
}
}
@ -130,11 +132,11 @@ func (w *walker) init() {
full := filepath.Join(w.root.Path, p)
if fi, err := os.Stat(full); err == nil {
w.ignoredDirs = append(w.ignoredDirs, fi)
if w.opts.Debug {
log.Printf("Directory added to ignore list: %s", full)
if w.opts.Logf != nil {
w.opts.Logf("Directory added to ignore list: %s", full)
}
} else if w.opts.Debug {
log.Printf("Error statting ignored directory: %v", err)
} else if w.opts.Logf != nil {
w.opts.Logf("Error statting ignored directory: %v", err)
}
}
}
@ -145,11 +147,11 @@ func (w *walker) init() {
func (w *walker) getIgnoredDirs(path string) []string {
file := filepath.Join(path, ".goimportsignore")
slurp, err := ioutil.ReadFile(file)
if w.opts.Debug {
if w.opts.Logf != nil {
if err != nil {
log.Print(err)
w.opts.Logf("%v", err)
} else {
log.Printf("Read %s", file)
w.opts.Logf("Read %s", file)
}
}
if err != nil {

View file

@ -14,7 +14,6 @@ import (
"go/token"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
@ -22,11 +21,11 @@ import (
"strconv"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/gopathwalk"
)
@ -263,7 +262,7 @@ type pass struct {
// loadPackageNames saves the package names for everything referenced by imports.
func (p *pass) loadPackageNames(imports []*ImportInfo) error {
if p.env.Debug {
if p.env.Logf != nil {
p.env.Logf("loading package names for %v packages", len(imports))
defer func() {
p.env.Logf("done loading package names for %v packages", len(imports))
@ -335,7 +334,7 @@ func (p *pass) load() ([]*ImportFix, bool) {
if p.loadRealPackageNames {
err := p.loadPackageNames(append(imports, p.candidates...))
if err != nil {
if p.env.Debug {
if p.env.Logf != nil {
p.env.Logf("loading package names: %v", err)
}
return nil, false
@ -529,7 +528,7 @@ func getFixes(fset *token.FileSet, f *ast.File, filename string, env *ProcessEnv
return nil, err
}
srcDir := filepath.Dir(abs)
if env.Debug {
if env.Logf != nil {
env.Logf("fixImports(filename=%q), abs=%q, srcDir=%q ...", filename, abs, srcDir)
}
@ -747,7 +746,6 @@ func getPackageExports(ctx context.Context, wrapped func(PackageExport), searchP
// the go command, the go/build package, etc.
type ProcessEnv struct {
LocalPrefix string
Debug bool
BuildFlags []string
@ -756,7 +754,7 @@ type ProcessEnv struct {
GOPATH, GOROOT, GO111MODULE, GOPROXY, GOFLAGS, GOSUMDB string
WorkingDir string
// Logf is the default logger for the ProcessEnv.
// If Logf is non-nil, debug logging is enabled through this function.
Logf func(format string, args ...interface{})
resolver Resolver
@ -792,7 +790,7 @@ func (e *ProcessEnv) GetResolver() Resolver {
if e.resolver != nil {
return e.resolver
}
out, err := e.invokeGo("env", "GOMOD")
out, err := e.invokeGo(context.TODO(), "env", "GOMOD")
if err != nil || len(bytes.TrimSpace(out.Bytes())) == 0 {
e.resolver = newGopathResolver(e)
return e.resolver
@ -823,38 +821,16 @@ func (e *ProcessEnv) buildContext() *build.Context {
return &ctx
}
func (e *ProcessEnv) invokeGo(verb string, args ...string) (*bytes.Buffer, error) {
goArgs := []string{verb}
if verb != "env" {
goArgs = append(goArgs, e.BuildFlags...)
func (e *ProcessEnv) invokeGo(ctx context.Context, verb string, args ...string) (*bytes.Buffer, error) {
inv := gocommand.Invocation{
Verb: verb,
Args: args,
BuildFlags: e.BuildFlags,
Env: e.env(),
Logf: e.Logf,
WorkingDir: e.WorkingDir,
}
goArgs = append(goArgs, args...)
cmd := exec.Command("go", goArgs...)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Env = e.env()
cmd.Dir = e.WorkingDir
if e.Debug {
defer func(start time.Time) { e.Logf("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("running go: %v (stderr:\n%s)", err, stderr)
}
return stdout, nil
}
func cmdDebugStr(cmd *exec.Cmd) string {
env := make(map[string]string)
for _, kv := range cmd.Env {
split := strings.Split(kv, "=")
k, v := split[0], split[1]
env[k] = v
}
return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args)
return inv.Run(ctx)
}
func addStdlibCandidates(pass *pass, refs references) {
@ -1261,7 +1237,7 @@ func (r *gopathResolver) scan(ctx context.Context, callback *scanCallback) error
case <-r.scanSema:
}
defer func() { r.scanSema <- struct{}{} }()
gopathwalk.Walk(roots, add, gopathwalk.Options{Debug: r.env.Debug, ModulesEnabled: false})
gopathwalk.Walk(roots, add, gopathwalk.Options{Logf: r.env.Logf, ModulesEnabled: false})
close(scanDone)
}()
select {
@ -1365,7 +1341,7 @@ func loadExportsFromFiles(ctx context.Context, env *ProcessEnv, dir string, incl
}
}
if env.Debug {
if env.Logf != nil {
sortedExports := append([]string(nil), exports...)
sort.Strings(sortedExports)
env.Logf("loaded exports in dir %v (package %v): %v", dir, pkgName, strings.Join(sortedExports, ", "))
@ -1381,7 +1357,7 @@ func findImport(ctx context.Context, pass *pass, candidates []pkgDistance, pkgNa
// ones. Note that this sorts by the de-vendored name, so
// there's no "penalty" for vendoring.
sort.Sort(byDistanceOrImportPathShortLength(candidates))
if pass.env.Debug {
if pass.env.Logf != nil {
for i, c := range candidates {
pass.env.Logf("%s candidate %d/%d: %v in %v", pkgName, i+1, len(candidates), c.pkg.importPathShort, c.pkg.dir)
}
@ -1419,14 +1395,14 @@ func findImport(ctx context.Context, pass *pass, candidates []pkgDistance, pkgNa
wg.Done()
}()
if pass.env.Debug {
if pass.env.Logf != nil {
pass.env.Logf("loading exports in dir %s (seeking package %s)", c.pkg.dir, pkgName)
}
// If we're an x_test, load the package under test's test variant.
includeTest := strings.HasSuffix(pass.f.Name.Name, "_test") && c.pkg.dir == pass.srcDir
_, exports, err := pass.env.GetResolver().loadExports(ctx, c.pkg, includeTest)
if err != nil {
if pass.env.Debug {
if pass.env.Logf != nil {
pass.env.Logf("loading exports in dir %s (seeking package %s): %v", c.pkg.dir, pkgName, err)
}
resc <- nil

View file

@ -146,7 +146,7 @@ func (r *ModuleResolver) init() error {
}
func (r *ModuleResolver) initAllMods() error {
stdout, err := r.env.invokeGo("list", "-m", "-json", "...")
stdout, err := r.env.invokeGo(context.TODO(), "list", "-m", "-json", "...")
if err != nil {
return err
}
@ -156,12 +156,14 @@ func (r *ModuleResolver) initAllMods() error {
return err
}
if mod.Dir == "" {
if r.env.Debug {
if r.env.Logf != nil {
r.env.Logf("module %v has not been downloaded and will be ignored", mod.Path)
}
// Can't do anything with a module that's not downloaded.
continue
}
// golang/go#36193: the go command doesn't always clean paths.
mod.Dir = filepath.Clean(mod.Dir)
r.modsByModPath = append(r.modsByModPath, mod)
r.modsByDir = append(r.modsByDir, mod)
if mod.Main {
@ -468,7 +470,7 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
if r.scannedRoots[root] {
continue
}
gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Debug: r.env.Debug, ModulesEnabled: true})
gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: r.env.Logf, ModulesEnabled: true})
r.scannedRoots[root] = true
}
close(scanDone)
@ -581,7 +583,7 @@ func (r *ModuleResolver) scanDirForPackage(root gopathwalk.Root, dir string) dir
}
modPath, err := module.UnescapePath(filepath.ToSlash(matches[1]))
if err != nil {
if r.env.Debug {
if r.env.Logf != nil {
r.env.Logf("decoding module cache path %q: %v", subdir, err)
}
return directoryPackageInfo{
@ -697,7 +699,7 @@ func getMainModuleAnd114(env *ProcessEnv) (*ModuleJSON, bool, error) {
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
stdout, err := env.invokeGo("list", "-m", "-f", format)
stdout, err := env.invokeGo(context.TODO(), "list", "-m", "-f", format)
if err != nil {
return nil, false, nil
}

5
vendor/modules.txt vendored
View file

@ -333,7 +333,7 @@ golang.org/x/crypto/pkcs12/internal/rc2
## explicit
golang.org/x/mod/module
golang.org/x/mod/semver
# golang.org/x/net v0.0.0-20200222125558-5a598a2470a0
# golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
## explicit
golang.org/x/net/bpf
golang.org/x/net/context
@ -374,11 +374,12 @@ golang.org/x/text/unicode/bidi
golang.org/x/text/unicode/norm
# golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
golang.org/x/time/rate
# golang.org/x/tools v0.0.0-20200212213243-2ee7536ab1cc
# golang.org/x/tools v0.0.0-20200303202040-658b03bcd3d8
## explicit
golang.org/x/tools/go/ast/astutil
golang.org/x/tools/imports
golang.org/x/tools/internal/fastwalk
golang.org/x/tools/internal/gocommand
golang.org/x/tools/internal/gopathwalk
golang.org/x/tools/internal/imports
# golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543